Spring Data REST

Standard
En esta ocasión les explicaré como levantar una API REST basado en un repositorio JPA con un par de entidades.
En el ejemplo levantaremos una aplicación web usando el Framework de Spring y una DB HSQL en memoria. Como prueba, haremos una serie de requests desde terminal para validar su funcionamiento.
A pesar de la simplicidad del ejemplo, el framework de Spring Data REST es muy potente y soporta una variedad de tecnologías en el backend, Relacionales y NoSQL, además de opciones muy potentes para filtrar, paginar y ordenar, les recomiendo explorar el sitio de Spring Data REST.

Arquitectura del proyecto

Evernote Snapshot 20151127 174247
productosapp
- pom.xml
/src/main/java/com/hunabsys/products/config/
- WebAppInitializer.java
- AppConfig.java
- WebConfig.java
/src/main/java/com/hunabsys/products/controller/
- AppController.java
/src/main/java/com/hunabsys/products/model/
- Category.java
- Product.java
/src/main/java/com/hunabsys/products/repository/
- CategoryRepository.java
- ProductRepository.java

Setup del Proyecto
Nota. Para el ejemplo use Netbeans 8.1 y Tomcat.
Empezamos con un proyecto web con Maven, en el pom.xml usaremos las siguientes dependencias:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-web-api</artifactId>
    <version>7.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.1.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>4.1.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-rest-webmvc</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-commons-core</artifactId>
    <version>1.4.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>1.7.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.3.6.Final</version>
</dependency>
<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.3.3</version>
</dependency>
<dependency>
    <groupId>javax.transaction</groupId>
    <artifactId>jta</artifactId>
    <version>1.1</version>
</dependency>
Te explico para que usaremos esas bibliotecas, las más importantes son:
– spring-webmvc. El Framework Web MVC que se usará para tener el patrón de diseño MVC en nuestro proyecto web.
– spring-data-rest-webmvc. El framework de Spring Data REST que nos permite crear servicios REST con base en nuestros modelos.
– spring-data-jpa. Spring Data JPA es usado para crear repositorios con JPA habilitados para nuestros modelos, nos permite usar el patrón de repositorio con nuestros datos.
– hibernate-entitymanager. JPA es solo una referencia, para usarlo ocupas de hecho una implementación de ella. En este ejemplo usaremos la de Hibernate.
– hsqldb. Para guardar nuestros datos usaremos una DB en memoria como lo es HSQLDB.
– javaee-web-api. Contiene entre otras cosas el estándar de Servlet 3 para configurar nuestra aplicación web sin XML.
Levantar Web Application Context
Para realizar esta operación ocupamos escribir tres clases: WebAppInitializer, AppConfig y WebConfig.
WebAppInitializer
Las aplicaciones Web de Java modernas ya no usan archivos xml para configuración, ahora se usa con JavaConfig.
/**
 * the web app initializer
 * @author <strong>omar.palomino@hunabsys.com</strong>
 */
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
 
    /**
     * customize servlet registration.
     * @param registration  the registration.
     */
    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        registration.setInitParameter("dispatchOptionsRequest", "true");
        registration.setAsyncSupported(true);
    }
 
    /**
     * get the root config classes
     * @return the config classes
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{AppConfig.class};
    }
 
    /**
     * get servlet config classes
     * @return the config classes
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebConfig.class};
    }
 
    /**
     * get servlet mappings
     * @return the mappings
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
 
    /**
     * get servlet filters
     * @return the filters
     */
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding(StandardCharsets.UTF_8.name());
        return new Filter[]{characterEncodingFilter};
    }
 
}
 Aún tenemos servlets, mapeos y filtros. Mapeamos al path raíz y vamos a usar la clase de WebConfig para el web context y el application context con la clase AppConfig.
Application Context
AppConfig
/**
 * the repository objects app config.
 * @author omar.palomino@hunabsys.com
 */
@Configuration
@EnableJpaRepositories(basePackages = {"com.hunabsys.products.repository"})
@ComponentScan(basePackages = {"com.hunabsys.products"}, excludeFilters = {
    @ComponentScan.Filter(value = Controller.class, type = FilterType.ANNOTATION),
    @ComponentScan.Filter(value = Configuration.class, type = FilterType.ANNOTATION)
})
public class AppConfig extends RepositoryRestMvcConfiguration {
 
