Motivation
Creating modern software always requires a number of things to do. One of these things is to persist data and to access persisted data. Within the Java Enterprise ecosystem this basic task evolved up to a point where using objects in Java as representations of data in a database, has become an easy to use commodity. With RESTful web services (added to JEE in version 5) attached to these entities, even network access to persisted data has become very easy.
Yet still a lot of boiler plate code needs to be written and creating JEE application quickly remains a challenge. Therefore approach described in this article shall provide a guide in how to setup an architecture implementing persistence and a RESTful interface layer. To do that properly a (reusable) pattern of inheritance + generic data types is proposed. This (yet unnamed) pattern is applied in persistence, Enterprise Java Beans and in RESTful web service implementation and therefore proved to be quite flexible.
A fully working example application of this pattern can be found on Sourceforge. Besides using described pattern it also contains an Javascript based client application that serves a UI to the Java based back end. This rigorous separation of UI is also intended as it helps to demonstrate architecture approach without mixing with UI issues.
Sourcecode
Lightweight REST JEE Arch PatternPurpose
You have a group of objects which are different in its structure but should implement the same basic behavior.Scenario
You have a lots of data objects in your application for whom the standard CRUD-functionality should be provided.Description
E.g. you are using JPA for data access and you have create several entities. It should be possible to do simple CRUD-operations with all the objects. Using an AbstractDao is a common way to handle the implementation of the basic stuff. An AbstractEntity is created as base class for all entities. In the AbstractDao general CRUD functions are implemented using the AbstractEntity. E.g. http://sidaof.sourceforge.net/sidaof/ is a project, which implements an AbstractDao-class binding the concrete entity to the AbstractDao.The architecture presented here goes further and uses this pattern through all layers of the entire architecture: business services (EJBs), restful webservices, junit-tests. Data objects extends the class "AbstractEntity". Each kind of service (EJB, restful webservice, jUnit-testclass) implements the general functionality around the data objects in an abstract class. For each data object and each kind of service contreted service-implementations are added. To prevent class-casts, Generics ensure the usage of concrete data object in the abstract part of the implementation.
The following class-diagram provides an overview of the architecture. The description afterwards is given a better understanding of the diagramm.
Advantage
You are really fast on creating or extending the backend of small JEE-application. You avoid to write boiler-plate code because you do not have to implement general functionality in each part of the architecture. You just have to implement additional stuff like special queries. For instance you have an News Entity and you want to diplay all news of the day. Because it is implement only once, all methods doing the same have the same names. Class-casts form abstract to concrete classes are prevented by using Generics.Disadvantage
Everywhere abstract classes are used, the risk of deep class hirarchies occures. This can be avoided by using composition or transfer logic in sperate classes.Example Implementation
Data Objects
complete codeAbstract Data Objects
In the example news and categories should be created, deleted and diplayed. The news have to be assigned to a category. At first the base data class "AbstractEntity" is written.package de.starwit.lirejarp.entity;
import java.io.Serializable;
...
@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name="id")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
Concrete Data Objects
The entities "CategryEntity" and "NewsEntity" are created and inherit the base-class "AbstractEntity".package de.starwit.lirejarp.entity;
import java.util.List;
...
@Entity
@Table(name = "CATEGORY")
public class CategoryEntity extends AbstractEntity {
private static final long serialVersionUID = -1458424080034108665L;
@NotNull
@Size(max = 20)
private String name;
private List<NewsEntity> news;
@Column(name="NAME", nullable = false, length=100)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@XmlTransient
@OneToMany(mappedBy="category")
public List<NewsEntity> getNews() {
return news;
}
public void setNews(List<NewsEntity> news) {
this.news = news;
}
}
package de.starwit.lirejarp.entity;
import java.util.Date;
...
@XmlRootElement
@Entity
@Table(name="NEWS")
public class NewsEntity extends AbstractEntity {
private static final long serialVersionUID = -3717230832637971328L;
@NotBlank
@Size(max = 100)
private String title;
@NotNull
private Date publishedAt;
@NotBlank
@Size(max = 100)
private String publishedBy;
@NotBlank
@Size(max = 1000)
private String content;
@NotNull
private CategoryEntity category;
@Column(name="TITLE", nullable = false, length=100)
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Column(name="PUBLISHED_BY", nullable = false, length=100)
public String getPublishedBy() {
return publishedBy;
}
public void setPublishedBy(String publishedBy) {
this.publishedBy = publishedBy;
}
@Column(name="CONTENT", nullable = false, length=1000)
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Column(name = "PUBLISHED_AT", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
public Date getPublishedAt() {
return publishedAt;
}
public void setPublishedAt(Date publishedAt) {
this.publishedAt = publishedAt;
}
@ManyToOne
@JoinColumn(name = "CATEGORY_ID", nullable = false)
public CategoryEntity getCategory() {
return category;
}
public void setCategory(CategoryEntity category) {
this.category = category;
}
}
Data Access Services
complete codeAbstract Data Access Services
All starts again with an AbstractService providing CRUD-functionality for all data access services. To ensure correct transaction handling Enterprise Java Beans are taken. For that reason an interface "AbstractService" and the implementation "AbstractServiceImpl" exist.Abstract Interface
package de.starwit.lirejarp.ejb;
import java.util.List;
...
public interface AbstractService<E extends AbstractEntity> {
E create(E entity);
void delete(Long id) throws EntityNotFoundException;
E update(E entity);
E findById(Long id);
List<E> findAll();
E findByIdWithRelations(Long id, String... attributeName);
EntityManager getEntityManager();
void setEntityManager(EntityManager em);
}
Abtract Implementation
package de.starwit.lirejarp.ejb.impl;
import java.lang.reflect.ParameterizedType;
...
public class AbstractServiceImpl<E extends AbstractEntity> implements AbstractService<E> {
private static Logger LOG = Logger.getLogger(AbstractServiceImpl.class);
@PersistenceContext(unitName = "MeineJpaPU")
private EntityManager em;
private E parentClass;
@SuppressWarnings("unchecked")
public Class<E> getParentClass() {
...
}
public EntityManager getEntityManager() {
return em;
}
public E create(E entity) {
em.persist(entity);
em.flush();
return entity;
}
public void delete(Long id) throws EntityNotFoundException {
E entity = em.find(getParentClass(), id);
if (entity == null) {
throw new EntityNotFoundException(id, getParentClass().getName());
}
em.remove(entity);
}
public E update(E entity) {
em.merge(entity);
em.flush();
return entity;
}
public List<E> findAll() {
...
}
public List<E> findAll(int firstResult, int maxResult) {
...
}
public Long countAll() {
...
}
public E findById(Long id) {
return em.find(getParentClass(), id);
}
public E findByIdWithRelations(Long id, String... relations) {
...
}
public E findByIdOrThrowException(Long id)
throws IllegalIdException, EntityNotFoundException {
...
}
public void setEntityManager(EntityManager em) {
this.em = em;
}
}
Concrete Data Access Services
The concrete service interfaces and implementations are using the the concrete entities in the interface-definition and service-implementation-definition. If you do need additional stuff, you can add some functions. Otherwise, it is enough just to create the classes and interfaces without any method. Only the services for the NewsEntity are presented below.NewsService Interface
package de.starwit.lirejarp.ejb;
import java.io.Serializable;
...
@Local
public interface NewsService extends Serializable, AbstractService<NewsEntity> {
List<NewsEntity> findToday();
List<NewsEntity> findByCategory(Long id);
}
NewsService Implementation
package de.starwit.lirejarp.ejb.impl;
import java.util.Calendar;
...
@Stateless(name="NewsService")
public class NewsServiceImpl extends AbstractServiceImpl<NewsEntity> implements NewsService {
private static final long serialVersionUID = -1034640519269748512L;
@Override
public List<NewsEntity> findToday() {
Calendar calBefore = Calendar.getInstance();
calBefore.setTime(new Date());
calBefore.set(Calendar.HOUR_OF_DAY, 0);
calBefore.set(Calendar.MINUTE, 0);
calBefore.set(Calendar.SECOND, 0);
calBefore.set(Calendar.MILLISECOND, 0);
Calendar calAfter = Calendar.getInstance();
calAfter.setTime(calBefore.getTime());
calAfter.add(Calendar.DATE, 1);
String sql = "select distinct news from NewsEntity news where news.publishedAt > :dateBefore and news.publishedAt < :dateAfter order by news.publishedAt desc";
TypedQuery<NewsEntity> query = getEntityManager().createQuery(
sql, NewsEntity.class);
query.setParameter("dateBefore", calBefore.getTime());
query.setParameter("dateAfter", calAfter.getTime());
return query.getResultList();
}
public List<NewsEntity> findByCategory(Long id) {
String sql = "select news from NewsEntity news where news.category.id = :id";
TypedQuery<NewsEntity> query = getEntityManager().createQuery(
sql, NewsEntity.class);
query.setParameter("id", id);
return query.getResultList();
}
}
Restful Webservices for User Interface
complete codeIn our example, the User Interface is based upon AngularJS and html. Restful webservices are providing the connection between User Interface and the data access layer. The implementation follows the same pattern we used before. With JAXB, some problems occured by using generic types. Hence we switched to Jackson as JSON-Parser. But also Jackson has some limitation handling generic types, additional concrete functions had to be added.
Abstract Restful Webservice
package de.starwit.lirejarp.api.rest;
import java.util.List;
...
public abstract class AbstractRest<E extends AbstractEntity> {
private static final Logger LOG = Logger.getLogger("fileAppender");
/**
* Deserialisation of JSON is not working. Implement abstract method instead:
****************************************************************************
@PUT
@Override
public ResultStateWrapper create(E entity) {
return super.createGeneric(entity);
}
****************************************************************************
* @param entity
* @return
*/
public abstract ResultWrapper<E> create(E entity);
/**
* Deserialisation of JSON is not working. Implement abstract method instead:
****************************************************************************
@POST
@Override
public ResultStateWrapper update(E entity) {
return super.updateGeneric(entity);
}
****************************************************************************
* @param entity
* @return
*/
public abstract ResultStateWrapper update(E entity);
/**
* Typify persistence service
*/
protected abstract AbstractService<E> getService();
public ResultWrapper<E> createGeneric(E entity) {
LOG.debug("************ FrontendService create for " + getService().getClass().getSimpleName());
E interalEntity = getService().create(entity);
ResultWrapper<E> result = new ResultWrapper<E>();
result.setResult(interalEntity);
result.setResultState(ResultValidator.savedResultExists(interalEntity));
return result;
}
//Update
@POST
public ResultStateWrapper updateGeneric(E entity) {
E interalEntity = getService().update(entity);
return ResultValidator.savedResultExists(interalEntity);
}
/**
* returns a flat entity with NO associated entities
*
* @param id
* @return
*/
@Path("/{entityId}")
@GET
public ResultWrapper<E> getById(@PathParam("entityId") Long id) {
E entity = getService().findById(id);
ResultWrapper<E> rw = new ResultWrapper<E>(entity);
return rw;
}
//Delete
@Path("/{entityId}")
@DELETE
public ResultStateWrapper delete(@PathParam("entityId") Long id) {
ResultStateWrapper resultState = new ResultStateWrapper();
try {
getService().delete(id);
resultState.setState(ResultState.OK);
resultState.setMessage("Der Eintrag wurde gelöscht.");
} catch (EntityNotFoundException e) {
resultState.setState(ResultState.NOT_DELETE);
resultState.setMessage("Der Eintrag konnte nicht gelöscht werden. " + e.getMessage());
}
return resultState;
}
protected ListResultWrapper<E> genericGetAll() {
List<E> entities = getService().findAll();
ListResultWrapper<E> resultWrapper = new ListResultWrapper<E>(entities);
ResultStateWrapper resultStateWrapper = ResultValidator.isNotEmpty(resultWrapper.getResult());
resultWrapper.setResultState(resultStateWrapper);
return resultWrapper;
}
}
Concrete Restfuls Webservices
package de.starwit.lirejarp.api.rest;
import java.util.Date;
...
@Path("/news")
@Consumes("application/json")
@Produces("application/json")
public class NewsRest extends AbstractRest<NewsEntity> {
@Inject
protected NewsService service;
@Override
protected NewsService getService() {
return service;
}
//Create
@PUT
@Override
public ResultWrapper<NewsEntity> create(NewsEntity entity) {
System.out.println("Category Id: " + entity.getCategory().getId());
System.out.println("Category name: " + entity.getCategory().getName());
entity.setPublishedAt(new Date());
return super.createGeneric(entity);
}
//Update
@POST
@Override
public ResultStateWrapper update(NewsEntity entity) {
System.out.println("Category Id: " + entity.getCategory().getId());
System.out.println("Category name: " + entity.getCategory().getName());
entity.setPublishedAt(new Date());
return super.updateGeneric(entity);
}
@Path("/all")
@GET
public ListResultWrapper<NewsEntity> getAll() {
return super.genericGetAll();
}
@Path("/ext/today")
@GET
public ListResultWrapper<NewsEntity> getToday() {
List<NewsEntity> entities = service.findToday();
ListResultWrapper<NewsEntity> resultWrapper = new ListResultWrapper<NewsEntity>(entities);
ResultStateWrapper resultStateWrapper = ResultValidator.isNotEmpty(resultWrapper.getResult());
resultWrapper.setResultState(resultStateWrapper);
return resultWrapper;
}
@Path("/ext/category/{catedoryId}")
@GET
public ListResultWrapper<NewsEntity> getByCategory(@PathParam("catedoryId") Long id) {
List<NewsEntity> entities = service.findByCategory(id);
ListResultWrapper<NewsEntity> resultWrapper = new ListResultWrapper<NewsEntity>(entities);
ResultStateWrapper resultStateWrapper = ResultValidator.isNotEmpty(resultWrapper.getResult());
resultWrapper.setResultState(resultStateWrapper);
return resultWrapper;
}
}
Keine Kommentare :
Kommentar veröffentlichen