This article is an extract from the book Java EE 8 Cookbook, authored by Elder Moraes.
Well, that's what this recipe is all about.
For both monolith and microservice projects, we will use the same dependency:
<dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency>
First, we need the entities that will represent the data kept by the application.
Here is the User entity:
@Entity
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column
private String name;
@Column
private String email;
public User(){
}
public User(String name, String email) {
this.name = name;
this.email = email;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Here is the UserAddress entity:
@Entity
public class UserAddress implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column
@ManyToOne
private User user;
@Column
private String street;
@Column
private String number;
@Column
private String city;
@Column
private String zip;
public UserAddress(){
}
public UserAddress(User user, String street, String number,
String city, String zip) {
this.user = user;
this.street = street;
this.number = number;
this.city = city;
this.zip = zip;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
}
Now we define one bean to deal with the transaction over each entity.
Here is the UserBean class:
@Stateless
public class UserBean {
@PersistenceContext
private EntityManager em;
public void add(User user) {
em.persist(user);
}
public void remove(User user) {
em.remove(user);
}
public void update(User user) {
em.merge(user);
}
public User findById(Long id) {
return em.find(User.class, id);
}
public List<User> get() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> pet = cq.from(User.class);
cq.select(pet);
TypedQuery<User> q = em.createQuery(cq);
return q.getResultList();
}
}
Here is the UserAddressBean class:
@Stateless
public class UserAddressBean {
@PersistenceContext
private EntityManager em;
public void add(UserAddress address){
em.persist(address);
}
public void remove(UserAddress address){
em.remove(address);
}
public void update(UserAddress address){
em.merge(address);
}
public UserAddress findById(Long id){
return em.find(UserAddress.class, id);
}
public List<UserAddress> get() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<UserAddress> cq = cb.createQuery(UserAddress.class);
Root<UserAddress> pet = cq.from(UserAddress.class);
cq.select(pet);
TypedQuery<UserAddress> q = em.createQuery(cq);
return q.getResultList();
}
}
Finally, we build two services to perform the communication between the client and the beans.
Here is the UserService class:
@Path("userService")
public class UserService {
@EJB
private UserBean userBean;
@GET
@Path("findById/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response findById(@PathParam("id") Long id){
return Response.ok(userBean.findById(id)).build();
}
@GET
@Path("get")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response get(){
return Response.ok(userBean.get()).build();
}
@POST
@Path("add")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response add(User user){
userBean.add(user);
return Response.accepted().build();
}
@DELETE
@Path("remove/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response remove(@PathParam("id") Long id){
userBean.remove(userBean.findById(id));
return Response.accepted().build();
}
}
Here is the UserAddressService class:
@Path("userAddressService")
public class UserAddressService {
@EJB
private UserAddressBean userAddressBean;
@GET
@Path("findById/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response findById(@PathParam("id") Long id){
return Response.ok(userAddressBean.findById(id)).build();
}
@GET
@Path("get")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response get(){
return Response.ok(userAddressBean.get()).build();
}
@POST
@Path("add")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response add(UserAddress address){
userAddressBean.add(address);
return Response.accepted().build();
}
@DELETE
@Path("remove/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response remove(@PathParam("id") Long id){
userAddressBean.remove(userAddressBean.findById(id));
return Response.accepted().build();
}
}
Now let's break it down!
Our monolith deals with User and UserAddress. So we will break it down into three microservices:
A gateway service is an API between the application client and the services. Using it allows you to simplify this communication, also giving you the freedom of doing whatever you like with your services without breaking the API contracts (or at least minimizing it).
The User entity, UserBean, and UserService will remain exactly as they are in the monolith. Only now they will be delivered as a separated unit of deployment.
The UserAddress classes will suffer just a single change from the monolith version, but keep their original APIs (that is great from the point of view of the client).
Here is the UserAddress entity:
@Entity
public class UserAddress implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column
private Long idUser;
@Column
private String street;
@Column
private String number;
@Column
private String city;
@Column
private String zip;
public UserAddress(){
}
public UserAddress(Long user, String street, String number,
String city, String zip) {
this.idUser = user;
this.street = street;
this.number = number;
this.city = city;
this.zip = zip;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getIdUser() {
return idUser;
}
public void setIdUser(Long user) {
this.idUser = user;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
}
Note that User is no longer a property/field in the UserAddress entity, but only a number (idUser). We will get into more details about it in the following section.
First, we create a class that helps us deal with the responses:
public class GatewayResponse {
private String response;
private String from;
public String getResponse() {
return response;
}
public void setResponse(String response) {
this.response = response;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
}
Then, we create our gateway service:
@Consumes(MediaType.APPLICATION_JSON)
@Path("gatewayResource")
@RequestScoped
public class GatewayResource {
private final String hostURI = "http://localhost:8080/";
private Client client;
private WebTarget targetUser;
private WebTarget targetAddress;
@PostConstruct
public void init() {
client = ClientBuilder.newClient();
targetUser = client.target(hostURI +
"ch08-micro_x_mono-micro-user/");
targetAddress = client.target(hostURI +
"ch08-micro_x_mono-micro-address/");
}
@PreDestroy
public void destroy(){
client.close();
}
@GET
@Path("getUsers")
@Produces(MediaType.APPLICATION_JSON)
public Response getUsers() {
WebTarget service =
targetUser.path("webresources/userService/get");
Response response;
try {
response = service.request().get();
} catch (ProcessingException e) {
return Response.status(408).build();
}
GatewayResponse gatewayResponse = new GatewayResponse();
gatewayResponse.setResponse(response.readEntity(String.class));
gatewayResponse.setFrom(targetUser.getUri().toString());
return Response.ok(gatewayResponse).build();
}
@POST
@Path("addAddress")
@Produces(MediaType.APPLICATION_JSON)
public Response addAddress(UserAddress address) {
WebTarget service =
targetAddress.path("webresources/userAddressService/add");
Response response;
try {
response = service.request().post(Entity.json(address));
} catch (ProcessingException e) {
return Response.status(408).build();
}
return Response.fromResponse(response).build();
}
}
As we receive the UserAddress entity in the gateway, we have to have a version of it in the gateway project too. For brevity, we will omit the code, as it is the same as in the UserAddress project.
The monolith application couldn't be simpler: just a project with two services using two beans to manage two entities.
So we split the monolith into three projects (microservices): the user service, the user address service, and the gateway service.
The user service classes remained unchanged after the migration from the monolith version. So there's nothing to comment on.
The UserAddress class had to be changed to become a microservice. The first change was made on the entity.
Here is the monolith version:
@Entity
public class UserAddress implements Serializable {
...
@Column
@ManyToOne
private User user;
...
public UserAddress(User user, String street, String number,
String city, String zip) {
this.user = user;
this.street = street;
this.number = number;
this.city = city;
this.zip = zip;
}
...
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
...
}
Here is the microservice version:
@Entity
public class UserAddress implements Serializable {
...
@Column
private Long idUser;
...
public UserAddress(Long user, String street, String number,
String city, String zip) {
this.idUser = user;
this.street = street;
this.number = number;
this.city = city;
this.zip = zip;
}
public Long getIdUser() {
return idUser;
}
public void setIdUser(Long user) {
this.idUser = user;
}
...
}
Note that in the monolith version, user was an instance of the User entity:
private User user;
In the microservice version, it became a number:
private Long idUser;
This happened for two main reasons:
This change also resulted in a change in the constructor.
Here is the monolith version:
public UserAddress(User user, String street, String number, String city, String zip)
Here is the microservice version:
public UserAddress(Long user, String street, String number, String city, String zip)
This could lead to a change of contract with the client regarding the change of the constructor signature. But thanks to the way it was built, it wasn't necessary.
Here is the monolith version:
public Response add(UserAddress address)
Here is the microservice version:
public Response add(UserAddress address)
Even if the method is changed, it could easily be solved with @Path annotation, or if we really need to change the client, it would be only the method name and not the parameters (which used to be more painful).
Finally, we have the gateway service, which is our implementation of the API gateway design pattern. Basically it is the one single point to access the other services.
The nice thing about it is that your client doesn't need to care about whether the other services changed the URL, the signature, or even whether they are available. The gateway will take care of them.
The bad part is that it is also on a single point of failure. Or, in other words, without the gateway, all services are unreachable. But you can deal with it using a cluster, for example.
So now you've built a microservice in Java EE code, that was once a monolith! If you found this tutorial helpful and would like to learn more, head over to this book Java EE 8 Cookbook, authored by Elder Moraes.
Oracle announces a new pricing structure for Java
Design a RESTful web API with Java [Tutorial]
How to convert Java code into Kotlin