    /**
     * configure the repository rest configuration.
     * @param config the rest config
     */
    @Override
    protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        super.configureRepositoryRestConfiguration(config);
        try {
            config.setBaseUri(new URI("/api"));
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * data source bean.
     * @return the data source
     */
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()//
                .setType(EmbeddedDatabaseType.HSQL)//
                .build();
    }
 
    /**
     * jpa vendor adapter bean.
     * @return the jpa vendor adapter
     */
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        adapter.setShowSql(true);
        adapter.setGenerateDdl(true);
        adapter.setDatabase(Database.HSQL);
        return adapter;
    }
 
    /**
     * entity manager factory bean
     * @return the entity manager factory
     * @throws ClassNotFoundException
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory()//
            throws ClassNotFoundException {
        LocalContainerEntityManagerFactoryBean factoryBean =//
                new LocalContainerEntityManagerFactoryBean();
        factoryBean.setDataSource(dataSource());
        factoryBean.setPackagesToScan("com.omarps.products.model");
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
        factoryBean.setJpaProperties(jpaProperties());
 
        return factoryBean;
    }
 
    /**
     * transaction manager bean.
     * @return the transaction manager
     * @throws ClassNotFoundException
     */
    @Bean
    public JpaTransactionManager transactionManager()//
            throws ClassNotFoundException {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
 
        return transactionManager;
    }
 
    /**
     * jpa properties bean.
     * @return the jpa properties
     */
    @Bean
    public Properties jpaProperties() {
        Properties properties = new Properties();
        properties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop");
        return properties;
    }
 
}
La clase contiene:
– @EnableJpaRepositories. Indica que usaremos Spring Data REST en el paquete com.hunabsys.products.repository.
– @ComponentScan. Con esta annotation indicamos que haga un scan en el paquete com.hunabsys.products y que omita los estereotipos de Controller y Service.
– RepositoryRestMvcConfiguration. Al extender esta clase permitimos que nuestra configuración web esté lista para crear servicios REST desde nuestros repositorios.
– configureRepositoryRestConfiguration(). En este método le indicamos la ruta para nuestro API Controller. (/api)
– Otros métodos son para levantar nuestros métodos de JPA, Hibernate y el Data Source de HSQL.
Levantar el Web Context
WebConfig
/**
 * the web mvc config.
 * @author omar.palomino@hunabsys.com
 */
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.omarps.products.controller")
public class WebConfig extends WebMvcConfigurerAdapter {
 
    /**
     * the servlet context.
     */
    @Autowired
    ServletContext context;
 
    @Bean
    public InternalResourceViewResolver getInternalResourceViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
 
    /**
     * configure default servlet handling.
     * @param configurer the configurer
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
 
    /**
     * web content interceptor bean.
     * @return the interceptor.
     */
    @Bean
    public WebContentInterceptor webContentInterceptor() {
        WebContentInterceptor interceptor = new WebContentInterceptor();
        interceptor.setCacheSeconds(0);
        interceptor.setUseExpiresHeader(true);
        interceptor.setUseCacheControlHeader(true);
        interceptor.setUseCacheControlNoStore(true);
        return interceptor;
    }
 
    /**
     * add interceptors.
     * @param registry the interceptor registry.
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(webContentInterceptor());
    }
 
}
La clase contiene las configuraciones del contexto web:
– @EnableWebMvc. Indica que vamos a usar Spring Web MVC.
– WebMvcConfigurerAdapter. Al extender esta clase nos permite agregar Interceptors.
– getInternalResourceViewResolver(). Nos permite mapear los JSPs para la presentación de nuestra app.
Agregando el Controller
Para el controller solo vamos a definir la raíz, que nos serviría en caso que quisieramos hacer una Single View App, la mapeamos a index.
AppController
/**
 * the application controller
 * @author omar.palomino@hunabsys.com
 */
@Controller
@RequestMapping("/")
public class AppController {
 
