Tuesday, February 4, 2014

Two implementations of the same websocket application powered by TomEE (Java EE and Spring Framework)

Among the different ways to implement JVM based Enterprise Applications at our disposal today, two stand out in the crowd: Java EE and Spring Framework. We will implement the same chat application with both of them. This blog post won’t try point out which one you should use. It will simply show how to implement the same thing in two different ways.

We don't care about the infrastructure either. We will use the default values of what TomEE offers out of the box:
  • Embedded database server
  • Embedded messaging server
  • TransactionManager
  • EntityManagerFactory

These points define the common ground of the implementations:

The Java EE based application (jchat) will use only the standards. The Spring based application (springchat) will use the following modules:
  • org.springframework:spring-jms:4.0.1.RELEASE
  • org.springframework.data:spring-data-jpa:1.4.1.RELEASE
  • org.springframework:spring-webmvc:4.0.1.RELEASE
  • org.springframework:spring-websocket:4.0.1.RELEASE
  • org.springframework.security:spring-security-web:3.2.0.RELEASE

How does it look like?

It's the "Hello World" version of a "Websockets" application. There is a lot more to see under the hood, but the user interface is pretty standard.

jchatspringchat
The applications run in different VMs. ActiveMQ plays the link between them. One of the applications should run as node.

 mvn clean install tomee:run -P node  
 mvn clean install tomee:run  

When you use -P node, TomEE will use an external ActiveMQ broker instead of using its own. "-P node" is not part of the tomee-maven-plugin. It's just another maven profile defined by our applications.

Source code


Resource injection with Java EE

   @Inject  
   private ConnectionsService connections;  

Resource injection with Spring Framework

   @Autowired  
   private ConnectionsService connections;  

