项目地址:
https://github.com/FateSolo/JSH-Test
目录:
本章继续对JAX-RS予以补充。
1. 整合Logback日志
关于日志你有多种选择,这里我选择使用Logback来代替此前一直使用的Log4j,关于它们二者的对比可以参阅网上的其他资料。
1) 打开web.xml,添加如下配置:1
2
3
4
5
6
7
8
9<!-- Logback -->
<context-param>
<param-name>logbackConfigLocation</param-name>
<param-value>classpath:config/log/logback.xml</param-value>
</context-param>
<listener>
<listener-class>ch.qos.logback.ext.spring.web.LogbackConfigListener</listener-class>
</listener>
2) 添加Logback依赖:1
/**
* Logback
*/
compile 'ch.qos.logback:logback-classic:1.2.1'
compile 'org.logback-extensions:logback-ext-spring:0.1.4'
compile 'org.slf4j:jcl-over-slf4j:1.7.22'
三个依赖依次是Logback(包括Logback本身的实现与slf4j接口)、Logback对Spring的支持、Apache Commons Logging到slf4j的桥接器(因为Spring大量使用了Apache Commons Logging接口,所以需要重定向到slf4j接口)
3) 在resources/config/log目录下创建Logback日志的配置文件logback.xml:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<description>Logback 日志配置</description>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${JSH-Test.root}/WEB-INF/logs/%d{yyyy-MM}/%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>500</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
2. 使Jersey支持返回json
在前两章的示例中,我们使用@Consumes和@Produces限制了Content-Type和Accept的类型仅支持Application/xml格式,然后在实体类Book上增加了@XmlRootElement注解,此后Jersey框架会使用JAXB为我们进行xml数据与实体的绑定。
RESTful服务通常情况下会使用更为轻便简洁的json格式来传输数据,而Jersey没有默认支持json的转换,因此我们需要自行添加支持(这里选择了Gson做为实现),编写Provider类来完成手动的转换。
1) 添加Gson依赖:1
/**
* Druid
*/
compile 'com.alibaba:druid:1.0.28'
2) 建立provider子包,并创建GsonProvider类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67package com.fatesolo.jsh.provider;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
@Provider
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class GsonProvider implements MessageBodyWriter<Object>, MessageBodyReader<Object> {
private final Gson gson;
public GsonProvider() {
gson = new GsonBuilder().disableHtmlEscaping().create();
}
@Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
@Override
public Object readFrom(Class<Object> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException, WebApplicationException {
try (InputStreamReader reader = new InputStreamReader(entityStream, "UTF-8")) {
return gson.fromJson(reader, type.equals(genericType) ? type : genericType);
}
}
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return true;
}
@Override
public long getSize(Object object, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public void writeTo(Object object, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException {
try (OutputStreamWriter writer = new OutputStreamWriter(entityStream, "UTF-8")) {
gson.toJson(object, type.equals(genericType) ? type : genericType, writer);
}
}
}
3) 与资源类一样,若想被Jersey使用,需要在Application类中注册:1
register(GsonProvider.class);
4) 修改BookResource中的@Consumes和@Produces,增加对json格式的支持:1
2
3
4
5@Controller
@Path("/book")
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public class BookResource {
3. 项目运行:
1) 使用Tomcat 8运行项目,使用curl工具添加一条数据:1
curl -H "Content-Type:application/json" -H "Accept:application/json" -d "{\"name\":\"Book2\",\"author\":\"Solo\"}" http://localhost:8080/rest/book
得到如下返回:1
{"code":"SUCCESS"}
2) 使用curl工具获取一条数据:1
curl -H "Accept:application/json" http://localhost:8080/rest/book/2
得到如下结果:1
{"id":2,"name":"Book2","author":"Solo"}
3) 使用curl工具获取所有数据:1
curl -H "Accept:application/json" http://localhost:8080/rest/book
得到如下结果:1
[{"id":1,"name":"Book","author":"Fate"},{"id":2,"name":"Book2","author":"Solo"}]
至此,Jersey已经支持了对json格式数据的转换。
4. 关于SpringMVC的数据转换
可以看到,当一个方法直接返回实体或包装它的数据结构时,JAX-RS会依据Accept的类型自动完成转换来返回相对应的类型,同理也会依据Content-Type的类型来自动完成请求数据到实体对象的绑定。
在此前使用SpringMVC中,我基本都是直接返回String类型,然后使用Gson的toJson方法手动完成转换,事实上SpringMVC的数据转换同样非常方便:
1) 当我们在spring-mvc.xml增加了如下配置后,SpringMVC会自动注册相应的数据转换器,其中就包括了xml和json的转换器。1
<mvc:annotation-driven/>
2) 但与Jersey类似,SpringMVC直接使用JAXB完成对xml的转换,而json则需要手动添加实现,这里仍以Gson示例:1
compile 'com.google.code.gson:gson:2.7'
3) 在实体类上增加注解:1
@XmlRootElement
4) 在Controller类中,没有专门的@Consumes和@Produces注解,而是在@RequestMapping中完成配置:1
@RequestMapping(path = "/{id}", method = RequestMethod.GET, produces = "application/xml")
1 | @RequestMapping(path = "/name/{name}", method = RequestMethod.GET, produces = "application/json") |
produces代表了可以返回的类型,也可以使用{}返回多种类型。
1 | @RequestMapping(path = "", method = RequestMethod.POST, consumes = "application/json") |
consumes代表支持的请求类型,与produces的语法相同,另外需要在实体前增加@RequestBody注解,以支持json、xml等数据到实体的转换。
至此,当返回类型是实体类或是包装其的数据结构时,SpringMVC会依据Content-Type和Accept自动完成数据的转换,比Jersey方便的一点是,SpringMVC直接支持操作Gson,而不再需要我们手动编写GsonProvider。
5. 关于@Repository和@RepositoryDefinition
在DAO系列的Spring-Data-JPA一章中的BookDaoSpringDataJpaImpl接口中,同时使用了@Repository和@RepositoryDefinition注解,而在本系列的BookRepository接口中则只使用了@RepositoryDefinition注解。
事实上使用@RepositoryDefinition便已完成了bean的注册,无需再使用@Repository注解。
Mybatis框架里声明的接口同样不需要再使用@Repository注解,但在service层注入时,Intellij IDEA会报错(事实上可以完成注入)。
为了看起来统一,DAO系列的六种实现都添加了@Repository注解,而Mybatis和Spring-Data-JPA两种方法增加与否并没有什么太大区别(区别就是上面提到的IDE报错)。
作者 [@FateSolo]
2017 年 03月 22日