    /**
     * view root application.
     * @return the index view
     */
    @RequestMapping(method = RequestMethod.GET)
    public String viewApplication() {
        return "index";
    }
 
}
Nota. El método viewApplication(), mapea hacia el JSP ubicado en /WEB-INF/views/index.jsp.
Definiendo los Modelos
Vamos a implementar las siguientes Entidades con modelos en JPA.
Evernote Snapshot 20151127 174332
Nota. Asegúrate de posicionarlas en el paquete com.hunabsys.products.model para que las tome el entityManagerFactory.
Category
/**
 * the category entity.
 * @author omar.palomino@hunabsys.com
 */
@Entity
public class Category {
 
    /**
     * the id property.
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
 
    /**
     * the name property.
     */
    @Column(nullable = false, unique = true)
    private String name;
 
    // TODO: OneToMany
 
    /**
     * @return the id
     */
    public int getId() {
        return id;
    }
 
    /**
     * @param id the id to set
     */
    public void setId(int id) {
        this.id = id;
    }
 
    /**
     * @return the name
     */
    public String getName() {
        return name;
    }
 
    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }
 
}
Product
/**
 * the product entity.
 * @author omar.palomino@hunabsys.com
 */
@Entity
public class Product {
 
    /**
     * the id property.
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
 
    /**
     * the name property.
     */
    @Column(nullable = false, unique = true)
    private String name;
 
    /**
     * the description property.
     */
    @Column
    private String description;
 
    @ManyToOne(optional = false)
    private Category category;
 
    /**
     * @return the id
     */
    public int getId() {
        return id;
    }
 
    /**
     * @param id the id to set
     */
    public void setId(int id) {
        this.id = id;
    }
 
    /**
     * @return the name
     */
    public String getName() {
        return name;
    }
 
    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }
 
    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }
 
    /**
     * @param description the description to set
     */
    public void setDescription(String description) {
        this.description = description;
    }
 
    /**
     * @return the category
     */
    public Category getCategory() {
        return category;
    }
 
    /**
     * @param category the category to set
     */
    public void setCategory(Category category) {
        this.category = category;
    }
 
}
 Repository
La parte más importante de este post es crear el repositorio basado en el modelo. Para hacerlo es muy sencillo, vamos a ver las clases para nuestras respectivas entidades.
CategoryRepository
/**
 * the category repository api controller.
 * @author omar.palomino@hunabsys.com
 */
@RepositoryRestResource(collectionResourceRel = "category", path = "category")
public interface CategoryRepository//
        extends PagingAndSortingRepository<Category, Integer> {
 
}
ProductRepository
/**
 * the product repository api controller.
 * @author omar.palomino@hunabsys.com
 */
@RepositoryRestResource(collectionResourceRel = "product", path = "product")
public interface ProductRepository extends PagingAndSortingRepository<Product, Integer> {
 
}
Esto nos generará un par de links para ver el contenido de nuestra API:
http://localhost:8080/productsapp/api/category
http://localhost:8080/productsapp/api/product
Pruebas REST
GET API
3A081BA9-3D1F-44B7-B1C4-ACCFA98668DE
POST Category
F1087067-4789-4402-BEA1-8BEB5DDD787E
GET Category
DEAAFE4C-2207-47B9-A47E-CD100449334C
Siguientes Pasos
Ya con esto listo se me ocurren muchas opciones para completar esta aplicación:
– Seleccionar una tecnología para hacer funcional tu aplicación, en mi caso yo usaría jQuery para hacer las llamadas AJAX, otros preferirían usar algo más de moda como AngularJS.
– Agregar un componente tipo Grid para mostrar el contenido de las entidades aprovechando la paginación, el filtrado y ordenamiento.
– Hacer la funcionalidad de un catálogo con las operaciones CRUD ya ofrecidas por el API Controller.
– Hacer una aplicación móvil? (iOS, Android)
En fin, espero que este post les sea útil, si tienen dudas de como usar las operaciones del API Controller (GET, POST, PUT, DELETE), deja un comentario y te puedo compartir las más comunes, con o sin relaciones.

Leave a Reply

Your email address will not be published. Required fields are marked *