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:
- Websockets [https://www.jcp.org/en/jsr/detail?id=356]
- Sencha Touch (GUI) [http://www.sencha.com/products/touch/]
- Apache TomEE [http://tomee.apache.org/]
- Apache OpenJPA [http://openjpa.apache.org/]
- HSQLDB [http://hsqldb.org/]
- Apache ActiveMQ [http://activemq.apache.org/]
- openjpa-maven-plugin [http://openjpa.apache.org/enhancement-with-maven.html]
- jslint4java-maven-plugin [http://jslint4java.googlecode.com/svn/docs/2.0.0/maven.html]
- maven-checkstyle-plugin [http://maven.apache.org/plugins/maven-checkstyle-plugin/]
- maven-pmd-plugin [http://maven.apache.org/plugins/maven-pmd-plugin/]
- tomee-maven-plugin [http://tomee.apache.org/tomee-maven-plugin.html]
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 |
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
- jchat: https://github.com/tveronezi/jchat [zip distribution]
- springchat: https://github.com/tveronezi/springchat [zip distribution]
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.
No comments:
Post a Comment