I am developing a client-server application in Java using Websocket. Currently, all the client messages are processed using switch-case as shown below.
@OnMessage
public String onMessage(String unscrambledWord, Session session) {
switch (unscrambledWord) {
case "start":
logger.info("Starting the game by sending first word");
String scrambledWord = WordRepository.getInstance().getRandomWord().getScrambledWord();
session.getUserProperties().put("scrambledWord", scrambledWord);
return scrambledWord;
case "quit":
logger.info("Quitting the game");
try {
session.close(new CloseReason(CloseCodes.NORMAL_CLOSURE, "Game finished"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
String scrambledWord = (String) session.getUserProperties().get("scrambledWord");
return checkLastWordAndSendANewWord(scrambledWord, unscrambledWord, session);
}
The server has to process more than 50 different requests from client and that results in more than 50 case statements. And in future, I expect it to grow. Is there any better way to process Websocket messages from client? Or, is this how it is usually done?
I read somewhere about the use of hashtable to avoid long switch-case scenario by mapping to function pointers. Is this possible in Java? Or, is there any better solutions?
Thanks.
As mentioned in the comments, one of websockets drawback is that you'll to specify the communication protocol yourself. AFAIK, the huge switch is the best option. To improve code readability and maintenance, I'll suggest to use encoders and decoders. Then, your problem becomes: how should I design my messages?
Your game looks like Scrabble. I don't know how to play Scrabble so let's take the example of card game with money. Let's assume you have three types of actions:
Then your messages can look like
public class AbstractAction{
// not relevant for global action but let's put that aside for the example
public abstract void endTurn();
}
public class GlobalAction{
// ...
}
public class MoneyAction{
enum Action{
PLACE_BET, PLACE_MAX_BET, SPLIT_BET, ...;
}
private MoneyAction.Action action;
// ...
}
public class CardAction{
// ...
}
Once your decoder and encoders are properly defined, your switch would be easier to read and easier to maintain. In my project, the code would look like this:
@ServerEndPoint(value = ..., encoders = {...}, decoders = {...})
public class ServerEndPoint{
@OnOpen
public void onOpen(Session session){
// ...
}
@OnClose
public void onClose(Session session){
// ...
}
@OnMessage
public void onMessage(Session session, AbstractAction action){
// I'm checking the class here but you
// can use different check such as a
// specific attribute
if(action instanceof GlobalAction){
// do some stuff
}
else if (action instanceof CardAction){
// do some stuff
}
else if (action instance of MoneyAction){
MoneyAction moneyAction = (MoneyAction) action;
switch(moneyAction.getAction()){
case PLACE_BET:
double betValue = moneyAction.getValue();
// do some stuff here
break;
case SPLIT_BET:
doSomeVeryComplexStuff(moneyAction);
break;
}
}
}
private void doSomeVeryComplexStuff(MoneyAction moneyAction){
// ... do something very complex ...
}
}
I prefer this approach because:
@OnMessage
method can be read as a summary of the protocol but details should not be displayed here. Each case
must contain few lines only. To go further beyond code readability, maintenance and efficiency, you can use a SessionHandler to intercept some CDI event if this can improve your code. I gave an example in this answer . If you need a more advanced example, Oracle provides a great tutorial about it . It might help you to improve your code.
After a bit of testing and study, I found two alternatives to avoid long switch case scenario.
Using Anonymous Class
Anonymous class method is the norm and following code shows how to implement it. I used Runnable in this example. If more control is required, create a custom interface.
public class ClientMessageHandler {
private final HashMap<String, Runnable> taskList = new HashMap<>();
ClientMessageHandler() {
this.populateTaskList();
}
private void populateTaskList() {
// Populate the map with client request as key
// and the task performing objects as value
taskList.put("action1", new Runnable() {
@Override
public void run() {
// define the action to perform.
}
});
//Populate map with all the tasks
}
public void onMessageReceived(JSONObject clientRequest) throws JSONException {
Runnable taskToExecute = taskList.get(clientRequest.getString("task"));
if (taskToExecute == null)
return;
taskToExecute.run();
}
}
Major drawback of this method is object creation. Say, we have 100 different tasks to perform. This Anonymous class approach will result in creating 100 objects for a single client. Too much object creation is not affordable for my application, where there will be more than 5,000 active concurrent connections. Have a look at this article http://blogs.microsoft.co.il/gilf/2009/11/22/applying-strategy-pattern-instead-of-using-switch-statements/
Reflection with Annotation
I really like this approach. I created a custom annotation to represent the tasks performed by methods. There is no overhead of object creation, like in Strategy pattern method, as tasks are performed by a single class.
Annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TaskAnnotation {
public String value();
}
The code given below maps the client request keys to the methods which process the task. Here, map is instantiated and populated only once.
public static final HashMap<String, Method> taskList = new HashMap<>();
public static void main(String[] args) throws Exception {
// Retrieves declared methods from ClientMessageHandler class
Method[] classMethods = ClientMessageHandler.class.getDeclaredMethods();
for (Method method : classMethods) {
// We will iterate through the declared methods and look for
// the methods annotated with our TaskAnnotation
TaskAnnotation annot = method.getAnnotation(TaskAnnotation.class);
if (annot != null) {
// if a method with TaskAnnotation is found, its annotation
// value is mapped to that method.
taskList.put(annot.value(), method);
}
}
// Start server
}
Now finally, our ClientMessageHandler class looks like the following
public class ClientMessageHandler {
public void onMessageReceived(JSONObject clientRequest) throws JSONException {
// Retrieve the Method corresponding to the task from map
Method method = taskList.get(clientRequest.getString("task"));
if (method == null)
return;
try {
// Invoke the Method for this object, if Method corresponding
// to client request is found
method.invoke(this);
} catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
logger.error(e);
}
}
@TaskAnnotation("task1")
public void processTaskOne() {
}
@TaskAnnotation("task2")
public void processTaskTwo() {
}
// Methods for different tasks, annotated with the corresponding
// clientRequest code
}
Major drawback of this approach is the performance hit. This approach is slow compared to Direct Method calling approach. Moreover, many articles are suggesting to stay away from Reflection, unless we are dealing with dynamic programming.
Read these answers to know more about reflection What is reflection and why is it useful?
Reflection performance related articles
Faster alternatives to Java's reflection
https://dzone.com/articles/the-performance-cost-of-reflection
FINAL RESULT
I continue to use switch statements in my application to avoid any performance hit.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.