Spring là một trong những framework Java EE được sử dụng phổ biến nhất. Trước đây, chúng ta đã tìm hiểu cách sử dụng Spring MVC để tạo các ứng dụng web Java. Hôm nay, chúng ta sẽ học cách tạo dịch vụ web RESTful sử dụng Spring MVC, sau đó kiểm tra hoạt động của chúng bằng Rest client. Cuối cùng, chúng ta cũng sẽ tìm hiểu cách gọi dịch vụ web RESTful Spring bằng Spring RestTemplate API.
Spring REST
Chúng ta sẽ sử dụng phiên bản Spring 4.0.0.RELEASE và tích hợp Jackson JSON với Spring để gửi response JSON trong các lệnh gọi REST. Bài hướng dẫn này được tạo trên IDE Spring STS để dễ dàng tạo cấu trúc dự án Spring MVC cơ bản, sau đó được mở rộng để triển khai kiến trúc RESTful.
Sau khi tạo một project Spring MVC mới trong STS, project cuối cùng sẽ trông như hình bên dưới. Ta sẽ lần lượt xem xét từng thành phần của nó ở các bước dưới.
Các file cấu hình XML của Spring REST
File pom.xml của chúng ta trông như sau:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.journaldev</groupId>
<artifactId>SpringRestExample</artifactId>
<name>SpringRestExample</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>1.6</java-version>
<org.springframework-version>4.0.0.RELEASE</org.springframework-version>
<org.aspectj-version>1.7.4</org.aspectj-version>
<org.slf4j-version>1.7.5</org.slf4j-version>
<jackson.databind-version>2.2.3</jackson.databind-version>
</properties>
<dependencies>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.databind-version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Công cụ STS tạo file pom.xml một cách tự động. Tuy nhiên, phiên bản Spring Framework, AspectJ, SLF4J, và Jackson đã được cập nhật lên bản mới nhất tại thời điểm viết bài. Hầu hết các nội dung trong file không có gì đặc biệt, điểm quan trọng cần lưu ý là việc thêm thư viện Jackson JSON vào phần dependency vì chúng ta sẽ dùng chúng để chuyển đổi giữa đối tượng Java và JSON.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="https://java.sun.com/xml/ns/javaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
File này được tạo tự động và chưa thay đổi gì. Tuy nhiên, nếu bạn muốn thay đổi các file cấu hình context và địa chỉ của chúng, bạn có thể thực hiện trong file web.xml.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
</beans>
File này chứa các tài nguyên dùng chung mà tất cả các web component đều có thể thấy. Chúng ta sẽ tạo một service REST đơn giản, vì vậy chưa thay đổi gì ở đây.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="https://www.springframework.org/schema/mvc"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="https://www.springframework.org/schema/beans"
xmlns:context="https://www.springframework.org/schema/context"
xsi:schemaLocation="https://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<!-- Configure to plugin JSON as request and response in method handler -->
<beans:bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<beans:property name="messageConverters">
<beans:list>
<beans:ref bean="jsonMessageConverter"/>
</beans:list>
</beans:property>
</beans:bean>
<!-- Configure bean to convert JSON to POJO and vice versa -->
<beans:bean id="jsonMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
</beans:bean>
<context:component-scan base-package="com.journaldev.spring.controller" />
</beans:beans>
Hầu hết nội dung được tạo tự động và chứa các cấu hình mẫu. Tuy nhiên, cần lưu ý phần <annotation-driven /> để hỗ trợ cấu hình dựa trên annotation và việc thêm MappingJackson2HttpMessageConverter vào messageConverters của RequestMappingHandlerAdapter.
Điều này cho phép Jackson API hoạt động để chuyển đổi giữa JSON và Java Bean. Với cấu hình này, chúng ta sẽ sử dụng JSON trong phần thân request và nhận dữ liệu JSON trong response.
Các lớp model trong Spring REST
Chúng ta hãy viết một lớp POJO đơn giản đóng vai trò là dữ liệu đầu vào và đầu ra cho các method của web service RESTful.
package com.journaldev.spring.model;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.DateSerializer;
public class Employee implements Serializable{
private static final long serialVersionUID = -7788619177798333712L;
private int id;
private String name;
private Date createdDate;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@JsonSerialize(using=DateSerializer.class)
public Date getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
}
Điểm quan trọng nhất cần lưu ý là việc sử dụng annotation @JsonSerialize để dùng lớp DateSerializer cho việc chuyển đổi kiểu Date của Java sang định dạng JSON và ngược lại.
Các end point của dịch vụ web RESTful Spring
Chúng ta sẽ có các endpoint của web service REST như sau.
STT | URI | Phương thức HTTP | Chi tiết |
---|---|---|---|
1 | /rest/emp/dummy | GET | Service kiểm tra “sức khỏe”, chèn dữ liệu mẫu vào kho lưu trữ dữ liệu Employees |
2 | /rest/emp/{id} | GET | Lấy đối tượng Employee dựa trên id |
3 | /rest/emps | GET | Lấy danh sách tất cả Employee trong kho dữ liệu |
4 | /rest/emp/create | POST | Tạo đối tượng Employee và lưu trữ |
5 | /rest/emp/delete/{id} | PUT | Xóa đối tượng Employee khỏi kho dữ liệu dựa trên id |
Chúng ta có một lớp định nghĩa tất cả các URI này dưới dạng hằng số String.
package com.journaldev.spring.controller;
public class EmpRestURIConstants {
public static final String DUMMY_EMP = "/rest/emp/dummy";
public static final String GET_EMP = "/rest/emp/{id}";
public static final String GET_ALL_EMP = "/rest/emps";
public static final String CREATE_EMP = "/rest/emp/create";
public static final String DELETE_EMP = "/rest/emp/delete/{id}";
}
Lớp Controller của dịch vụ web RESTful sử dụng Spring
Lớp EmployeeController của chúng ta sẽ cung cấp tất cả các endpoint đã đề cập ở trên. Hãy xem qua code của nó trước, sau đó chúng ta sẽ tìm hiểu kĩ hơn về từng phương thức.
package com.journaldev.spring.controller;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.journaldev.spring.model.Employee;
/**
* Handles requests for the Employee service.
*/
@Controller
public class EmployeeController {
private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class);
//Map to store employees, ideally we should use database
Map<Integer, Employee> empData = new HashMap<Integer, Employee>();
@RequestMapping(value = EmpRestURIConstants.DUMMY_EMP, method = RequestMethod.GET)
public @ResponseBody Employee getDummyEmployee() {
logger.info("Start getDummyEmployee");
Employee emp = new Employee();
emp.setId(9999);
emp.setName("Dummy");
emp.setCreatedDate(new Date());
empData.put(9999, emp);
return emp;
}
@RequestMapping(value = EmpRestURIConstants.GET_EMP, method = RequestMethod.GET)
public @ResponseBody Employee getEmployee(@PathVariable("id") int empId) {
logger.info("Start getEmployee. ID="+empId);
return empData.get(empId);
}
@RequestMapping(value = EmpRestURIConstants.GET_ALL_EMP, method = RequestMethod.GET)
public @ResponseBody List<Employee> getAllEmployees() {
logger.info("Start getAllEmployees.");
List<Employee> emps = new ArrayList<Employee>();
Set<Integer> empIdKeys = empData.keySet();
for(Integer i : empIdKeys){
emps.add(empData.get(i));
}
return emps;
}
@RequestMapping(value = EmpRestURIConstants.CREATE_EMP, method = RequestMethod.POST)
public @ResponseBody Employee createEmployee(@RequestBody Employee emp) {
logger.info("Start createEmployee.");
emp.setCreatedDate(new Date());
empData.put(emp.getId(), emp);
return emp;
}
@RequestMapping(value = EmpRestURIConstants.DELETE_EMP, method = RequestMethod.PUT)
public @ResponseBody Employee deleteEmployee(@PathVariable("id") int empId) {
logger.info("Start deleteEmployee.");
Employee emp = empData.get(empId);
empData.remove(empId);
return emp;
}
}
Để đơn giản, ta lưu trữ tất cả dữ liệu employee trong HashMap empData. Annotation @RequestMapping được dùng để map URI của request tới phương thức xử lý (handler method).
Chúng ta cũng có thể chỉ định phương thức HTTP mà client cần sử dụng để gọi phương thức REST. Annotation @ResponseBody được dùng để đưa đối tượng trả về vào phần thân của response. Khi đối tượng response được trả về bởi phương thức xử lý, MappingJackson2HttpMessageConverter sẽ hoạt động để chuyển đổi nó thành response JSON.
Annotation @PathVariable là cách dễ dàng để trích xuất dữ liệu từ URI của REST và map vào tham số của phương thức. Annotation @RequestBody được dùng để map dữ liệu JSON từ request body vào đối tượng Employee, việc này cũng được thực hiện bởi MappingJackson2HttpMessageConverter.
Phần code còn lại khá đơn giản và dễ hiểu. Ứng dụng của chúng ta đã sẵn sàng để kiểm thử và triển khai. Chỉ cần export ra file WAR và sao chép vào thư mục web app của servlet container. Nếu bạn đã cấu hình server trong STS, bạn chỉ cần chạy ứng dụng trên server đó để triển khai.
Ở đây ta sử dụng WizTools RestClient để thực hiện các lệnh gọi REST, nhưng bạn cũng có thể dùng extension Postman trên Chrome. Các ảnh chụp màn hình bên dưới minh họa các lệnh gọi khác nhau đến REST API do ứng dụng của chúng ta cung cấp và kết quả trả về.
Kiểm tra Health Check – Lệnh gọi REST lấy Employee mẫu
Lệnh gọi REST POST tạo Employee: Đảm bảo Content-Type của request được đặt là “application/json”, nếu không bạn sẽ gặp lỗi HTTP 415.
Lệnh gọi REST lấy Employee
Lệnh gọi REST xóa Employee
Lệnh gọi REST lấy tất cả Employee
Chương trình Spring Rest Client
Các Rest client rất hữu ích để kiểm thử web service REST. Nhưng trong đa số trường hợp, chúng ta cần gọi các service REST từ chính chương trình của mình.
Chúng ta có thể sử dụng RestTemplate của Spring để dễ dàng gọi các phương thức này. Dưới đây là một chương trình đơn giản gọi các phương thức REST của ứng dụng bằng RestTemplate API.
package com.journaldev.spring;
import java.util.LinkedHashMap;
import java.util.List;
import org.springframework.web.client.RestTemplate;
import com.journaldev.spring.controller.EmpRestURIConstants;
import com.journaldev.spring.model.Employee;
public class TestSpringRestExample {
public static final String SERVER_URI = "https://localhost:9090/SpringRestExample";
public static void main(String args[]){
testGetDummyEmployee();
System.out.println("*****");
testCreateEmployee();
System.out.println("*****");
testGetEmployee();
System.out.println("*****");
testGetAllEmployee();
}
private static void testGetAllEmployee() {
RestTemplate restTemplate = new RestTemplate();
//we can't get List<Employee> because JSON convertor doesn't know the type of
//object in the list and hence convert it to default JSON object type LinkedHashMap
List<LinkedHashMap> emps = restTemplate.getForObject(SERVER_URI+EmpRestURIConstants.GET_ALL_EMP, List.class);
System.out.println(emps.size());
for(LinkedHashMap map : emps){
System.out.println("ID="+map.get("id")+",Name="+map.get("name")+",CreatedDate="+map.get("createdDate"));;
}
}
private static void testCreateEmployee() {
RestTemplate restTemplate = new RestTemplate();
Employee emp = new Employee();
emp.setId(1);emp.setName("Pankaj Kumar");
Employee response = restTemplate.postForObject(SERVER_URI+EmpRestURIConstants.CREATE_EMP, emp, Employee.class);
printEmpData(response);
}
private static void testGetEmployee() {
RestTemplate restTemplate = new RestTemplate();
Employee emp = restTemplate.getForObject(SERVER_URI+"/rest/emp/1", Employee.class);
printEmpData(emp);
}
private static void testGetDummyEmployee() {
RestTemplate restTemplate = new RestTemplate();
Employee emp = restTemplate.getForObject(SERVER_URI+EmpRestURIConstants.DUMMY_EMP, Employee.class);
printEmpData(emp);
}
public static void printEmpData(Employee emp){
System.out.println("ID="+emp.getId()+",Name="+emp.getName()+",CreatedDate="+emp.getCreatedDate());
}
}
Phần lớn code ở trên đều dễ hiểu. Tuy nhiên, khi gọi phương thức REST để trả về một Collection, chúng ta cần sử dụng LinkedHashMap. Lý do là vì quá trình chuyển đổi JSON sang đối tượng ở trên không biết trước kiểu Employee object cụ thể, nên nó sẽ chuyển đổi thành một Collection chứa các LinkedHashMap.
Chúng ta có thể viết một phương thức để chuyển đổi từ LinkedHashMap sang đối tượng Java Bean mong muốn. Khi chạy chương trình trên, chúng ta sẽ nhận được kết quả sau trong console:
ID=9999,Name=Dummy,CreatedDate=Tue Mar 04 21:02:41 PST 2014
*****
ID=1,Name=Pankaj Kumar,CreatedDate=Tue Mar 04 21:02:41 PST 2014
*****
ID=1,Name=Pankaj Kumar,CreatedDate=Tue Mar 04 21:02:41 PST 2014
*****
2
ID=1,Name=Pankaj Kumar,CreatedDate=1393995761654
ID=9999,Name=Dummy,CreatedDate=1393995761381
Một điểm khác cần lưu ý là các phương thức PUT của RestTemplate thường không có tùy chọn để nhận đối tượng response. Lý do là phương thức PUT được dùng để lưu trữ dữ liệu trên server, và thường chỉ cần trả về mã trạng thái HTTP 200 là đủ.
Tổng kết
Tải project dịch vụ web RESTful với Spring ở trên bằng link này.
Do có nhiều yêu cầu cung cấp ví dụ tương tự hỗ trợ cả XML và JSON, chúng tôi đã mở rộng ứng dụng này từ bài viết Spring REST XML JSON để hỗ trợ cả request và response dạng XML lẫn JSON. Bạn nên đọc qua bài đó để thấy được sự linh hoạt của Spring framework và cách đạt được điều đó dễ như thế nào.
Bạn có thể tải về project hoàn chỉnh từ repo GitHub này.