当使用HandlerInterceptor拦截器的postHandle()对controller的返回值进行拦截处理时,如果controller的方法被@ResponseBody修饰,则无法拦截controller方法的返回值,因为在postHandle()执行之前,controller方法的返回值就已经提交了。
详情见spring-framework SPR-9226
Response is committed before Interceptor postHandle invoked
In certain circumstances, the response is committed before the Interceptor postHandle method is invoked. This appears to be caused when the HandlerMethod is annotated with @ResponseBody.
Steps to reproduce:
1. Create a Controller method annotated with @ResponseBody.
2. Configure an Interceptor for this path (or all path mappings)
3. Submit a request to the path above, with a breakpoint set in the Interceptor's postHandle method.
4. Check the value of response.isCommitted()
Expected:
Response should not be committed
Actual:
Response is committed.
Implications:
Interceptors are unable to modify the response for @ResponseBody HandlerMethods. This prevents Interceptors from being able to add headers to the response.
@ControllerAdvice,是spring3.2提供的新注解。@ControllerAdvice注解内部使用@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到所有的 @RequestMapping注解的方法。
我们要做的就是创建一个用@ControllerAdvice注释的类,并创建相应的三个方法,这三个方法分别用@ExceptionHandler注释以进行全局异常处理,@InitBinder用于全局init绑定,而@ModelAttribute用于全局model属性添加。
当请求达到Controller类中带@RequestMapping注解的方法时,如果没有本地定义的@ExceptionHandler,@InitBinder和@ModelAttribute时,将使用由@ControllerAdvice注解标记的类中的相应方法。
默认情况下,在@ControllerAdvice的方法会应用到所有的Controller中,但是你可以使用@ControllerAdvice的basePackages属性来限制应用到特定包路径下的controller。
指定单个包路径:
@ControllerAdvice("org.my.pkg") 相当于@ControllerAdvice(basePackages="org.my.pkg")
指定一组包路径:
@ControllerAdvice(basePackages={"org.my.pkg","org.my.other.pkg"})
代码示例:
@ControllerAdvice(basePackages = {"com.concretepage.controller"} )
public class GlobalControllerAdvice {
@InitBinder
public void dataBinding(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, "dob", new CustomDateEditor(dateFormat, true));
}
@ModelAttribute
public void globalAttributes(Model model) {
model.addAttribute("msg", "Welcome to My World!");
}
@ExceptionHandler(FileNotFoundException.class)
public ModelAndView myError(Exception exception) {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", exception);
mav.setViewName("error");
return mav;
}
}
创建一个@RequestMapping方法的Controller,在该Controller我们也定义了一个本地的@InitBinder方法
@Controller
@RequestMapping("/myworld")
public class MyWorldController {
@Autowired
private UserValidator userValidator;
@RequestMapping(value="signup", method = RequestMethod.GET)
public ModelAndView user(){
return new ModelAndView("user","user",new User());
}
@InitBinder
public void dataBinding(WebDataBinder binder) {
binder.addValidators(userValidator);
}
@RequestMapping(value="save", method = RequestMethod.POST)
public String createUser(@ModelAttribute("user") @Valid User user,BindingResult result, ModelMap model)
throws FileNotFoundException {
if(result.hasErrors()) {
return "user";
}
if(user.getName().equals("exception")) {
throw new FileNotFoundException("Error found.");
}
System.out.println("Name:"+ user.getName());
System.out.println("Date of Birth:"+ user.getDob());
return "success";
}
}
上面Controller中抛出的FileNotFoundException异常没被处理,所以它会被@ControllerAdvice注解标记的类的异常处理方法处理。
@ControllerAdvice和ResponseBodyAdvice
先引用spring-framework 4.3官方文档的22.4节的这句话:
Intercepting requests with a HandlerInterceptor
Note that the postHandle method of HandlerInterceptor is not always ideally suited for use with @ResponseBody and ResponseEntity methods. In such cases an HttpMessageConverter writes to and commits the response before postHandle is called which makes it impossible to change the response, for example to add a header. Instead an application can implement ResponseBodyAdvice and either declare it as an @ControllerAdvice bean or configure it directly on RequestMappingHandlerAdapter.
ResponseBody接口官方API也有描述到:
public interface ResponseBodyAdvice
Allows customizing the response after the execution of an @ResponseBody or a ResponseEntity controller method but before the body is written with an HttpMessageConverter.
Implementations may be registered directly with RequestMappingHandlerAdapter and ExceptionHandlerExceptionResolver or more likely annotated with @ControllerAdvice in which case they will be auto-detected by both.
也就是说,通过实现ResponseBodyAdvice接口,则带@ResponseBody或者ResponseEntity的控制器方法可以在执行完成之后,response写入输出流(通过HttpMessageConverter)之前,完成对response的拦截。
使用示例如下:
package com.test.yhgl.framework.interceptor;
import java.util.HashMap;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice(basePackages="com.test.yhgl.controller")
public class RegisterReturnedInterceptor implements ResponseBodyAdvice{
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// TODO Auto-generated method stub
//获取当前处理请求的controller的方法
String methodName=returnType.getMethod().getName();
String method= "egisteAccount";
return method.equals(methodName); //只有当请求的方法为“egisteAccount”才进行拦截,返回true才能调用beforeBodyWrite()方法
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// TODO Auto-generated method stub
HashMap resp = (HashMap) body; //获取controller方法的返回值
boolean flag = (boolean) resp.get("flag");
if(flag){ //如果用户注册成功
}
return resp;
}
}
Controller如下:
package com.test.yhgl.controller;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping(value = "/regist")
public class RegistController {
private IUserService userService;
@RequestMapping(value = "/egisteAccount.do")
public @ResponseBody Map egisteAccount(HttpServletRequest request, HttpServletResponse response) throws IOException {
Map result = new HashMap();
boolean su = userService.registUser(request.getParameter("yhxx"));
if(su){
result.put("msg", "注册成功");
result.put("flag", true);
}else{
result.put("msg", "注册失败");
result.put("flag", false);
}
return result;
}
}
参考:spring 4.3 官方文档-HandlerInterceptor
@ControllerAdvice官方API
Spring MVC @ControllerAdvice Annotation Example
spring SPR-9226 Response is committed before Interceptor postHandle invoked
@ControllerAdvice,ResponseBodyAdvice 统一处理返回值/响应体
ResponseBodyAdvice官方API