If an implementation of JSR-330 is detected, Spring is capable of using @Inject instead of @Autowired. Check [http://docs.spring.io/spring/docs/4.0.1.RELEASE/spring-framework-reference/htmlsingle/#beans-standard-annotations].

JMS MessageListener with Java EE

MessagesService.java
 @MessageDriven(messageListenerInterface = MessageListener.class, activationConfig = {  
     @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Topic"),  
     @ActivationConfigProperty(propertyName = "destination", propertyValue = "topic/Messages"),  
     @ActivationConfigProperty(propertyName = "subscriptionDurability", propertyValue = "Durable"),  
     @ActivationConfigProperty(propertyName = "clientId", propertyValue = "jchat")  
 })  

 @RunAs("chat-system")  
 public class NewTextMessage implements MessageListener {  
   @Inject  
   private ConnectionsService connections;  

   @Inject  
   private MessagesService messagesService;  

   @Override  
   public void onMessage(Message jms) {  
     MessageDto dto = new MessageDto();  
     try {  
       dto.setContent(jms.getStringProperty("message"));  
       dto.setFrom(jms.getStringProperty("user"));  
       dto.setTimestamp(jms.getLongProperty("date"));  
     } catch (JMSException e) {  
       throw new UnmanagedException(e);  
     }  
     MessageEntity bean = messagesService.save(dto.getTimestamp(), dto.getFrom(), dto.getContent());  
     dto.setId(bean.getUid());  
     connections.sendToAll("text", dto);  
   }  
 }  

JMS MessageListener with Spring Framework

MessagesService.java
 public class NewTextMessage implements MessageListener {  
   @Autowired  
   private ConnectionsService connections;  

   @Autowired  
   private MessagesService messagesService;  

   @Override  
   public void onMessage(Message jms) {  
     MessageDto dto = new MessageDto();  
     try {  
       dto.setContent(jms.getStringProperty("message"));  
       dto.setFrom(jms.getStringProperty("user"));  
       dto.setTimestamp(jms.getLongProperty("date"));  
     } catch (JMSException e) {  
       throw new UnmanagedException(e);  
     }  
     MessageEntity bean = messagesService.save(dto.getTimestamp(), dto.getFrom(), dto.getContent());  
     dto.setId(bean.getUid());  
     connections.sendToAll("text", dto);  
   }  
 }  

application-context.xml
  <jee:jndi-lookup id="jmsConnectionFactory" jndi-name="java:comp/env/myJmsConnectionFactory"/>  
  <bean id="jmsDestResolver" class=" org.springframework.jms.support.destination.JndiDestinationResolver"/>  
  <bean id="messageListener" class="springchat.jms.NewTextMessage"/>  
  <bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">  
   <property name="connectionFactory" ref="jmsConnectionFactory"/>  
   <property name="destinationName" value="java:comp/env/NewTextMessageChannel"/>  
   <property name="destinationResolver" ref="jmsDestResolver"/>  
   <property name="messageListener" ref="messageListener"/>  
   <property name="pubSubDomain" value="true"/>  
   <property name="subscriptionDurable" value="true"/>  
   <property name="clientId" value="springchat"/>  
  </bean>  

Please note java:comp/env/myJmsConnectionFactory and java:comp/env/NewTextMessageChannel. These resources are created by the application server.

RESTful WebServices with Java EE

Extend the javax.ws.rs.core.Application class and list the classes with restful endpoints.

ApplicationRestConfig.java
 @ApplicationPath("/rest")  
 public class ApplicationRestConfig extends Application {  
   @Override  
   public Set<Class<?>> getClasses() {  
     final Set<Class<?>> classes = new HashSet<Class<?>>();  
     classes.add(AuthenticationRest.class);  
     classes.add(MessagesRest.class);  
     return classes;  
   }  
 }  

MessagesRest .java
 @Path("/messages")  
 public class MessagesRest {  
   @Inject  
   private MessagesService messages;  

   @POST  
   public Response postMessage(@FormParam("message") String message) {  
     messages.postMessage(message);  
     return Response.ok().build();  
   }  
.
.
.
 }  

RESTful WebServices with Spring Framework

application-context.xml
 <mvc:annotation-driven/>  

MessagesRest .java
 @Controller  
 public class MessagesRest {  
   @Autowired  
   private MessagesService messages;  

   @RequestMapping(value = "/rest/messages", method = RequestMethod.POST)  
   @ResponseStatus(HttpStatus.OK)  
   public void postMessage(@RequestParam("message") String message) {  
     messages.postMessage(message);  
   }  
.
.
.
 }  

Services with Java EE

MessagesService.java
 @Stateless  
 public class MessagesService {  
.
.
.
 }  

Services with Spring Framework

MessagesService.java
 @Service  
 public class MessagesServiceImpl implements MessagesService {  
.
.
.  
 }  

Please note that the Java EE version does not implement an interface. It's not mandatory in both cases, but an interface makes it easier to mock dependencies during unit tests. Our Java EE application is tested with a real server (via Arquillian), so no mocking is required.

Manual user authentication with Java EE

We use the standard JAAS to validate the user name and password. TomEE provides many ways to implement it. We use the ScriptLoginModule, which allows the use of a script in order to validate user credentials. According to the wikipedia, the main goal of JAAS is to separate the concerns of user authentication so that they may be managed independently. Therefore, some server side configuration is required. We hard code this configuration via tomee-maven-plugin. For more information about JAAS, check [http://tomee.apache.org/tomee-jaas.html].

login.config
 ScriptLogin {  
   org.apache.openejb.core.security.jaas.ScriptLoginModule required  
        engineName="js"  
        scriptURI="loginScript.js";  
 };  

loginScript.js
 function localAuthentication() {  
   var result = new java.util.ArrayList();  
   result.add('chat-user');  
   if (!user || user.trim() === '') {  
     throw 'Bad user or password. Test.';  
   }  
   return result;  
 }  
 localAuthentication();  

context.xml
 <Context antiJARLocking="true" path="/jchat">  
  <Realm className="org.apache.catalina.realm.JAASRealm" appName="ScriptLogin"  
      userClassNames="org.apache.openejb.core.security.jaas.UserPrincipal"  
      roleClassNames="org.apache.openejb.core.security.jaas.GroupPrincipal">  
  </Realm>  
 </Context>  

AuthenticationRest.java
 @Path("/auth")  
 public class AuthenticationRest {  
   @POST  
   @Produces("application/json")  
   public AuthenticationResultDto postUser(@FormParam("user") String user, @Context HttpServletRequest request) {  
     AuthenticationResultDto dto = new AuthenticationResultDto();  
     dto.setSessionId(request.getSession().getId());  
     try {  
       request.login(user, "");  
       request.getSession().setAttribute("authenticated", Boolean.TRUE);  
       dto.setSuccess(true);  
     } catch (ServletException e) {  
       dto.setSuccess(false);  
       dto.setInfo("bad.username.or.password");  
       request.getSession().setAttribute("authenticated", Boolean.FALSE);  
     }  
     return dto;  
   }  
.
.
. 
 }  

The request.login(user, "") call triggers our ScriptLoginModule.

Manual user authentication with Spring Framework

We use the Spring Security framework.

web.xml
  <filter>  
   <filter-name>springSecurityFilterChain</filter-name>  
   <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>  
  </filter>  
  <filter-mapping>  
   <filter-name>springSecurityFilterChain</filter-name>  
   <url-pattern>/*</url-pattern>  
  </filter-mapping>  

UserDetailsServiceImpl.java
 import org.springframework.security.core.authority.AuthorityUtils;  
 import org.springframework.security.core.userdetails.User;  
 import org.springframework.security.core.userdetails.UserDetails;  
 import org.springframework.security.core.userdetails.UserDetailsService;  
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
  
 public class UserDetailsServiceImpl implements UserDetailsService {  
   @Override  
   public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {  
     return new User(name, "", AuthorityUtils.createAuthorityList("ROLE_CHATUSER"));  
   }  
 }  

application-context.xml
  <security:http auto-config="true">  
   <security:intercept-url pattern="/fakepath/*" access="ROLE_USER"/>  
  </security:http>  
  <security:authentication-manager alias="authenticationManager">  
   <security:authentication-provider user-service-ref="userDetailsService"/>  
  </security:authentication-manager>  
  <security:global-method-security secured-annotations="enabled"/>  

Note that we need to declare at least one security:intercept-url, otherwise the system fails to load the controllers. Also note the ROLE_ prefix. It is required by the RoleVoter class.

AuthenticationRest.java
 import org.springframework.security.authentication.AbstractAuthenticationToken;  
 import org.springframework.security.authentication.AuthenticationManager;  
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;  
 import org.springframework.security.core.Authentication;  
 import org.springframework.security.core.context.SecurityContextHolder;  
 import org.springframework.security.web.authentication.WebAuthenticationDetails;  
 
 @Controller  
 public class AuthenticationRest {  
   @Autowired  
   @Qualifier("authenticationManager")  
   protected AuthenticationManager authenticationManager;  

   @RequestMapping(value = "/rest/auth", method = RequestMethod.POST, produces = {"application/json"})  
   @ResponseBody  
   public AuthenticationResultDto postUser(@RequestParam("user") String user, HttpServletRequest request) {  
     AuthenticationResultDto dto = new AuthenticationResultDto();  
     dto.setSessionId(request.getSession().getId());  
     try {  
       // Must be called from request filtered by Spring Security, otherwise SecurityContextHolder is not updated  
       AbstractAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, "");  
       token.setDetails(new WebAuthenticationDetails(request));  
       Authentication authentication = authenticationManager.authenticate(token);  
       SecurityContextHolder.getContext().setAuthentication(authentication);  
       dto.setSuccess(Boolean.TRUE);  
       request.getSession().setAttribute("authenticated", Boolean.TRUE);  
     } catch (Exception e) {  
       SecurityContextHolder.getContext().setAuthentication(null);  
       dto.setSuccess(Boolean.FALSE);  
       request.getSession().setAttribute("authenticated", Boolean.FALSE);  
     }  
     return dto;  
   }  
.
.
.
 }  

Websockets with Java EE

ChatSocketConnection.java
 import javax.websocket.*;  
 import javax.websocket.server.ServerEndpoint;  
   
 @ServerEndpoint(value = "/ws/connection")  
 public class ChatSocketConnection {  
   
   @Inject  
   private ConnectionsService service;  
   
   @OnOpen  
   public void open(Session session, EndpointConfig conf) {  
     service.addSession(session);  
   }  
   
   @OnClose  
   public void close(Session session) {  
     service.removeSession(session);  
   }  
   
   @OnError  
   public void error(Session session, Throwable cause) {  
     service.removeSession(session);  
   }  
   
 }  

Websockets with Spring Framework

ConnectionHandler.java
 import org.springframework.beans.factory.annotation.Autowired;  
 import org.springframework.web.socket.CloseStatus;  
 import org.springframework.web.socket.WebSocketSession;  
 import org.springframework.web.socket.handler.TextWebSocketHandler;  
 import springchat.service.ConnectionsService;  
   
 public class ConnectionHandler extends TextWebSocketHandler {  
   
   @Autowired  
   private ConnectionsService service;  
   
   @Override  
   public void afterConnectionEstablished(WebSocketSession session) throws Exception {  
     service.addSession(session);  
   }  
   
   @Override  
   public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {  
     service.removeSession(session);  
   }  
   
   @Override  
   public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {  
     service.removeSession(session);  
   }  
 }  

application-context.xml
  <bean id="connectionHandler" class="springchat.sockets.ConnectionHandler"/>  
  <websocket:handlers>  
   <websocket:mapping path="/ws/connection" handler="connectionHandler"/>  
  </websocket:handlers>  

Testing (Arquillian and EasyMock)

The jchat project uses the Arquillian mantra: "No more mocks. No more container lifecycle and deployment hassles. Just real tests!" [http://arquillian.org/] You may call it integration tests, but the modern Application Servers (Hint hint... TomEE) make it possible to run such tests almost as fast as the old mock objects. You don't need to create glue code to mock your beans, simply start your server and test the real thing.

The springchat project uses EasyMock [http://easymock.org/], following what's proposed by the reference documentation. You can also use Arquillian with your Spring code. Check [http://arquillian.org/blog/tags/spring/] for more information.

Conclusion

At the end of the day, we just want to code and have fun with it. Whether you choose Java EE, Spring or a combination of them is entirely up to you - in a perfect world. ;) The last thing we need in our jobs is to worry about how to setup our external dependencies and servers. Just let TomEE do it for you.

No comments:

Post a Comment