JAX-RS初探 三:其他


项目地址:
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
67
package 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
2
@RequestMapping(path = "", method = RequestMethod.POST, consumes = "application/json")
public Book addBook(@RequestBody Book book) {

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日