Build A Restful App With Spring MVC and Angularjs
Build A Restful App With Spring MVC and Angularjs
of Contents
Introduction 1.1
Overview 1.2
An introduction to REST 1.3
Prerequisites 1.4
Getting Started 1.5
Project skeleton 1.5.1
Configure Spring WebMVC 1.5.2
Configure Datasource 1.5.3
Configure JPA 1.5.4
Configure Spring Security 1.5.5
Configure Swagger 1.5.6
Maven profiles and Spring profiles 1.5.7
Gettting started with Spring Boot 1.6
Project skeleton 1.6.1
Configure Datasource 1.6.2
Configure JPA 1.6.3
Configure Spring Security 1.6.4
Configure Swagger 1.6.5
Maven profiles and Spring profiles 1.6.6
Build REST API 1.7
Handle Exceptions 1.8
Test APIs 1.9
Visualize and document REST APIs 1.10
Secure APIs 1.11
Upgrade to Spring Boot 1.4 1.12
1
Introduction
2
Overview
Overview
In this minibook, I will demonstrate how to implement a RESTful web application
with Spring MVC and AngularJS.
It will be consist of a series of small posts as time goes by. Every post is a
standalone chapter focused on a topic.
Assumption
I assume you are a Java developer and have some experience of Spring
framework.
Else you should learn the basic Java and Java EE knowledge, and master basic
usage of Spring framework.
The official Oracle Java tutorial and Java EE tutorial are ready for Java
newbies.
Read the Spring official guides to getting started with Spring framework.
In these posts, it will not cover all Spring and Java EE features, but the following
technologies will be used.
Spring framework
Spring MVC
3
Overview
Spring Security
Spring Security fills this field, which makes the security control become easy,
and provides a simple programming model. Spring Security is also
compatible with JAAS specification, and provides JAAS integration at
runtime.
JPA
Based on JDBC specification, JPA provides a high level ORM abstraction and
brings OOP philosophy to interact with traditional RDBMS. Hibernate and
EclipseLink also support NoSQL.
Hibernate
4
Overview
We also used some third party utilities, such as Lombok project to remove the
tedious getters and setters of POJOs.
For testing purpose, Spring test/JUnit, Mockito, Rest Assured will be used.
Smaple application
In order to demonstrate how to build RESTful APIs, I will implement a simple Blog
system to explain it in details.
Imagine there are two roles will use this blog system.
A administrater should have more advanced permissions, eg. he can manage the
system users.
5
Overview
Sample codes
The complete sample codes are hosted on my Github.com account.
https://github.com/hantsy/angularjs-springmvc-sample
A Spring Boot based envolved version provides more featuers to demonstrate the
cutting-edge technologies.
https://github.com/hantsy/angularjs-springmvc-sample-boot
Please read the README.md file in these respositories and run them under your
local system.
Feedback
The source of this book are hosted on my github.com account.
https://github.com/hantsy/angularjs-springmvc-sample-gitbook
6
An introduction to REST
An Introduction to REST
REST is the abbreviation of Representational State Transter. The term REST was
introduced and defined in 2000 by Roy Fielding in his doctoral dissertation,
Architectural Styles and the Design of Network-based Software Architectures. For
Chinese users, you can find a Chinese translation copy from InfoQ.com.
7
Prerequisites
Prerequisites
Before writing any codes, please install the latest JDK 8, Apache Maven, and your
favorate IDE.
Java 8
Oracle Java 8 is recommended. For Windows user, just go to Oracle Java website
to download it and install into your system. Redhat has just released a OpenJDK 8
for Windows user at DevNation 2016, if you are stick on the OpenJDK, go to
Redhat Developers website and get it.
Most of the Linux distributions includes the OpenJDK, install it via the Linux
package manager.
Optionally, you can set JAVA_HOME environment variable and add <JDK
installation dir>/bin in your PATH environment variable.
Type this command in system terminal to verify your Java environment installed
correctly.
#java -version
java version "1.8.0_102"
Java(TM) SE Runtime Environment (build 1.8.0_102-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode)
Apache Maven
Download the latest Apache Maven from http://maven.apache.org, and
uncompress it into your local system.
Optionally, you can set M2_HOME environment varible, and also do not forget to
append <Maven Installation dir>/bin your PATH environment variable.
8
Prerequisites
#mvn -v
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 20
15-11-11T00:41:47+08:00)
Maven home: D:\build\maven
Java version: 1.8.0_102, vendor: Oracle Corporation
Java home: D:\jdk8\jre
Default locale: en_US, platform encoding: Cp1252
OS name: "windows 10", version: "10.0", arch: "amd64", family: "
dos"
If you are a Gradle fan, you can use Gradle as build tool. Gradle could be an
alternative of Apache Maven.
Lombok
I would like use Lombok to simply codes and make the codes clean. Go to
Lombok project to get know with Lombok.
IDE
The source codes are Maven based, it is IDE independent, so you can choose
your favorate IDE. Nowdays the popular IDEs includes Eclipse, IDEA, NetBeans.
We will use JPA critera metadata to provide type safe query, and use Lombok to
simplfy the codes, you have to enable Annotation Processing feature in your
IDEs.
Spring ToolSuite
Spring Tool Suite is an Eclipse based IDE, and provides a lot of built-in Spring
supports, it is highly recommended for new Spring users.
Go to Spring official site, download a copy of Spring Tool Suite. At the moment,
the latest version is 3.8.
9
Prerequisites
Alternatively, you can download a copy of Eclipse Java EE bundle from Eclise
official website, and install the STS plugin from Eclipse Marketplace.
Extract the files into your local disk. Go to root folder, there is STS.exe file, double
click it and starts up Spring Tool Suite.
Go to Lombok project website, and follow the official the installation guideline) to
install Lombok plugin into your Eclipse IDE.
Intellij IDEA
No doubt, Intellij IDEA is the most productive Java IDE. It includes free and open
source community version and enterprise version.
1. Go to File / Settings
2. Search annotation processor
3. Enable Annotation processing
You can install Lombok plugin from IDEA plugin manager to get Lombok support
in your IDEA.
NetBeans
NetBeans is the simplest IDE for Java development, which was originally brought
by Sun microsystem(and later maintained by Oracle), it is free and open source.
10
Prerequisites
In the next posts, let's try to create a project skeleton for our blog sample
application.
11
Getting Started
Getting started
Before writing codes of REST APIs for the blog sample application, we have to
prepare the development environment, and create a project skeleton, and
understand the basic concept and essential configurations of Spring. Then we will
enrich it according to the requirements, and make it more like a real world
application.
12
Project skeleton
For those new to Spring, the regular approache(none Spring Boot) is more easy to
understand Spring essential configurations.
mvn -DarchetypeGroupId=org.codehaus.mojo.archetypes
-DarchetypeArtifactId=webapp-javaee7
-DarchetypeVersion=1.1
-DgroupId=your_group_id
-DartifactId=angularjs-springmvc-sample
-Dversion=1.0.0-SNAPSHOT
-Dpackage=com.hantsylabs.restsampels
-Darchetype.interactive=false
--batch-mode
archetype:generate
13
Project skeleton
<dependencyManagement>
<dependencies>
<!-- Spring BOM -->
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>2.0.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
platform-bom manages all dependencies of Spring projects, and you can add
dependency declaration directly without specifying a version. platform-bom
manages the versions, and resolved potential conflicts for you.
In order to get IOC container support, you have to add the several core
dependencies into pom.xml.
14
Project skeleton
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
If you would like use @Inject instead of @Autowire in codes, add inject
dependency.
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
Get the codes from my github account to explore all configuration classes.
15
Project skeleton
16
Configure Spring WebMVC
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
@Order(0)
public class AppInitializer extends AbstractAnnotationConfigDisp
atcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] {
AppConfig.class, //
DataSourceConfig.class, //
17
Configure Spring WebMVC
JpaConfig.class, //
DataJpaConfig.class,//
SecurityConfig.class,//
Jackson2ObjectMapperConfig.class,//
MessageSourceConfig.class
};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] {
WebConfig.class, //
SwaggerConfig.class //
};
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter encodingFilter = new CharacterEnc
odingFilter();
encodingFilter.setEncoding("UTF-8");
encodingFilter.setForceEncoding(true);
18
Configure Spring WebMVC
@Configuration
@EnableWebMvc
@ComponentScan(
basePackageClasses = {Constants.class},
useDefaultFilters = false,
includeFilters = {
@Filter(
type = FilterType.ANNOTATION,
value = {
Controller.class,
RestController.class,
ControllerAdvice.class
}
)
19
Configure Spring WebMVC
}
)
public class WebConfig extends SpringDataWebConfiguration {
@Inject
private ObjectMapper objectMapper;
@Override
public void addResourceHandlers(ResourceHandlerRegistry regi
stry) {
registry.addResourceHandler("/swagger-ui.html")
.addResourceLocations("classpath:META-INF/resour
ces/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:META-INF/resour
ces/webjars/");
}
@Override
public void addViewControllers(ViewControllerRegistry regist
ry) {
@Override
public void configureHandlerExceptionResolvers(List<HandlerE
xceptionResolver> exceptionResolvers) {
exceptionResolvers.add(exceptionHandlerExceptionResolver
());
}
@Override
public void configureDefaultServletHandling(DefaultServletHa
ndlerConfigurer configurer) {
configurer.enable();
20
Configure Spring WebMVC
@Override
public void configureContentNegotiation(ContentNegotiationCo
nfigurer configurer) {
configurer.favorParameter(false);
configurer.favorPathExtension(false);
}
@Override
public void configureMessageConverters(List<HttpMessageConve
rter<?>> converters) {
List<HttpMessageConverter<?>> messageConverters = messag
eConverters();
converters.addAll(messageConverters);
}
@Bean
public ExceptionHandlerExceptionResolver exceptionHandlerExc
eptionResolver() {
ExceptionHandlerExceptionResolver exceptionHandlerExcept
ionResolver = new ExceptionHandlerExceptionResolver();
exceptionHandlerExceptionResolver.setMessageConverters(m
essageConverters());
return exceptionHandlerExceptionResolver;
}
MappingJackson2HttpMessageConverter jackson2Converter =
new MappingJackson2HttpMessageConverter();
jackson2Converter.setSupportedMediaTypes(Arrays.asList(M
ediaType.APPLICATION_JSON));
jackson2Converter.setObjectMapper(objectMapper);
messageConverters.add(jackson2Converter);
return messageConverters;
21
Configure Spring WebMVC
I would like use application/json as default content type, and uses Jackson
to serialize and deserialize messages.
22
Configure Spring WebMVC
@Configuration
public class Jackson2ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
DeserializationFeature.FAIL_ON_IGNORED_PROPERTIE
S,
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIE
S);
builder.featuresToEnable(DeserializationFeature.ACCEPT_S
INGLE_VALUE_AS_ARRAY);
return builder.build();
}
}
23
Configure Datasource
Configure DataSource
In order to use Hibernate, Jdbc, or JPA similar persistence framework or tools, you
have to configure a java.sql.DataSource for it.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
@Configuration
public class DataSourceConfig {
@Bean
public DataSource testDataSource() {
BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUrl("jdbc:mysql://localhost:3306");
bds.setUsername("jdbc.username");
bds.setPassword("jdbc.password");
return bds;
}
24
Configure Datasource
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
In above codes, we set username, password etc in hard codes, but in a real
application, it is better to externalize these configurations into a property file.
@Configuration
@ComponentScan(
basePackageClasses = {Constants.class},
excludeFilters = {
@Filter(
type = FilterType.ANNOTATION,
value = {
RestController.class,
ControllerAdvice.class,
Configuration.class
}
)
}
)
@PropertySource("classpath:/app.properties")
@PropertySource(value = "classpath:/database.properties", ignore
ResourceNotFound = true)
public class AppConfig {
25
Configure Datasource
jdbc.url=@jdbc.url@
jdbc.username=@jdbc.username@
jdbc.password=@jdbc.password@
hibernate.dialect=@hibernate.dialect@
@Inject
private Environment env;
@Bean
public DataSource testDataSource() {
BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUrl(env.getProperty(ENV_JDBC_URL));
bds.setUsername(env.getProperty(ENV_JDBC_USERNAME));
bds.setPassword(env.getProperty(ENV_JDBC_PASSWORD));
return bds;
}
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
26
Configure Datasource
We have discussed the usages of Apache Commons Dbcp earlier, you can add
extra pool configuration for this datasource.
For application server built-in DataSource, Spring can access it via a Jndi proxy.
Firstly configure a Jndi DataSource in appliation server GUI, then defines
JndiObjectFactoryBean to access it via Jndi name.
@Bean
public DataSource prodDataSource() {
JndiObjectFactoryBean ds = new JndiObjectFactoryBean();
ds.setLookupOnStartup(true);
ds.setJndiName("jdbc/postDS");
ds.setCache(true);
@Configuration
public class DataSourceConfig {
27
Configure Datasource
me";
private static final String ENV_JDBC_URL = "jdbc.url";
@Inject
private Environment env;
@Bean
@Profile("dev")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
@Bean
@Profile("staging")
public DataSource testDataSource() {
BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUrl(env.getProperty(ENV_JDBC_URL));
bds.setUsername(env.getProperty(ENV_JDBC_USERNAME));
bds.setPassword(env.getProperty(ENV_JDBC_PASSWORD));
return bds;
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
JndiObjectFactoryBean ds = new JndiObjectFactoryBean();
ds.setLookupOnStartup(true);
ds.setJndiName("jdbc/postDS");
ds.setCache(true);
28
Configure Datasource
Three DataSouce beans are configured. Do not worry about the @Profile
annotation, I will explain it in a Spring Profile related section for it.
29
Configure JPA
Configure JPA
JPA was proved a greate success in Java community, and it is wildly used in Java
applications, including some desktop applications.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
</dependency>
30
Configure JPA
Cooperate with Hibernate Core, we also use Bean Validation and hibernate-
validator to generate database schema constraints.
31
Configure JPA
@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class JpaConfig {
@Inject
private Environment env;
@Inject
private DataSource dataSource;
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerF
actory() {
LocalContainerEntityManagerFactoryBean emf = new LocalCo
ntainerEntityManagerFactoryBean();
emf.setDataSource(dataSource);
emf.setPackagesToScan("com.hantsylabs.restexample.spring
mvc");
emf.setPersistenceProvider(new HibernatePersistenceProvi
der());
emf.setJpaProperties(jpaProperties());
return emf;
}
32
Configure JPA
extraProperties.put(ENV_HIBERNATE_FORMAT_SQL, env.getPro
perty(ENV_HIBERNATE_FORMAT_SQL));
extraProperties.put(ENV_HIBERNATE_SHOW_SQL, env.getPrope
rty(ENV_HIBERNATE_SHOW_SQL));
extraProperties.put(ENV_HIBERNATE_HBM2DDL_AUTO, env.getP
roperty(ENV_HIBERNATE_HBM2DDL_AUTO));
if (log.isDebugEnabled()) {
log.debug(" hibernate.dialect @" + env.getProperty(E
NV_HIBERNATE_DIALECT));
}
if (env.getProperty(ENV_HIBERNATE_DIALECT) != null) {
extraProperties.put(ENV_HIBERNATE_DIALECT, env.getPr
operty(ENV_HIBERNATE_DIALECT));
}
return extraProperties;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory().
getObject());
}
}
33
Configure JPA
<persistence version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.
xsd">
<persistence-unit name="primary" transaction-type="RESOUECE_L
OCAL">
<class>...Post</class>
<none-jta-data-source/>
<properties>
<!-- Properties for Hibernate -->
<property name="hibernate.hbm2ddl.auto" value="create-d
rop" />
<property name="hibernate.show_sql" value="false" />
</properties>
</persistence-unit>
</persistence>
You have to specify entity classes will be loaded and datasource here. Spring
provides an alternative, LocalEntityManagerFactoryBean to simplify the
configuration, there is a setPackagesToScan method provided to specify which
packages will be scanned, and another setDataSource method to setup Spring
DataSource configuration and no need to use database connection defined in the
persistence.xml file at all.
34
Configure JPA
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
@Configuration
@EnableJpaRepositories(basePackages = {"com.hantsylabs.restexamp
le.springmvc"})
public class DataJpaConfig {
35
Configure Spring Security
@Order(1)
public class SecurityInitializer extends AbstractSecurityWebAppl
icationInitializer {
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/**/*.html", //
"/css/**", //
"/js/**", //
"/i18n/**",//
"/libs/**",//
"/img/**", //
"/webjars/**",//
"/ico/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
36
Configure Spring Security
.authorizeRequests()
.antMatchers("/api/**")
.authenticated()
.and()
.authorizeRequests()
.anyRequest()
.permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy
.STATELESS)
.and()
.httpBasic()
.and()
.csrf()
.disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder())
.withUser("admin").password("test123").autho
rities("ROLE_ADMIN")
.and()
.withUser("test").password("test123").au
thorities("ROLE_USER");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() thr
ows Exception {
return super.authenticationManagerBean();
}
@Bean
@Override
37
Configure Spring Security
38
Maven profiles and Spring profiles
In this sample application, I would like use Maven profile to specify the
spring.profiles.active for different development stage.
For staging stage, I would like use similar environment with produciton to run
the application with a CI server, such as Jenkins, Travis CI, Circle CI etc.
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<log4j.level>DEBUG</log4j.level>
<spring.profiles.active>dev</spring.profiles.active>
<!-- hibernate -->
<hibernate.hbm2ddl.auto>create</hibernate.hbm2ddl.au
39
Maven profiles and Spring profiles
to>
<hibernate.show_sql>true</hibernate.show_sql>
<hibernate.format_sql>true</hibernate.format_sql>
<log4j.level>INFO</log4j.level>
<spring.profiles.active>staging</spring.profiles.act
ive>
<hibernate.hbm2ddl.auto>update</hibernate.hbm2ddl.au
to>
<hibernate.show_sql>false</hibernate.show_sql>
<hibernate.format_sql>false</hibernate.format_sql>
<hibernate.dialect>org.hibernate.dialect.MySQL5Diale
ct</hibernate.dialect>
</properties>
<dependencies>
<dependency>
40
Maven profiles and Spring profiles
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources-staging</direc
tory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</profile>
<profile>
<id>prod</id>
<properties>
<log4j.level>INFO</log4j.level>
<spring.profiles.active>prod</spring.profiles.active
>
<hibernate.hbm2ddl.auto>none</hibernate.hbm2ddl.auto
>
<hibernate.show_sql>false</hibernate.show_sql>
<hibernate.format_sql>false</hibernate.format_sql>
<hibernate.dialect>org.hibernate.dialect.MySQL5Diale
ct</hibernate.dialect>
</properties>
<build>
<resources>
<resource>
<directory>src/main/resources-prod</director
y>
<filtering>true</filtering>
</resource>
</resources>
</build>
</profile>
</profiles>
41
Maven profiles and Spring profiles
The above command will package the applicatin for prod profile, it also apply
spring.profiles.active value( prod ) for this application when it is running.
@Bean
@Profile("prod")
public DataSource prodDataSource() {
JndiObjectFactoryBean ds = new JndiObjectFactoryBean();
ds.setLookupOnStartup(true);
ds.setJndiName("jdbc/postDS");
ds.setCache(true);
42
Gettting started with Spring Boot
43
Project skeleton
44
Project skeleton
The Maven project configuration pom.xml, and several maven wrapper files
which is like Gradle wrapper and use to download a specific maven for this
project.
A Spring Boot specific Application class as the application entry.
A dummy test for the Application class.
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourc
eEncoding>
<java.version>1.8</java.version>
</properties>
45
Project skeleton
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId
>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId
>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifact
Id>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId
>
</plugin>
</plugins>
</build>
</project>
The package type is jar, which means it will include an embedded tomcat at
46
Project skeleton
build time. You can start the application via command line java -jar
app.jar .
The parent module is spring-boot-starter-parent which is a BOM(Bill
of material) and includes the declaration of Spring Boot dependencies. Just
add the dependencies you want to use under the dependencies node.
Every starter will handle transitive dependencies. Besides those starters we
selected, it also includes a starter for test purpose which will add the popular
test dependencies transitively, such as hamcrest, assertj, mockito etc.
spring-boot-maven-plugin allow you run the project in the embedded
Tomcat.
@SpringBootApplication
public class DemoApplication {
47
Project skeleton
Till now, if you added some dependencies into pom.xml , you can start to code
now. It is the quick way to prototype your application.
Although Spring Boot provides auto-configuration feature, but it does not prevent
you to customize your configuration.
48
Configure Datasource
Configure DataSource
Instead of configuring DataSource in Java code.
server:
port: 9000
contextPath:
spring:
profiles:
active: dev
devtools.restart.exclude: static/**,public/**
datasource:
dataSourceClassName: org.h2.jdbcx.JdbcDataSource
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
databaseName:
serverName:
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
database: H2
openInView: false
show_sql: true
generate-ddl: true
hibernate:
ddl-auto:
naming-strategy: org.hibernate.cfg.EJB3NamingStrateg
y
properties:
hibernate.cache.use_second_level_cache: true
49
Configure Datasource
hibernate.cache.use_query_cache: false
hibernate.generate_statistics: true
hibernate.cache.region.factory_class: org.hibernate.
cache.internal.NoCachingRegionFactory
data:
jpa.repositories.enabled: true
freemarker:
check-template-location: false
messages:
basename: messages
logging:
file: app.log
level:
root: INFO
org.springframework.web: INFO
com.hantsylabs.restexample.springmvc: DEBUG
It is easy to understand.
server.port specifies the port number this application will serve at start up.
NOTE: Spring Boot also supports properties, groovy DSL format for application
configuration.
50
Configure JPA
Configure JPA
Make sure the following dependencies are added in your dependency section of
the project pom.xml file.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
It will add managed JPA, Spring Data JPA and Hibernate into your project.
We have already configured DataSource , and JPA and Spring Data JPA via
application.yml .
You can also use Java code configuration to add some extra features.
@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
@EntityScan(basePackageClasses = {User.class, Jsr310JpaConverter
s.class})
@EnableJpaAuditing(auditorAwareRef = "auditor")
public class JpaConfig {
@Bean
public AuditorAware<User> auditor() {
return () -> SecurityUtil.currentUser();
}
51
Configure JPA
@EntityScan(basePackageClasses = {User.class,
Jsr310JpaConverters.class}) add the JPA entity scan scope, Java 8
DateTime support is added in Spring Data JPA via JPA 2.1
AttributeConvertor feature.
52
Configure Spring Security
/**
*
* @author hantsy
*/
@Configuration
public class SecurityConfig {
@Bean
public WebSecurityConfigurerAdapter webSecurityConfigure(){
return new WebSecurityConfigurerAdapter() {
@Override
protected void configure(HttpSecurity http) throws E
xception {
// @formatter:off
http
.authorizeRequests()
.antMatchers("/api/signup", "/api/users/user
name-check")
.permitAll()
.and()
.authorizeRequests()
.regexMatchers(HttpMethod.GET, "^/api/us
ers/[\\d]*(\\/)?$").authenticated()
.regexMatchers(HttpMethod.GET, "^/api/us
ers(\\/)?(\\?.+)?$").hasRole("ADMIN")
.regexMatchers(HttpMethod.DELETE, "^/api
53
Configure Spring Security
/users/[\\d]*(\\/)?$").hasRole("ADMIN")
.regexMatchers(HttpMethod.POST, "^/api/u
sers(\\/)?$").hasRole("ADMIN")
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPo
licy.STATELESS)
.and()
.httpBasic()
.and()
.csrf()
.disable();
// @formatter:on
}
};
}
}
To customize security, you could have to define your own UserDetails and
UserDetailsService .
@Entity
@Table(name = "users")
public class User implements UserDetails, Serializable {
54
Configure Spring Security
@Component
public class SimpleUserDetailsServiceImpl implements UserDetails
Service {
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("username not fo
und:" + username);
}
return user;
55
Configure Spring Security
56
Configure Swagger
Configure Swagger
SwaggerConfig is no difference with before version.
57
Maven profiles and Spring profiles
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
<log4j.level>DEBUG</log4j.level>
</properties>
<build>
<resources>
<resource>
<directory>src/main/resources-dev</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</profile>
The above Maven profile(dev) will be activated by default, and it will add
/src/main/reources-dev as resource folder.
We can define some other profile based application.yml for different stages. Every
application.yml will add different configuration, such as in development stage, use
a H2 database for easy testing, and in produciton profile, the application.yml will
use a pool datasource for better performance at runtime.
58
Maven profiles and Spring profiles
The above approach combine Maven profiles and Spring profile to get clean and
simple configuration for applications.
Alternatively, Spring Boot provides more powerful capability to switch profile via
environment variables. You can package all profile based configuration in the
same application package, and add a parameter to select a profile when the
application is bootstraping.
59
Build REST API
To demonstrate REST API, we use a simple Post entity to persist blog entries,
and expose the CRUD operations via REST APIs to client applications. As a
REST API consumer, the client applications could be a website, a desktop
application, or a mobile application.
Following the REST API convention and HTTP protocol specification, the post
APIs can be designed as the following table.
Http
Uri Request Response Description
Method
200, [{'id':1, Get all
/posts GET
'title'},{}] posts
{'title':'test
Create a
/posts POST title','content':'test 201
new post
content'}
200, {'id':1, Get a post
/posts/{id} GET
'title'} by id
{'title':'test
Update a
/posts/{id} PUT title','content':'test 204
post
content'}
Delete a
/posts/{id} DELETE 204
post
A Post model to store the blog entries posted by users. A Comment model to
store the comments on a certain post. A User model to store users will user this
blog application.
60
Build REST API
Every domain object should be identified. JPA entities satisfy this requirement.
Every JPA entity has an @Id field as identifier.
A simple Post entity can be designated as the following. Besides id, it includes
a title field , a content field, and createdDate timestamp, etc.
@Entity
@Table(name = "posts")
public class Post implements Serializable {
@Id()
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "title")
private String title;
@Column(name = "content")
@Size(max = 2000)
private String content;
@Column(name = "created_date")
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
61
Build REST API
For example.
For example, there are a post existed in a collection, adding another Post into
the same collection should check the post existance firstly.
The title field can be used to identify two posts in a collection, because they are
not presisted in a persistent storage at the moment, id value are same--null.
62
Build REST API
When an entity instance is being persisted into a database table, the id will be
filled.
WARNING: Every databases has its specific generation strategy, if you are
building an application which will run across databases. AUTO is recommended.
Other id generation strategies include TABLE, IDENTITY. And JPA providers have
their extensions, such as with Hibernate, you can use uuid2 for PostgresSQL.
Lombok
Lombok is a greate helper every Java developer should use in projects. Utilize
Java annotation processor, it can generate getters, setters, equals, hashCode,
toString and class constructor at compile runtime with some Lombok annotations.
Add @Data to Post class, you can remove all getters and setters, and
equals , hashcode , toString methods. The code now looks more clean.
63
Build REST API
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "posts")
public class Post implements Serializable {
@Id()
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "title")
private String title;
@Column(name = "content")
@Size(max = 2000)
private String content;
@Column(name = "status")
@Enumerated(value = EnumType.STRING)
private Status status = Status.DRAFT;
@Column(name = "created_date")
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
}
64
Build REST API
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
If there are several JAP(Java annotaition processor) exist in the project, such as
JPA metadata generator, it is better to add Lombok processor to maven compiler
plugin.
For example.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<compilerArgument>-Xlint</compilerArgument>
<annotationProcessors>
<annotationProcessor>lombok.launch.AnnotationProcess
orHider$AnnotationProcessor</annotationProcessor>
<annotationProcessor>org.hibernate.jpamodelgen.JPAMe
taModelEntityProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
NOTE: If you are using Eclipse based IDE, such as Spring Tool Suite, or Intellij
IDEA, you could have to install the Lombok plugin manually, check the Lombok
download page for installation information. Luckily, NetBeans IDE can recognise
the Lombok facilities automatcially.
Unlike JPA metadata generator which generates metedata source for JPA entities.
Lombok modifies target classes directly.
Execute javap Post.class in command line, you can get the follwing info.
65
Build REST API
#>javap classes\com\hantsylabs\restexample\springmvc\domain\Pos
t.class
Compiled from "Post.java"
public class com.hantsylabs.restexample.springmvc.domain.Post im
plements java.io.Serializable {
public com.hantsylabs.restexample.springmvc.domain.Post(java.l
ang.String, java.lang.String);
public static com.hantsylabs.restexample.springmvc.domain.Post
$PostBuilder builder();
public java.lang.Long getId();
public java.lang.String getTitle();
public java.lang.String getContent();
public com.hantsylabs.restexample.springmvc.domain.Post$Status
getStatus();
public com.hantsylabs.restexample.springmvc.domain.User getCre
atedBy();
public java.time.LocalDateTime getCreatedDate();
public com.hantsylabs.restexample.springmvc.domain.User getLas
tModifiedBy();
public java.time.LocalDateTime getLastModifiedDate();
public void setId(java.lang.Long);
public void setTitle(java.lang.String);
public void setContent(java.lang.String);
public void setStatus(com.hantsylabs.restexample.springmvc.dom
ain.Post$Status);
public void setCreatedBy(com.hantsylabs.restexample.springmvc.
domain.User);
public void setCreatedDate(java.time.LocalDateTime);
public void setLastModifiedBy(com.hantsylabs.restexample.sprin
gmvc.domain.User);
public void setLastModifiedDate(java.time.LocalDateTime);
public boolean equals(java.lang.Object);
protected boolean canEqual(java.lang.Object);
public int hashCode();
public java.lang.String toString();
public com.hantsylabs.restexample.springmvc.domain.Post();
public com.hantsylabs.restexample.springmvc.domain.Post(java.l
ang.Long, java.lang.String, java.lang.String, com.hantsylabs.res
texample.springmvc.domain.Post$Status, com.hantsylabs.restexampl
66
Build REST API
You can create a Post object using Post builder like this.
Compare to following legacy new an object, the Builder pattern is more friendly to
developers, and codes become more readable.
Model associations
Let's create other related models, Comment and User .
Comment class is associated with Post and User . Every comment should
be belong to a post, and has an author( User ).
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
67
Build REST API
@Table(name = "comments")
public class Comment implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
@Id()
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "content")
private String content;
@JoinColumn(name = "post_id")
@ManyToOne()
private Post post;
@ManyToOne
@JoinColumn(name = "created_by")
@CreatedBy
private User createdBy;
@Column(name = "created_on")
@CreatedDate
private LocalDateTime createdDate;
@Override
public int hashCode() {
int hash = 5;
hash = 89 * hash + Objects.hashCode(this.content);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
68
Build REST API
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Comment other = (Comment) obj;
if (!Objects.equals(this.content, other.content)) {
return false;
}
return true;
}
}
User class contains fields of a user account, including username and password
which used for authentication.
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
@Id()
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username")
private String username;
69
Build REST API
@Column(name = "password")
private String password;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
@Column(name = "role")
private String role;
@Column(name = "created_date")
@CreatedDate
private LocalDateTime createdDate;
@ManyToOne
@JoinColumn(name = "created_by")
@CreatedBy
User createdBy;
}
70
Build REST API
Generally, in Spring application, in order to make JPA work, you have to configure
a DataSource , EntityManagerFactory , TransactionManager .
JPA overview
JPA standardised Hibernate and it is part of Java EE specification since Java EE
5. Currently there are some popular JPA providers, such as Hibernate, OpenJPA,
EclipseLink etc. EclipseLink is shipped wtih Glassfish, and Hibernate is included
JBoss Wildfly/Redhat EAP.
In the above Modeling section, we have created models, which are JPA entities. In
this section, let's see how to make it work.
Configure DataSources
Like other ORM frameworks, you have to configure a DataSource.
@Configuration
public class DataSourceConfig {
@Inject
private Environment env;
@Bean
@Profile("dev")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
71
Build REST API
@Bean
@Profile("staging")
public DataSource testDataSource() {
BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUrl(env.getProperty(ENV_JDBC_URL));
bds.setUsername(env.getProperty(ENV_JDBC_USERNAME));
bds.setPassword(env.getProperty(ENV_JDBC_PASSWORD));
return bds;
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
JndiObjectFactoryBean ds = new JndiObjectFactoryBean();
ds.setLookupOnStartup(true);
ds.setJndiName("jdbc/postDS");
ds.setCache(true);
72
Build REST API
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<log4j.level>DEBUG</log4j.level>
<spring.profiles.active>dev</spring.profiles.active
>
<!-- hibernate -->
<hibernate.hbm2ddl.auto>create</hibernate.hbm2ddl.a
uto>
<hibernate.show_sql>true</hibernate.show_sql>
<hibernate.format_sql>true</hibernate.format_sql>
73
Build REST API
<profile>
<id>dev</id>
//...
<build>
<resources>
<resource>
<directory>src/main/resources-dev</directory
>
<filtering>true</filtering>
</resource>
</resources>
</build>
</profile>
Becomes:
@PropertySource("classpath:/app.properties")
@PropertySource(value = "classpath:/database.properties", i
gnoreResourceNotFound = true)
public class AppConfig {
NOTE: Read the Spring official document about Spring profile and Environment.
74
Build REST API
@Repository
@Transactional
public class PostRepository{
@PersistenceContext
private EntityManager em;
}
For example,
75
Build REST API
@Configuration
@EnableJpaRepositories(basePackages = {"com.hantsylabs.restexamp
le.springmvc"})
@EnableJpaAuditing(auditorAwareRef = "auditor")
public class JpaConfig {
@Bean
public AuditorAware<User> auditor() {
return () -> SecurityUtil.currentUser();
}
@CreatedBy
@CreatedDate
@LastModifiedBy
@LastModifiedDate
76
Build REST API
Repository
You can imagine a repository as a domain object collection, allow you retreive
data from it or save change state back.
Spring Data Commons project defines a series of interfaces for common data
operations for different storages, including NoSQL and RDBMS.
77
Build REST API
78
Build REST API
if (status != null) {
predicates.add(cb.equal(root.get(Post_.status), stat
us));
}
And you want to get a pageable result, you can use like this, just add a
Pageable argument.
79
Build REST API
Application Service
A service can delegate CRUD operations to repository, also act as gateway to
other bound context, such as messageing, sending email, fire events etc.
@Service
@Transactional
public class BlogService {
@Inject
public BlogService(PostRepository postRepository){
this.postRepository = postRepository;
}
80
Build REST API
if (post == null) {
throw new ResourceNotFoundException(id);
}
81
Build REST API
In this service there are some POJOs created for input request, response
presentation etc.
@NotBlank
private String title;
Some exceptions are threw in the service if the input data can not satisfy the
requirements. In the furthur post, I will focus on exception handling topic.
There is a DTOUtils which is responsible for data copy from one class to
another class.
82
Build REST API
/**
*
* @author Hantsy Bai<hantsy@gmail.com>
*/
public final class DTOUtils {
private DTOUtils() {
throw new InstantiationError( "Must not instantiate this
class" );
}
return list;
}
83
Build REST API
For RESTful applications, JSON and XML are the commonly used exchange
format, we do not need a template engine(such as Freemarker, Apache Velocity)
for view, Spring MVC will detect HTTP headers, such as Content Type, Accept
Type, etc. to determine how to produce corresponding view result. Most of the
time, we do not need to configure the view/view resolver explictly. This is called
Content negotiation. There is a ContentNegotiationManager bean which is
responsible for Content negotiation and enabled by default in the latest version.
The configuration details are motioned in before posts. We are jumping to write
@Controller to produce REST APIs.
@RestController
@RequestMapping(value = Constants.URI_API + Constants.URI_POSTS)
public class PostController {
84
Build REST API
@Inject
public PostController(BlogService blogService) {
this.blogService = blogService;
}
85
Build REST API
86
Build REST API
blogService.deletePostById(id);
@RequestMapping defines URL, HTTP methods etc are matched, the annotated
method will handle the request.
87
Build REST API
A POST method on /api/posts is use for creating a new post, return HTTP status
201, and set HTTP header Location value to the new created post url if the
creation is completed successfully.
Run
For none Spring Boot application, run it as a general web application in IDE.
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/angularjs-springmvc-sample</path>
</configuration>
</plugin>
mvn tomcat7:run
NOTE: The tomcat maven plugin development is not active, if you are using
Servlet 3.1 features, you could have to use other plugin instead.
Jetty is the fastest embedded Servlet container and wildly used in development
community.
88
Build REST API
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.3.7.v20160115</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<stopPort>8005</stopPort>
<stopKey>STOP</stopKey>
<webApp>
<contextPath>/angularjs-springmvc-sample</contextPat
h>
</webApp>
</configuration>
</plugin>
mvn jetty:run
Another frequently used is Cargo which provides support for all popular applcation
servers, and ready for all hot build tools, such as Ant, Maven, Gradle etc.
89
Build REST API
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<configuration>
<container>
<containerId>tomcat8x</containerId>
<type>embedded</type>
</container>
<configuration>
<properties>
<cargo.servlet.port>9000</cargo.servlet.port>
<cargo.logging>high</cargo.logging>
</properties>
</configuration>
</configuration>
</plugin>
For Spring boot application, it is simple, just run the application like this.
mvn spring-boot:run
By default, it uses Tomcat embedded server, but you can switch to Jetty and
JBoss Undertow if you like. Check the Spring boot docs for details.
Source Code
Check out sample codes from my github account.
90
Build REST API
Read the live version of thess posts from Gitbook:Building RESTful APIs with
Spring MVC.
91
Handle Exceptions
Handles Exceptions
In the real world applications, a user story can be described as different flows.
For example, when a user tries to register an account in this application. The
server side should check if the existance of the input username, if the username is
taken by other users, the server should stop the registration flow and wraps the
message into UsernameWasTakenException and throws it. Later the APIs
should translate it to client friend message and reasonable HTTP status code, and
finally they are sent to client and notify the user.
Define Exceptions
Define an exeption which stands for the exception path. For example,
ResourceNotFoundException indicates an resource is not found in the
applicatin when query the resource by id.
92
Handle Exceptions
if (post == null) {
throw new ResourceNotFoundException(id);
}
Translates exceptions
Internally, Spring has a series of built-in ExceptionTranslator s to translate the
exceptions to Spring declarative approaches, eg. all JDBC exceptions are
translated to Spring defined exceptions(DataAccessException and its subclasses).
In the presentation layer, these exceptions can be caught and converted into user
friendly message.
93
Handle Exceptions
You can extend this class and override the default exeption hanlder methods, or
add your exception hanlder to handle custom exceptions.
@ControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler extends ResponseEntityExceptio
nHandler {
@ExceptionHandler(value = {ResourceNotFoundException.class})
@ResponseBody
public ResponseEntity<ResponseMessage> handleResourceNotFoun
dException(ResourceNotFoundException ex, WebRequest request) {
if (log.isDebugEnabled()) {
log.debug("handling ResourceNotFoundException...");
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
Spring supports JSR 303(Bean validation) natively, the bean validation constraints
error can be gathered by BindingResult in the controller class.
94
Handle Exceptions
In the controller class, if the BindingResult has errors, then wraps the error
info into an exception.
95
Handle Exceptions
@ExceptionHandler(value = {InvalidRequestException.class})
public ResponseEntity<ResponseMessage> handleInvalidRequestExcep
tion(InvalidRequestException ex, WebRequest req) {
if (log.isDebugEnabled()) {
log.debug("handling InvalidRequestException...");
}
if (!fieldErrors.isEmpty()) {
fieldErrors.stream().forEach(e -> {
alert.addError(e.getField(), e.getCode(), e.getDefau
ltMessage());
});
}
The detailed validation errors are wrapped as content and sent to the client, and a
HTTP status to indicate the form data user entered is invalid.
96
Handle Exceptions
All business related exceptions should designed and converted to a valid HTTP
status code and essential messages.
200 OK
201 Created
204 NO Content
400 Bad Resquest
401 Not Authoried
403 Forbidden
409 Conflict
Source Code
Check out sample codes from my github account.
Read the live version of thess posts from Gitbook:Building RESTful APIs with
Spring MVC.
97
Test APIs
Testing
Before release your applicaiton to the public world, you have to make sure it
works as expected.
1. Write a test first, then run test and get failure, failure info indicates what to
do(You have not written any codes yet).
2. Code the implementation, and run test again and again, untill the test get
passed.
3. Adjust the test to add more featurs, and refactor the codes, till all
considerations are included.
But some developers prefer writing codes firstly and then write tests to verify
them, it is OK. There is no policy to force you accept TDD. For a skilled developer,
both are productive in work.
We have written some codes in the early posts, now it is time to add some test
codes to show up how to test Spring components.
Spring provides a test context environment for developers, it supports JUnit and
TestNG.
In this sample applcation, I will use JUnit as test runner, also use Mockito to test
service in isolation, and use Rest Assured BDD like fluent APIs to test REST from
client view.
98
Test APIs
public PostTest() {
}
@BeforeClass
public static void setUpClass() {
}
@AfterClass
public static void tearDownClass() {
}
@Before
public void setUp() {
post = new Post();
post.setTitle("test title");
post.setContent("test content");
}
@After
public void tearDown() {
post = null;
}
/**
* Test of getId method, of class Post.
*/
@Test
public void testPojo() {
assertEquals("test title", post.getTitle());
assertEquals("test content", post.getContent());
}
99
Test APIs
You could see the following output summary for this test.
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.hantsylabs.restexample.springmvc.domain.PostTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed:
0.015 sec - in com.hantsylabs.restexample.springmvc.domain.PostT
est
Results :
Test Service
BlogService depends on PostRepository , but most of time, we only want to
check if the business logic and flow correct in the BlogService and assume the
dependency PostRepository are always working as expected. Thus it is easy
to focus on testing BlogService itself.
100
Test APIs
Mockito provides the simplest approaches to mock the dependencies, and setup
the assumption, and provides an isolation environment to test BlogService .
@Configuration
public class MockDataConfig {
@Bean
public PostRepository postRepository() {
final Post post = createPost();
PostRepository posts = mock(PostRepository.class);
when(posts.save(any(Post.class))).thenAnswer((Invocation
OnMock invocation) -> {
Object[] args = invocation.getArguments();
Post result = (Post) args[0];
result.setId(post.getId());
result.setTitle(post.getTitle());
result.setContent(post.getContent());
result.setCreatedDate(post.getCreatedDate());
return result;
});
when(posts.findOne(1000L)).thenThrow(new ResourceNotFoun
dException(1000L));
when(posts.findOne(1L)).thenReturn(post);
when(posts.findAll(any(Specification.class), any(Pageabl
e.class))).thenReturn(new PageImpl(Arrays.asList(post), new Page
Request(0, 10), 1L));
when(posts.findAll()).thenReturn(Arrays.asList(post));
return posts;
}
101
Test APIs
@Bean
public CommentRepository commentRepository() {
return mock(CommentRepository.class);
}
@Bean
public BlogService blogService(PostRepository posts, Comment
Repository comments){
return new BlogService(posts, comments);
}
@Bean
public Post createPost() {
Post post = new Post();
post.setCreatedDate(new Date());
post.setId(1L);
post.setTitle("First post");
post.setContent("Content of my first post!");
post.setCreatedDate(new Date());
return post;
}
}
102
Test APIs
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {MockDataConfig.class})
public class MockBlogServiceTest {
@Inject
private PostRepository postRepository;
@Inject
private CommentRepository commentRepository;
@Test
public void testSavePost() {
PostForm form = new PostForm();
form.setTitle("saving title");
form.setContent("saving content");
//...
}
103
Test APIs
@Test(expected = ResourceNotFoundException.class)
public void testGetNoneExistingPost() {
blogService.findPostById(1000L);
}
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.hantsylabs.restexample.springmvc.test.MockBlogServic
eTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed:
1.943 sec - in com.hantsylabs.restexample.springmvc.test.MockBlo
gServiceTest
Results :
Integration test
We have known BlogService works when we mocked the dependencies.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppConfig.class, DataSourceConf
104
Test APIs
@Inject
private PostRepository postRepository;
@Inject
private BlogService blogService;
public BlogServiceTest() {
}
@Before
public void setUp() {
postRepository.deleteAll();
post = postRepository.save(Fixtures.createPost("My first
post", "content of my first post"));
assertNotNull(post.getId());
}
@After
public void tearDown() {
}
@Test
public void testSavePost() {
PostForm form = new PostForm();
form.setTitle("saving title");
form.setContent("saving content");
105
Test APIs
ls.getId());
assertNotNull(details.getId());
assertNotNull(updatedDetails.getId());
assertTrue("updating title".equals(updatedDetails.getTit
le()));
assertTrue("updating content".equals(updatedDetails.getC
ontent()));
@Test(expected = ResourceNotFoundException.class)
public void testGetNoneExistingPost() {
blogService.findPostById(1000L);
}
In the @Before method, all Post data are cleared for each tests, and save a
Post for further test assertion.
106
Test APIs
The above codes are similar with early Mockito version, the main difference is we
have switched configuraitons to a real database. Check the
@ContextConfiguration annotated on BlogServiceTest .
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.hantsylabs.restexample.springmvc.test.BlogServiceTes
t
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed:
5.908 sec - in com.hantsylabs.restexample.springmvc.test.BlogSer
viceTest
Results :
Test Controller
Spring provides a sort of mock APIs to emulate a Servlet container environment,
thus it is possbile to test MVC related feature without a real container.
@RunWith(MockitoJUnitRunner.class)
107
Test APIs
@Mock
private BlogService blogService;
@Mock
Pageable pageable = mock(PageRequest.class);
@InjectMocks
PostController postController;
@BeforeClass
public static void beforeClass() {
log.debug("==================before class===============
==========");
}
@AfterClass
public static void afterClass() {
log.debug("==================after class================
=========");
}
@Before
public void setup() {
log.debug("==================before test case===========
==============");
Mockito.reset();
MockitoAnnotations.initMocks(this);
mvc = standaloneSetup(postController)
.setCustomArgumentResolvers(new PageableHandlerM
ethodArgumentResolver())
.setViewResolvers(new ViewResolver() {
108
Test APIs
@Override
public View resolveViewName(String viewName,
Locale locale) throws Exception {
return new MappingJackson2JsonView();
}
})
.build();
}
@After
public void tearDown() {
log.debug("==================after test case============
=============");
}
@Test
public void savePost() throws Exception {
PostForm post = Fixtures.createPostForm("First Post", "C
ontent of my first post!");
when(blogService.savePost(any(PostForm.class))).thenAnsw
er(new Answer<PostDetails>() {
@Override
public PostDetails answer(InvocationOnMock invocatio
n) throws Throwable {
PostForm fm = (PostForm) invocation.getArgumentA
t(0, PostForm.class);
return result;
}
});
mvc.perform(post("/api/posts").contentType(MediaType.APP
LICATION_JSON).content(objectMapper.writeValueAsString(post)))
.andExpect(status().isCreated());
109
Test APIs
verify(blogService, times(1)).savePost(any(PostForm.clas
s));
verifyNoMoreInteractions(blogService);
}
@Test
public void retrievePosts() throws Exception {
PostDetails post1 = new PostDetails();
post1.setId(1L);
post1.setTitle("First post");
post1.setContent("Content of first post");
post1.setCreatedDate(new Date());
when(blogService.searchPostsByCriteria(anyString(), any(
Post.Status.class), any(Pageable.class)))
.thenReturn(new PageImpl(Arrays.asList(post1, po
st2), new PageRequest(0, 10, Direction.DESC, "createdDate"), 2))
;
verify(blogService, times(1))
.searchPostsByCriteria(anyString(), any(Post.Sta
tus.class), any(Pageable.class));
verifyNoMoreInteractions(blogService);
log.debug("get posts result @" + response.getResponse().
110
Test APIs
getContentAsString());
}
@Test
public void retrieveSinglePost() throws Exception {
when(blogService.findPostById(1L)).thenReturn(post1);
mvc.perform(get("/api/posts/1").accept(MediaType.APPLICA
TION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType("application/js
on;charset=UTF-8"))
.andExpect(jsonPath("id").isNumber());
verify(blogService, times(1)).findPostById(1L);
verifyNoMoreInteractions(blogService);
}
@Test
public void removePost() throws Exception {
when(blogService.deletePostById(1L)).thenReturn(true);
mvc.perform(delete("/api/posts/{id}", 1L))
.andExpect(status().isNoContent());
verify(blogService, times(1)).deletePostById(1L);
verifyNoMoreInteractions(blogService);
}
@Test()
public void notFound() {
when(blogService.findPostById(1000L)).thenThrow(new Reso
urceNotFoundException(1000L));
try {
111
Test APIs
mvc.perform(get("/api/posts/1000").accept(MediaType.
APPLICATION_JSON))
.andExpect(status().isNotFound());
} catch (Exception ex) {
log.debug("exception caught @" + ex);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppConfig.class, Jackson2Object
MapperConfig.class, DataSourceConfig.class, JpaConfig.class, Dat
aJpaConfig.class, WebConfig.class})
@WebAppConfiguration
public class PostControllerTest {
@Inject
WebApplicationContext wac;
@Inject
ObjectMapper objectMapper;
@Inject
private PostRepository postRepository;
112
Test APIs
@BeforeClass
public static void beforeClass() {
log.debug("==================before class===============
==========");
}
@AfterClass
public static void afterClass() {
log.debug("==================after class================
=========");
}
@Before
public void setup() {
log.debug("==================before test case===========
==============");
mvc = webAppContextSetup(this.wac).build();
postRepository.deleteAll();
post = postRepository.save(Fixtures.createPost("My first
post", "content of my first post"));
}
@After
public void tearDown() {
log.debug("==================after test case============
=============");
}
@Test
public void savePost() throws Exception {
PostForm post = Fixtures.createPostForm("First Post", "C
ontent of my first post!");
mvc.perform(post("/api/posts").contentType(MediaType.APP
LICATION_JSON).content(objectMapper.writeValueAsString(post)))
113
Test APIs
.andExpect(status().isCreated());
@Test
public void retrievePosts() throws Exception {
@Test
public void retrieveSinglePost() throws Exception {
mvc.perform(get("/api/posts/{id}", post.getId()).accept(
MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType("application/js
on"))
.andExpect(jsonPath("$.id").isNumber())
.andExpect(jsonPath("$.title", is("My first post
")));
@Test
public void removePost() throws Exception {
mvc.perform(delete("/api/posts/{id}", post.getId()))
.andExpect(status().isNoContent());
}
114
Test APIs
@Test()
public void notFound() {
try {
mvc.perform(get("/api/posts/1000").accept(MediaType.
APPLICATION_JSON))
.andExpect(status().isNotFound());
} catch (Exception ex) {
log.debug("exception caught @" + ex);
}
}
In this test class, the Mockito codes are replaced with Spring test, and load the
configuraitons defined in this project. It is close to the final production
environment, except there is not a real Servlet container.
@RunWith(BlockJUnit4ClassRunner.class)
public class IntegrationTest {
@BeforeClass
public static void init() {
log.debug("==================before class===============
==========");
}
@Before
115
Test APIs
@After
public void afterTestCase() {
log.debug("==================after test case============
=============");
}
@Test
public void testPostCrudOperations() throws Exception {
PostForm newPost = Fixtures.createPostForm("My first pos
t", "content of my first post");
String postsUrl = BASE_URL + "api/posts";
116
Test APIs
@Test
public void noneExistingPost() throws Exception {
String noneExistingPostUrl = BASE_URL + "api/posts/1000"
;
try {
template.getForEntity(noneExistingPostUrl, Post.clas
s);
} catch (HttpClientErrorException e) {
assertTrue(HttpStatus.NOT_FOUND.equals(e.getStatusCo
de()));
}
}
}
RestTemplate is use for interaction with remote REST API, this test acts as a
remote client, and shake hands with our backend through REST APIs.
117
Test APIs
ry(getRequestFactory(),
interceptors));
}
@Override
public ClientHttpResponse intercept(HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution) throws IOE
xception {
byte[] token = Base64.getEncoder().encode(
(this.username + ":" + this.password).getByt
es());
request.getHeaders().add("Authorization", "Basic " +
new String(token));
return execution.execute(request, body);
}
}
}
118
Test APIs
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<includes>
<include>**/*IntegrationTest*</include>
</includes>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution>
<id>verify</id>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
119
Test APIs
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.3.7.v20160115</version>
<configuration>
<scanIntervalSeconds>10</scanIntervalSeconds>
<stopPort>8005</stopPort>
<stopKey>STOP</stopKey>
<webApp>
<contextPath>/angularjs-springmvc-sample</contextPat
h>
</webApp>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<goal>stop</goal>
<goal>start</goal>
</goals>
<configuration>
<scanIntervalSeconds>0</scanIntervalSeconds>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
In the pre-integration-test phase, check if the jetty is running and starts up it, in
post-integration-test phase, shutdown the container.
120
Test APIs
In the console, after all unit tess are done, it will start jetty and deploy the project
war into jetty and run the IntegrationTest on it.
Results :
[WARNING] File encoding has not been set, using platform encodin
g Cp1252, i.e. build is platform dependent!
[INFO]
[INFO] --- jetty-maven-plugin:9.3.7.v20160115:stop (stop-jetty)
@ angularjs-springmvc-sample ---
[INFO]
[INFO] --- maven-failsafe-plugin:2.12.4:verify (verify) @ angula
rjs-springmvc-sample ---
As you see in the console, after the test is done, it is trying to shutdown jetty.
Rest Assured
Rest Assured provides BDD like syntax, such as given, when, then, it is friendly
for those familiar with BDD.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Slf4j
public class RestAssuredApplicationTest extends WebIntegrationTe
stBase {
121
Test APIs
@Before
public void beforeTest() {
super.setup();
RestAssured.port = port;
}
@Test
public void testDeletePostNotExisted() {
String location = "/api/posts/1000";
given()
.auth().basic(USER_NAME, PASSWORD)
.contentType(ContentType.JSON)
.when()
.delete(location)
.then()
.assertThat()
.statusCode(HttpStatus.SC_NOT_FOUND);
}
@Test
public void testGetPostNotExisted() {
String location = "/api/posts/1000";
given()
.auth().basic(USER_NAME, PASSWORD)
.contentType(ContentType.JSON)
.when()
.get(location)
.then()
.assertThat()
.statusCode(HttpStatus.SC_NOT_FOUND);
}
@Test
public void testPostFormInValid() {
PostForm form = new PostForm();
given()
.auth().basic(USER_NAME, PASSWORD)
122
Test APIs
.body(form)
.contentType(ContentType.JSON)
.when()
.post("/api/posts")
.then()
.assertThat()
.statusCode(HttpStatus.SC_BAD_REQUEST);
}
@Test
public void testPostCRUD() {
PostForm form = new PostForm();
form.setTitle("test title");
form.setContent("test content");
given().auth().basic(USER_NAME, PASSWORD)
.contentType(ContentType.JSON)
.when()
.get(location)
.then()
.assertThat()
.body("title", is("test title"))
123
Test APIs
given()
.auth().basic(USER_NAME, PASSWORD)
.body(updateForm)
.contentType(ContentType.JSON)
.when()
.put(location)
.then()
.assertThat()
.statusCode(HttpStatus.SC_NO_CONTENT);
given().auth().basic(USER_NAME, PASSWORD)
.contentType(ContentType.JSON)
.when()
.get(location)
.then()
.assertThat()
.body("title", is("test udpate title"))
.body("content", is("test update content"));
given()
.auth().basic(USER_NAME, PASSWORD)
.contentType(ContentType.JSON)
.when()
.delete(location)
.then()
.assertThat()
.statusCode(HttpStatus.SC_NO_CONTENT);
given().auth().basic(USER_NAME, PASSWORD)
.contentType(ContentType.JSON)
.when()
.get(location)
.then()
.assertThat()
124
Test APIs
.statusCode(HttpStatus.SC_NOT_FOUND);
This test is also run as client, and interacts with backend via REST API.
The above Rest Assured sample codes are available in the Spring Boot version,
check out the codes and experience yourself.
It also includes a simple JBehave sample, if you are a JBehave user, you maybe
interested in it.
Source Code
Check out sample codes from my github account.
Read the live version of thess posts from Gitbook:Building RESTful APIs with
Spring MVC.
125
Visualize and document REST APIs
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.version}</version>
</dependency>
126
Visualize and document REST APIs
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket postsApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("public-api")
.apiInfo(apiInfo())
.select()
.paths(postPaths())
.build();
}
127
Visualize and document REST APIs
When the application starts up, it will scan all Controllers and generate
Swagger schema definition at runtime, Swagger UI will read definitions and
render user friendly UI for REST APIs.
128
Visualize and document REST APIs
You can save this page content as a json file and upload to http://editor.swagger.io
and edit it online.
The Swagger schema definition generation will consume lots of system resourcs
at runtime.
129
Visualize and document REST APIs
<swagger2markup.extensions.dynamicOverview.cont
entPath>${project.basedir}/src/docs/asciidoc/extensions/over
view</swagger2markup.extensions.dynamicOverview.contentPath>
<swagger2markup.extensions.dynamicDefinitions.c
ontentPath>${project.basedir}/src/docs/asciidoc/extensions/d
efinitions</swagger2markup.extensions.dynamicDefinitions.con
tentPath>
<swagger2markup.extensions.dynamicPaths.content
130
Visualize and document REST APIs
Path>${project.basedir}/src/docs/asciidoc/extensions/paths</
swagger2markup.extensions.dynamicPaths.contentPath>
<swagger2markup.extensions.dynamicSecurity.cont
entPath>${project.basedir}src/docs/asciidoc/extensions/secur
ity/</swagger2markup.extensions.dynamicSecurity.contentPath>
<swagger2markup.extensions.springRestDocs.snipp
etBaseUri>${swagger.snippetOutput.dir}</swagger2markup.exten
sions.springRestDocs.snippetBaseUri>
<swagger2markup.extensions.springRestDocs.defau
ltSnippets>true</swagger2markup.extensions.springRestDocs.de
faultSnippets>
</config>
</configuration>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>convertSwagger2markup</goal>
</goals>
</execution>
</executions>
</plugin>
131
Visualize and document REST APIs
<artifactId>jruby-complete</artifactId>
<version>${jruby.version}</version>
</dependency>
<dependency>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctorj-pdf</artifactId>
<version>${asciidoctorj-pdf.version}</version>
</dependency>
</dependencies>
<!-- Configure generic document generation settings -->
<configuration>
<sourceDirectory>${asciidoctor.input.directory}</so
urceDirectory>
<sourceDocumentName>index.adoc</sourceDocumentName>
<sourceHighlighter>coderay</sourceHighlighter>
<attributes>
<doctype>book</doctype>
<toc>left</toc>
<toclevels>3</toclevels>
<numbered></numbered>
<hardbreaks></hardbreaks>
<sectlinks></sectlinks>
<sectanchors></sectanchors>
<generated>${generated.asciidoc.directory}</gen
erated>
</attributes>
</configuration>
<!-- Since each execution can only handle one backend,
run
separate executions for each desired output type -->
<executions>
<execution>
<id>output-html</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html5</backend>
132
Visualize and document REST APIs
<outputDirectory>${asciidoctor.html.output.
directory}</outputDirectory>
</configuration>
</execution>
<execution>
<id>output-pdf</id>
<phase>test</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>pdf</backend>
<outputDirectory>${asciidoctor.pdf.output.d
irectory}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
spring-restdocs will generate the sample code snippets from test, which
can be combined into the final docs.
133
Visualize and document REST APIs
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-spring-restdocs-ext</artifac
tId>
<version>${swagger2markup.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
@WebAppConfiguration
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class, SwaggerConfig
.class})
public class MockMvcApplicationTest {
@Rule
public final JUnitRestDocumentation restDocumentation =
new JUnitRestDocumentation(System.getProperty("io.springfox
.staticdocs.snippetsOutputDir"));
@Inject
private WebApplicationContext context;
@Inject
private ObjectMapper objectMapper;
134
Visualize and document REST APIs
@Inject
private PostRepository postRepository;
@Before
public void setUp() {
this.mockMvc = webAppContextSetup(this.context)
.apply(documentationConfiguration(this.rest
Documentation))
.alwaysDo(document("{method-name}",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint())))
.build();
savedIdentity = postRepository.save(newEntity());
}
@Test
public void createSpringfoxSwaggerJson() throws Excepti
on {
//String designFirstSwaggerLocation = Swagger2Marku
pTest.class.getResource("/swagger.yaml").getPath();
135
Visualize and document REST APIs
//SwaggerAssertions.assertThat(Swagger20Parser.pars
e(springfoxSwaggerJson)).isEqualTo(designFirstSwaggerLocatio
n);
}
// @Test
// public void convertToAsciiDoc() throws Exception
{
// this.mockMvc.perform(get("/v2/api-docs")
// .accept(MediaType.APPLICATION_JSON))
// .andDo(
// Swagger2MarkupResultHandler.o
utputDirectory("src/docs/asciidoc")
// .withExamples(snippetsDir).bu
ild())
// .andExpect(status().isOk());
// }
@Test
public void getAllPosts() throws Exception {
this.mockMvc
.perform(
get("/api/posts/{id}", savedIdentit
y.getId())
.accept(MediaType.APPLICATION_JSON)
)
//.andDo(document("get_a_post", preprocessR
esponse(prettyPrint())))
.andExpect(status().isOk());
}
@Test
public void getAllIdentities() throws Exception {
this.mockMvc
.perform(
get("/api/posts")
.accept(MediaType.ALL)
)
//.andDo(document("get_all_posts"))
.andExpect(status().isOk());
136
Visualize and document REST APIs
@Test
public void createPost() throws Exception {
this.mockMvc
.perform(
post("/api/posts")
.contentType(MediaType.APPLICATION_
JSON)
.content(newEntityAsJson())
)
//.andDo(document("create_a_new_post"))
.andExpect(status().isCreated());
}
@Test
public void updatePost() throws Exception {
this.mockMvc
.perform(
put("/api/posts/{id}", savedIdentit
y.getId())
.contentType(MediaType.APPLICATION_
JSON)
.content(newEntityAsJson())
)
//.andDo(document("update_an_existing_post"
))
.andExpect(status().isNoContent());
}
@Test
public void deletePost() throws Exception {
this.mockMvc
.perform(
delete("/api/posts/{id}", savedIden
tity.getId())
.contentType(MediaType.APPLICATION_
JSON)
)
//.andDo(document("delete_an_existing_post"
137
Visualize and document REST APIs
))
.andExpect(status().isNoContent());
}
return post;
}
4. Run mvn clean verify to execute all tests and generate HTML5 and PDF
file for the REST APIs.
138
Visualize and document REST APIs
139
Visualize and document REST APIs
Source Code
Check out sample codes from my github account.
Read the live version of thess posts from Gitbook:Building RESTful APIs with
Spring MVC.
140
Secure APIs
Secures APIs
We have configured Spring Security in before posts.
In this post, I will show you using Spring Security to protect APIs, aka provides
Anthentication and Anthorization service for this sample application.
Authentication
In Spring security, it is easy to configure JAAS compatible authentication strategy,
such as FORM, BASIC, X509 Certiciate etc.
Motioned in before posts, the simplest way to configure Spring security is using
AuthenticationManagerBuilder to build essential required resources.
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder())
.withUser("admin").password("test123").authorities("
ROLE_ADMIN")
.and()
.withUser("test").password("test123").authoritie
s("ROLE_USER");
}
141
Secure APIs
If you want to store users into your database, firstly create a custom
UserDetailsService bean and implement the findByUsername method and
return a UserDetails object.
@Override
public UserDetails loadUserByUsername(String username) throw
s UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("username not fo
und:" + username);
}
return user;
142
Secure APIs
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User implements UserDetails, Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
@Id()
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "name")
private String name;
@Column(name = "email")
private String email;
@Column(name = "role")
private String role;
@Column(name = "created_date")
@CreatedDate
private LocalDateTime createdDate;
143
Secure APIs
{
return this.username;
}
return name;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities
() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_"
+ this.role));
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
144
Secure APIs
return true;
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(new SimpleUserDetailsServiceImpl(use
rRepository))
.passwordEncoder(passwordEncoder);
}
Anthorization
Once user is authenticated, when he tries to access some resources, such as
URL, or execute some methods, it should check if the resource is protected, or
has granted permissions on executing the methods.
145
Secure APIs
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers("/api/ping")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated()
//....
}
The access control is filter by the Matcher , there are two built-in matchers,
Apache Ant path matcher, and perl like regex matchers. The later is a little
complex, but more powerful.
http...antMatchers(HttpMethod.POST,
"/api/posts").hasRoles("ADMIN") indicates only users that have been
granted ADMIN role have permission to create a new post.
Combined with resource URLs and HTTP methods, it follows rest convention
exactly.
In the a real world application, you can centralize the URL pattern, HTTP Method,
and granted ROLES into a certain persistent storage(such as RDBMS or NOSQL)
and desgin a friendly web UI to control resource access.
146
Secure APIs
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled
= true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@PreAuthorized("hasRole('ADMIN')")
public void savePost(Post post){}
@PreAuthorized("#post.author.id==principal.id")
public void update(Post post){}
@Secured("ROLE_USER")
public void savePost(Post post){}
Programmatic authorizations
147
Secure APIs
And you can also inject current authenticated Principal like this.
After got the security principal info, you can control the authorizations in codes.
Source Code
Check out sample codes from my github account.
Read the live version of thess posts from Gitbook:Building RESTful APIs with
Spring MVC.
148
Upgrade to Spring Boot 1.4
New starter:spring-boot-starter-test
Spring Boot 1.4 brings a new starter for test scope, named spring-boot-
starter-test .
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Instead of:
149
Upgrade to Spring Boot 1.4
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
150
Upgrade to Spring Boot 1.4
@LocalSeverPort
int port;
@RunWith(SpringRunner.class)
If you have to use other runners instead of SpringRunner , and want to use the
Spring test context in the tests, declare a SpringClassRule and
SpringMethodRule in the test to fill the gap.
151
Upgrade to Spring Boot 1.4
@RunWith(AnotherRunner.class)
public class SomeTest{
@ClassRule
public static final SpringClassRule SPRING_CLASS_RULE = new
SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringM
ethodRule();
@WebMvcTest(PostController.class)
public class PostControllerMvcTest{
152
Upgrade to Spring Boot 1.4
@RestClientTest provides REST client environment for the test, esp the
RestTemplateBuilder etc.
These annotations are not composed with SpringBootTest , they are combined
with a series of AutoconfigureXXX and a @TypeExcludesFilter
annotations.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {}
You can add your @AutoconfigureXXX annotation to override the default config.
@AutoConfigureTestDatabase(replace=NONE)
@DataJpaTest
public class TestClass{
}
JsonComponent
@JsonComponent is a specific @Component to register custome Jackson
JsonSerializer and JsonDeserializer .
153
Upgrade to Spring Boot 1.4
@JsonComponent
@Slf4j
public class LocalDateTimeJsonComponent {
@Override
public void serialize(LocalDateTime value, JsonGenerator
jgen, SerializerProvider provider) throws IOException {
jgen.writeString(value.atZone(ZoneId.systemDefault()
).toInstant().toString());
}
}
@Override
public LocalDateTime deserialize(JsonParser p, Deseriali
zationContext ctxt) throws IOException, JsonProcessingException
{
ObjectCodec codec = p.getCodec();
JsonNode tree = codec.readTree(p);
String dateTimeAsString = tree.textValue();
log.debug("dateTimeString value @" + dateTimeAsStrin
g);
return LocalDateTime.ofInstant(Instant.parse(dateTim
eAsString), ZoneId.systemDefault());
}
}
}
If you are using the Spring Boot default Jackson configuration, it will be activated
by default when the application starts up.
154
Upgrade to Spring Boot 1.4
scanned at all.
@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder(JsonCompo
nentModule jsonComponentModule) {
return builder;
}
@RunWith(SpringRunner.class)
public class MockBeanTest {
@MockBean
private UserRepository userRepository;
155
Upgrade to Spring Boot 1.4
@TestConfiguration
static class TestConfig{
}
@TestComponent
static class TestBean{}
Spring 4.3
There are a few features added in 4.3, the following is impressive.
Composed annotations
The effort of Spring Composed are merged into Spring 4.3.
A series of new composed annotations are available, but the naming is a little
different from Spring Composed.
For example, a RestController can be simplfied by the new annotations, list as the
following table.
156
Upgrade to Spring Boot 1.4
For example, in the old Spring 4.2, an custom exception handler class looks like
the following.
@ControllerAdvice()
public class RestExceptionHandler {
@ExceptionHandler(value = {SomeException.class})
@ResponseBody
public ResponseEntity<ResponseMessage> handleGenericExceptio
n(SomeException ex, WebRequest request) {
}
}
@RestControllerAdvice()
public class RestExceptionHandler {
@ExceptionHandler(value = {SomeException.class})
public ResponseEntity<ResponseMessage> handleGenericExceptio
n(SomeException ex, WebRequest request) {
}
}
157
Upgrade to Spring Boot 1.4
@RestController
@RequestMapping(value = Constants.URI_API_PREFIX + Constants.URI
_POSTS)
public class PostController {
@Inject
public PostController(BlogService blogService) {
this.blogService = blogService;
}
}
@RestController
@RequestMapping(value = Constants.URI_API_PREFIX + Constants.URI
_POSTS)
public class PostController {
158
Upgrade to Spring Boot 1.4
@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class ApplicationSecurity extends WebSecurityCo
nfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.userDetailsService(new SimpleUserDetailsServiceImpl(u
serRepository))
.passwordEncoder(passwordEncoder);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throw
s Exception {
return super.authenticationManagerBean();
}
159
Upgrade to Spring Boot 1.4
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEn
coder();
return passwordEncoder;
}
@Bean
public UserDetailsService userDetailsService(UserRepository user
Repository){
return new SimpleUserDetailsServiceImpl(userRepository);
}
@Bean
public WebSecurityConfigurerAdapter securityConfig(){
return new WebSecurityConfigurerAdapter() {
@Override
protected void configure(HttpSecurity http) throws Excep
tion {//...}
}
More details can be found in the What’s New in Spring Security 4.1 chapter of
Spring Secuirty documentation.
Hibernate 5.2
The biggest change of Hibernate 5.2 is the packages had been reorganised,
Hibernate 5.2 is Java 8 ready now.
160
Upgrade to Spring Boot 1.4
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-java8</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
NOTE:If you are using Spring 4.2 with Hibernate 5.2.0.Final, it could break some
dependencis, such as spring-orm , spring-boot-data-jpa-starter which
depends on hibernate-entitymanager. Spring Boot 1.4.0.RC1 and Spring 4.3 GA
fixed the issues. But I noticed in the Hibernate 5.2.1.Final, hibernate-
entitymanager is back.
Hibernate 5.2 also added Java Stream APIs support, I hope it will be available in
the next JPA specification.
Source code
Clone the codes from Github account.
161