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.
 | |
 |
| jchat | | springchat |
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.