《Spring in Action》4th Edition 学习笔记
Spring MVC 能处理从 请求 - 处理 - 返回 的所有流程,来看看它是如何工作的。
请求生命周期
-
front controller也就是
DispatcherServlet
接受到请求 -
DispatcherServlet
根据请求url映射到对应的 controller -
DispatcherServlet
发送请求到对应的 controller - controller 处理请求,把需要返回的数据放入 model 中,然后指定 view name,把包含这些数据的 request 发送回
DispatcherServlet
-
DispatcherServlet
生成一个 view resolver 处理逻辑视图名称 - request 到达 view implementation
- 现在,view implementation 将使用 request 传入的 modal data 去渲染视图(request 的工作完成了),然后将视图写入 response object,返回给 client 端
配置 Spring MVC
使用 Servlet 3 规范,可以使用 java
来配置 servlet,而不仅仅是 xml 文件。这里主要介绍如何使用 java
配置 web 应用和 spring MVC。
package spittr.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import spittr.web.WebConfig;
public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
Servlet 3.0 规范和 Spring DispatcherServlet 配置
在 Servlet 3.0 的环境中,容器会在 classpath 中寻找继承了javax.servlet.ServletContainerInitializer
接口的类,用它来配置 servlet 容器。
Spring 提供了一个继承这个接口的类SpringServletContainerInitializer
,在这个类中,它会寻找任何继承了WebApplicationInitializer
接口的类并用其来配置 servlet 容器。Spring 3.2 提供了一个继承了WebApplicationInitializer
接口的基类AbstractAnnotationConfigDispatcherServletInitializer
。所以,你的 servlet 配置类只需要继承AbstractAnnotation-ConfigDispatcherServletInitializer
,就会被发现而用于 servlet 容器的配置。
DispatcherServlet
VS ContextLoaderListener
在 Spring MVC 中存在两种应用上下文:DispatcherServlet
创建的和拦截器 ContextLoaderListener
创建的上下文:
-
DispatcherServlet
:加载包含 web 组件的 bean,比如 controllers,view resolvers 和 hanlder mappings。 -
ContextLoaderListener
:加载其他 bean,通常是一些中间层和数据层的组件(比如数据库配置 bean 等)。
在 AbstractAnnotationConfigDispatcherServletInitializer
中 DispatcherServlet
和 ContextLoaderListener
都会被创建,而基类中的方法就可用来创建不同的应用上下文:
-
getServletConfigClasses()
:定义DispatcherServlet
应用上下文中的 beans -
getRootConfigClasses()
:定义拦截器ContextLoaderListener
应用上下文中的 beans
Note:为了使用 AbstractAnnotationConfigDispatcherServletInitializer
必须保证 web 服务器支持 Servlet 3.0 标准(如 tomcat 7 或更高版本) 。
自定义 DispatcherServlet 配置
因为我们使用 AbstractAnnotationConfigDispatcherServletInitializer
来配置 DispatcherServlet
,所以可以通过 customizeRegistration()
方法来自定义 DispatcherServlet
。原文如下:
One such method is
customizeRegistration()
. AfterAbstractAnnotationConfigDispatcherServletInitializer
registersDispatcherServlet
with the servlet
container, it calls thecustomizeRegistration()
method, passing in theServletRegistration.Dynamic
that resulted from the servlet registration. By overridingcustomizeRegistration()
, you can apply additional configuration to DispatcherServlet.
通过 ServletRegistration.Dynamic
参数配置 DispatcherServlet
的 load-on-startup 优先级 setLoadOnStartup(int loadOnStartup)
,设置初始化参数 setInitParameters()
等。具体查看文档 ServletRegistration.Dynamic。
配置额外的 servlets 和 filters
使用 java 配置 servlet 的一个好处(不同于 web.xml)就是:可以定义任意数量的初始化类。所以,如果需要定义额外的 servlets 或 filters,只需要创建额外的初始化类。在 Spring MVC 中可以通过继承 WebApplicationInitializer
接口来实现。
接下来,我们定义一个新的 servlet:
package com.myapp.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import com.myapp.MyServlet;
public class MyServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext throws ServletException {
Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
myServlet.addMapping("/custom/**");
}
}
当然,你也可以用来定义 filters 和 listeners:
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
// 定义filter
javax.servlet.FilterRegistration.Dynamic filter =
servletContext.addFilter("myFilter", MyFilter.class);
filter.addMappingForUrlPatterns(null, false, "/custom/*");
}
如果你需要为 DispatcherServlet
添加 filter 的话,就不用这么麻烦了,你只要重写 AbstractAnnotationConfigDispatcherServletInitializer
类的 getServletFilters()
方法就行了:
@Override
protected Filter[] getServletFilters() {
return new Filter[] { new MyFilter() };
}
不需要 mapping,因为会自动 mapping 到 DispatcherServlet
上,通过返回多个 filter,可以添加多个 filter。
开启 Spring MVC 支持
Spring 使用如下方法开启 MVC 的支持:
-
@EnableWebMvc
注解(JavaConfig):和@Configuration
注解一起使用 -
<mvc:annotation-driven />
元素(XML 配置)
开启 MVC 支持,它会从 WebMvcConfigurationSupport
导入 Spring MVC 的配置,会在处理请求时加入注解的支持(比如 @RequestMapping
,@ExceptionHandler
等注解)。
如果需要自定义配置,从 @EnableWebMvc
的文档上来看,需要继承 @WebMvcConfigurer
接口或者继承基类 WebMvcConfigurerAdapter
(它继承了 @WebMvcConfigurer
接口,但是用空方法实现)。所以,覆盖相应的方法就能实现 mvc 配置的自定义。
那么,我们需要在 web mvc 配置中做哪些事情呢:
- 开启 ComponentScan
- View Resolver(视图解析)
- 静态文件处理
View Resolver 将在后面介绍,这里先讨论如何处理静态文件(html, css, js)
静态文件处理
Spring 可以有两种方式处理静态文件:
- 转发到默认的 web 服务器的 servlet 处理(比如 tomcat 来处理)
- 使用 Spring ResourceHandler 处理
使用这两种办法都需要继承 WebMvcConfigurerAdapter
基类,覆盖其中相应的方法实现。
默认 Servlet 处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
如此配置后,如果 Sping 遇到没有 mapping 的 url 地址,就会转发到默认的 Servlet 处理(如 tomcat)。这其中就包括静态文件(前提是你没有为静态文件设置 RequestMapping)。
Spring ResourceHandler
使用 Spring ResourceHandler 可以使用 Spring 来处理静态文件:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/resources/**")
.addResourceLocations("/resources/", "classpath:/resources/");
}
我们为 url 地址符合 /resource/**
的文件设置了指定的文件路径,spring 会按照配置的先后顺序在指定的路径中查找文件是否存在并返回。
Spring 4.1 提供了新的静态资源的特性 ResourceResolvers
和 ResourceTransformers
,具体用法请参考 Spring Framework 4.1 - handling static web resources。
如下为完整的 WebConfig
配置:
package spittr.web;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration
@EnableWebMvc
@ComponentScan("spittr.web")
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
// 开启默认转发
configurer.enable();
}
}
RootConfig
配置:
package spittr.config;
import java.util.regex.Pattern;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import spittr.config.RootConfig.WebPackage;
@Configuration
@ComponentScan(basePackages={"spittr"},
excludeFilters={
@Filter(type=FilterType.ANNOTATION, value=EnableWebMvc.class)
})
public class RootConfig {
}
配置很简单,因为还没有配置数据库等,所以只是开启了 ComponentScan,通过注解排除了 WebConfig
文件。
Spring 控制器
在 Spring MVC 中,控制器就是一个类,其中有很多被 @RequestMapping
注解的方法,标明它处理的请求类型。
package spittr.web;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HomeController {
@RequestMapping(value = "/", method = GET)
public String home(Model model) {
return "home";
}
}
@Controller
注解基于 @Component
注解,标明这是一个控制器,但是完全可以使用 @Component
注解,只是 @Controller
更明确。
@RequestMapping
的 value 值表示这个控制器处理的请求路径,而 methos
属性标明它能够处理的 HTTP 方法是 GET
方法。
在 home
方法中,参数 model
可用于给 ViewResolver 传递数据。Model
也可用 Map
代替。
home
方法返回的是一个字符串 home
,标明用于处理该视图的视图名称为 home
。可能是 jsp,也可能是 velocity 模板,取决于你使用的视图。前面我们说过,Spring MVC 最后都会有一个视图解析的过程,它始终需要解析到一个视图上,然后返回 html 页面给 client。所以,视图解析就可能给这个 home
视图名称加上前缀和后缀,然后找到他的位置,然后处理数据(也就是控制器传入的 Model
),然后把处理过后得到的页面返回给 client。
如果使用前面配置的 InternalResourceViewResolver
,那么 home
视图就会被解析到 /WEB-INF/views/home.jsp。然后在 jsp 中就可以访问 Model
中的数据。如果返回的不是字符串指定视图名,那么 Spring 会使用方法名称作为视图名称。
不过,你也可以把 @RequestMapping
注解加在类上,它会应用在所有的方法的 @RequestMapping
之上。
package spittr.web;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/home")
public class HomeController {
// 这会处理 /home/page 的GET请求
@RequestMapping(value="/page", method = GET)
public String home(Model model) {
return "home";
}
}
传递 Modal 数据给视图
通过给参数 Model
添加属性,可以给视图传递 key-value 组合的数据。所以容器既可以是 Model
类型,也可以是 Map
类型。
@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model) {
model.addAttribute("spittleList",
spittleRepository.findSpittles(Long.MAX_VALUE, 20));
return "spittles";
}
获取请求输入
Spring MVC 提供三种方式来获取 client 传输的数据:
- 查询参数(Query parameters)
- 表单参数(Form parameters)
- 路径变量(Path variables)
查询参数(Query parameters)
Spring MVC 中可以通过 @RequestParam
注解获取请求中的参数,还可以通过 defaultValue
属性设置默认值(只能使用 String
类型)。
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
@RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
@RequestParam(value="count", defaultValue="20") int count) {
return spittleRepository.findSpittles(max, count);
}
路径变量(Path variables)
如果需要通过 ID 查询一个资源,我们可以把 ID 放在请求参数的位置上(/spittles/show?spittle_id=12345),也可以放在路径变量的位置上(/spittles/12345)。对于一个资源来说,后一种方式要更好,前一种方式表明一个动作带有请求参数,后一种就代表我是请求一个 ID 为 12345 的资源,更明确也更简单。
为了写一个面向资源的控制器,需要使用 {
和 }
把路径变量括起来,这样 Spring 才能解析。然后,使用 @PathVariable
注解将路径变量的值赋值给一个变量,以便在控制器中使用。
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(
@PathVariable("spittleId") long spittleId,
Model model) {
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
在本例中,使用了 spittleId
作为 url 上的占位符,然后赋值给 spittleId
。如果省略 @PathVariable
注解的 value
属性,那么必须保证占位符和变量名称匹配,这样才能正确解析。
表单参数
如果请求参数包含一个 bean(比如整个表单的提交),那么可以使用 Spring 自动将请求参数组合成一个 Bean。
@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) throws Exception {
spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),
form.getLongitude(), form.getLatitude()));
return "redirect:/spittles";
}
在 saveSpittle
方法的参数上,有个 SpittleForm
类型的参数。Spring 会用请求参数中和 SpittleForm
中成员变量相同名称的参数的值来填充 from
变量。
本例中,返回了一个 redirect:
作为前缀的字符串,当 InternalResourceViewResolver
看到这个前缀是,将会执行 redirect
动作,而不是渲染视图。当然,如果要执行 forward
只需要把前缀修改为 forward:
就行了。
表单验证
Spring 支持 Java Validation API(又叫做 JSR-303),从 Spring 3.0 开始,Spring MVC 就支持 Java Validation API。不需要任何额外的配置,需要保证项目 classpath 有 Java Validation API 的实现(比如 Hibernate Validator)就行了。
Java Validation API 定义了一些注解,可用来限制 JavaBean 中属性的值,只需要将需要的注解放在属性上就行了。这些注解所在的包为 javax.validation.constraints
,如下列出常用的:
注解 | 描述 |
---|---|
@AssertFalse |
必须为 Boolean 类型且为 false
|
@AssertTure |
必须为 Boolean 类型且为 true
|
@DecimalMax |
数值必须小于或等于一个给定的 BigDecimalString 值 |
@DecimalMin |
必须为数字且小于或等于一个给定的 BigDecimalString 的值 |
@Digits |
必须为数字,且值必须为给定的数值 |
@Future |
值必须为一个未来的日期 |
@Max |
必须为数字,且值小于或等于给定的值 |
@Min |
必须为数字,且值大于或等于给定的值 |
@NotNull |
不能为 null
|
@Null |
必须为 null
|
@Past |
值必须为一个过去的日期 |
@Pattern |
值必须满足给定的正则表达式 |
@Size |
必须为 String ,集合或数组的一种,且长度需满足给定的范围 |
要验证某个 Bean,只需在成员变量上加上需要的注解,然后在控制器上使用 @Valid
注解。
package spittr.web;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class SpittleForm {
@NotNull
@Size(min=1, max=140)
private String message;
@Min(-180)
@Max(180)
private Double longitude;
@Min(-90)
@Max(90)
private Double latitude;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Double getLongitude() {
return longitude;
}
public void setLongitude(Double longitude) {
this.longitude = longitude;
}
public Double getLatitude() {
return latitude;
}
public void setLatitude(Double latitude) {
this.latitude = latitude;
}
}
控制器上在方法参数的 bean 上使用 @Valid
注解,Spring MVC 就会根据 bean 属性上的注解去验证 bean:
@RequestMapping(value="/register", method=POST)
public String processRegistration(@Valid Spitter spitter, Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
其中,方法参数中的 Errors
标明验证的结果(注意:Errors
必须紧跟在 @Valid
注解的需验证的 bean 后面)。
理解视图解析
在前面的例子中,我们看到控制器返回的都是一个逻辑视图的名称,然后把这个逻辑视图名称交给view resolver,然后返回渲染后的 html 页面给 client。
把请求处理逻辑和视图渲染解耦是 Spring MVC 重要的特性之一。通过在控制器和视图之间传递 model
数据,可以使代码分离,逻辑清晰,更利于维护等优点。
Spring MVC 定义了一个 ViewResolver
的接口:
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
ViewResolver
方法,当提供一个 viewName
和 locale
以后,返回一个 View
实体。View
是另外一个接口:
public interface View {
String getContentType();
void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception;
}
View
接口的作用就是利用 model 数据,还有 request 和 response 对象渲染视图内容,然后返回给 response。
当然你在实际使用的过程中不会接触到这些内部的接口,因为 Spring 提供了很多视图技术的支持:FreeMarkerViewResolver
, InternalResourceViewResolver
, VelocityViewResolver
等。具体使用时请参考具体的视图使用教程。
异常处理
在应用中抛出异常,最后还是需要写入到 response 中,Spring 提供如下方式将异常转化为 response:
- 特定的 Spring 异常自动映射为 HTTP 状态码
- 异常可映射为 HTTP 状态码通过在异常上使用
@ResponseStatus
注解 - 一个方法可用来处理异常通过在其上使用
@ExceptionHandler
注解
异常映射 HTTP 状态码
通过 @ResponseStatus
注解将异常和 HTTP 状态码对应:
package spittr.web;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value=HttpStatus.NOT_FOUND,
reason="Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {
}
现在只需要在控制器中抛出异常,就会被映射为指定的 HTTP 状态码:
@RequestMapping(value="/{spittleId}", method=RequestMethod.GET)
public String spittle(@PathVariable("spittleId") long spittleId,
Model model) {
Spittle spittle = spittleRepository.findOne(spittleId);
if (spittle == null) {
throw new SpittleNotFoundException();
}
model.addAttribute(spittle);
return "spittle";
}
使用 exception-handling 方法
映射异常到 HTTP 状态码的方式简单、高效,但是如果需要返回更多的信息就不行了(比如返回一个 view 视图)。所以,Spring 提供了 @ExceptionHandler
注解,可像处理请求那样处理异常。
@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
try {
spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),
form.getLongitude(), form.getLatitude()));
return "redirect:/spittles";
} catch (DuplicateSpittleException e) {
return "error/duplicate";
}
}
如上,我们在控制器中既包含了业务处理代码,也包含了异常处理代码。使用 @ExceptionHandler
就可以让你专注于业务逻辑代码,而在另外的方法中专门处理异常。
@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle() {
return "error/duplicate";
}
注意,异常处理的方法必须放在会抛出该异常的控制器类中才行,也就是说只能捕获当前控制器类抛出的指定的异常。该异常处理返回了一个逻辑视图的名称,Spring 会根据这个名称返回相应的 html 页面。通过 @ExceptionHandler
注解,我们可以定义一个方法处理所有该控制器中任意 handler 抛出的 DuplicateSpittleException
异常,简化了代码。
// 新的业务逻辑 handler
@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
spittleRepository.save(
new Spittle(null, form.getMessage(), new Date(),
form.getLongitude(), form.getLatitude()));
return "redirect:/spittles";
}
但是,需要为每个控制器都定义一个 @ExceptionHandler
方法是不是有点重复。所以,一般情况下会把 exception-handler 方法放在 BaseController,或者使用 @ControllerAdvice
。
Advising controllers
为了使某些方法(如 exception-handler 方法)在全部的控制器中都能够发挥作用,Spring 3.2 引入了 controller advice。Controller advice 是一个被 @ControllerAdvice
注解的类,它包含一个或多个如下类型的方法:
- 被
@ExceptionHandler
注解的方法 - 被
@InitBinder
注解的方法 - 被
@ModelAttribute
注解的方法
在被 @ControllerAdvice
注解的类中的这些方法能够应用到所有被 @RequestMapping
注解的方法。
因为,@ControllerAdvice
本身被 @Component
注解,所以能够被 component-scan 扫描被注入,就像 @Controller
注解一样。
如下,就是使用 @ControllerAdvice
为所有的控制器定义 @ExceptionHandler
异常处理方法。
package spitter.web;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class AppWideExceptionHandler {
@ExceptionHandler(DuplicateSpittleException.class)
public String duplicateSpittleHandler() {
return "error/duplicate";
}
}
现在,然后被 @RequestMapping
注解的控制器方法如果抛出 DuplicateSpittleException
异常,都可以被该方法捕获到,从而进行处理。
面向资源的控制器
因为 Javascript 在客户端的大量使用,现在服务器端很多时候需要传回 XML 或 JSON 格式的数据,那么 Spring MVC 如何返回不同类型的数据,而不是返回 html 页面呢?
- Content negotiation:一种把 model 数据渲染成客户端需要的格式的视图
- Message conversion:能够把控制器返回的对象转换为 client 需要的格式的转换器,没有了视图渲染的环节
因为,Content negotiation 只能转换 model 数据,而 model 本质上来说是 map 类型的数据格式。所以,转换后的数据可能并不是理想的 client 需要的格式。出于这个原因,我们倾向于使用 Message conversion。
使用 HTTP message converters
Spring MVC 自带多种 message converters:Jaxb2RootElementHttpMessageConverter
, MappingJacksonHttpMessageConverter
, MappingJackson2HttpMessageConverter
, ResourceHttpMessageConverter
, 其他的 converters 请参考官方文档 HTTP Message Conversion。
那么如何使用这些 converters:
- 向 client 发送数据:根据 request’s
Accept
header 确定 - 从 client 接收数据:根据
Content-Type
header 确定
很多 converter 都是默认注册的,所以你不需要格外的配置,但是可能需要添加额外的依赖到项目的 classpath 中以便使用这些 converters。比如,如果需要使用 MappingJacksonHttpMessageConverter
在 JSON messages 和 Java 对象之间相互转换,你需要添加 Jackson JSON Processor 依赖到 classpath 中。自定义 Jackson converter 参考 Latest Jackson integration improvements in Spring。
自定义 Message converters
除了使用 Spring MVC 默认提供的 message converters 外,我们还可以自定义 converter。比如自定义 MappingJackson2HttpMessageConverter
,让其可以处理其他的 media types。
@Configuration
@EnableWebMvc
@ComponentScan("org.acherie.demo.web")
public class WebConfig extends WebMvcConfigurerAdapter {
...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jacksonConverter());
}
@Bean
public MappingJackson2HttpMessageConverter jacksonConverter() {
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.APPLICATION_XML);
MappingJackson2HttpMessageConverter converter =
new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(mediaTypes);
return converter;
}
}
@ResponseBody
返回资源
如果你需要返回 JSON 或 XML 到 client,你需要告诉 Spring 略过通常的 model/view 流程,并且使用 message converter。而这个东西就是 @ResponseBody
注解:
@RequestMapping(method=RequestMethod.GET, produces="application/json")
public @ResponseBody List<Spittle> spittles(
@RequestParam(value="max", defaultValue=MAX_LONG_AS_STRING) long max,
@RequestParam(value="count", defaultValue="20") int count) {
return spittleRepository.findSpittles(max, count);
}
@ResponseBody
告诉 Spring 你想将控制器返回的 Java object 转换(使用 converter)为资源返回 client。更近一步,DispatcherServlet
会通过请求的 Accept
header 确定 client 想要的格式(比如 JSON),然后寻找合适的 message converter 去做转换。
@RequestBody
接收 client 的资源
使用 @RequestBody
告诉 Spring 根据 Content-Type
header 确定合适的 message converter 转换资源(比如 JSON,XML)为 Java Objects。
@RequestMapping(method=RequestMethod.POST, consumes="application/json")
@ResponseStatus(HttpStatus.CREATED)
public ResponseEntity<Spittle> saveSpittle(@RequestBody Spittle spittle, UriComponentsBuilder ucb) {
Spittle saved = spittleRepository.save(spittle);
HttpHeaders headers = new HttpHeaders();
URI locationUri = ucb.path("/spittles/")
.path(String.valueOf(saved.getId()))
.build()
.toUri();
headers.setLocation(locationUri);
ResponseEntity<Spittle> responseEntity = new ResponseEntity<Spittle>(saved, headers, HttpStatus.CREATED);
return responseEntity;
}
错误状态返回
当控制器出现错误时,我们可能需要返回不同的 HTTP 状态码。在 Spring MVC 中有如下两种方式可以选择:
ResponseEntity
@ResponseStatus
使用 ResponseEntity
可以不用使用 @ResponseBody
,Spring MVC 知道会使用 message converter 去转换。
我们首先定义 Error
class:
public class Error {
private int code;
private String message;
public Error(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
然后使用 ResponseEntity
返回对于信息:
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<?> spittleById(@PathVariable long id) {
Spittle spittle = spittleRepository.findOne(id);
if (spittle == null) {
Error error = new Error(4, "Spittle [" + id + "] not found");
return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
}
而如果要使用 @ResponseStatus
的话,我们需要使用把错误处理 的代码提出来放在其他地方,因为一个方法只能有一个 @ResponseStatus
注解。我们使用 @ExceptionHandler
注解来统一处理控制器中抛出的异常,@ExceptionHandler
需使用在控制器方法上:
@ExceptionHandler(SpittleNotFoundException.class)
public ResponseEntity<Error> spittleNotFound(SpittleNotFoundException e) {
long spittleId = e.getSpittleId();
Error error = new Error(4, "Spittle [" + spittleId + "] not found");
return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
}
// SpittleNotFoundException类
public class SpittleNotFoundException extends RuntimeException {
private long spittleId;
public SpittleNotFoundException(long spittleId) {
this.spittleId = spittleId;
}
public long getSpittleId() {
return spittleId;
}
}
这样所有控制器抛出的 SpittleNotFoundException
异常都会被该处理器处理,看到我们还是使用的 ResponseEntity
返回的 404 状态码。现在原来的控制器就变得简单了:
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<Spittle> spittleById(@PathVariable long id) {
Spittle spittle = spittleRepository.findOne(id);
if (spittle == null) { throw new SpittleNotFoundException(id); }
return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
}
接下来,我们使用 @ResponseStatus
注解:
@RequestMapping(value="/{id}", method=RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
public @ResponseBody Spittle spittleById(@PathVariable long id) {
Spittle spittle = spittleRepository.findOne(id);
if (spittle == null) { throw new SpittleNotFoundException(id); }
return spittle;
}
其中,因为控制器默认会返回 200(OK),所以这里的 @ResponseStatus
注解是可以省略的。
我们也可以对异常处理方法做同样的简化:
@ExceptionHandler(SpittleNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public @ResponseBody Error spittleNotFound(SpittleNotFoundException e) {
long spittleId = e.getSpittleId();
return new Error(4, "Spittle [" + spittleId + "] not found");
}
这样就能够返回 404(NOT_FOUND)状态码,而不需要使用 @ResponseEntity
了。