Spring进阶之@ControllerAdvice与统一异常处理

14,107 阅读3分钟

@ControllerAdvice

Spring源码中有关@ControllerAdvice的注解如下:

Specialization of {@link Component @Component} for classes that declare {@link ExceptionHandler @ExceptionHandler}, {@link InitBinder @InitBinder}, or {@link ModelAttribute @ModelAttribute} methods to be shared across multiple {@code @Controller} classes.

理解:

@ControllerAdvice是一个特殊的@Component,用于标识一个类,这个类中被以下三种注解标识的方法:@ExceptionHandler@InitBinder@ModelAttribute,将作用于所有的@Controller类的接口上。

那么,这个三个注解分别是什么意思,起到什么作用呢?


@InitBinder

Annotation that identifies methods which initialize the {@link org.springframework.web.bind.WebDataBinder} which will be used for populating command and form object arguments of annotated handler methods. Such init-binder methods support all arguments that {@link RequestMapping} supports, except for command/form objects and corresponding validation result objects. Init-binder methods must not have a return value; they are usually declared as {@code void}.

作用:注册属性编辑器,对HTTP请求参数进行处理,再绑定到对应的接口,比如格式化的时间转换等。应用于单个@Controller类的方法上时,仅对该类里的接口有效。与@ControllerAdvice组合使用可全局生效。

示例:

@ControllerAdvice
public class ActionAdvice {
    
    @InitBinder
    public void handleException(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
    }
}

@ExceptionHandler

作用:统一异常处理,也可以指定要处理的异常类型

示例:

@ControllerAdvice
public class ActionAdvice {
    
    @ExceptionHandler(Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public Map handleException(Exception ex) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", 400);
        map.put("msg", ex.toString());
        return map;
    }
}

@ModelAttribute

作用:绑定数据

示例:

@ControllerAdvice
public class ActionAdvice {
    
    @ModelAttribute
    public void handleException(Model model) {
        model.addAttribute("user", "zfh");
    }
}

在接口中获取前面绑定的参数:

@RestController
public class BasicController {
    
    @GetMapping(value = "index")
    public Map index(@ModelAttribute("user") String user) {
        //...
    }
}

完整示例代码:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * 统一异常处理
 * @author zfh
 * @version 1.0
 * @since 2019/1/4 15:23
 */
@ControllerAdvice
public class ControllerExceptionHandler {

    private Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);

    @InitBinder
    public void initMyBinder(WebDataBinder binder) {
        // 添加对日期的统一处理
        //binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));

        // 添加表单验证
        //binder.addValidators();
    }

    @ModelAttribute
    public void addMyAttribute(Model model) {
        model.addAttribute("user", "zfh"); // 在@RequestMapping的接口中使用@ModelAttribute("name") Object name获取
    }

    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody // 如果使用了@RestControllerAdvice,这里就不需要@ResponseBody了
    public Map handler(Exception ex) {
        logger.error("统一异常处理", ex);
        Map<String, Object> map = new HashMap<>();
        map.put("code", 400);
        map.put("msg", ex);
        return map;
    }
}

测试接口:

@RestController
public class TestAction {

    @GetMapping(value = "testAdvice")
    public JsonResult testAdvice(@ModelAttribute("user") String user, Date date) throws Exception {
        System.out.println("user: " + user);
        System.out.println("date: " + date);
        throw new Exception("直接抛出异常");
    }
}

高阶应用--格式化时间转Date

使用@ControllerAdvice + @InitBinder,可将http请求参数中的时间自动转换成Date类型。

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        GenericConversionService genericConversionService = (GenericConversionService) binder.getConversionService();
        if (genericConversionService != null) {
            genericConversionService.addConverter(new DateConverter());
        }
    }

自定义的时间类型转换器:

import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 日期转换类
 * 将标准日期、标准日期时间、时间戳转换成Date类型
 */
public class DateConverter implements Converter<String, Date> {
    private static final String dateFormat = "yyyy-MM-dd HH:mm:ss";
    private static final String shortDateFormat = "yyyy-MM-dd";
    private static final String timeStampFormat = "^\\d+$";

    @Override
    public Date convert(String value) {

        if(StringUtils.isEmpty(value)) {
            return null;
        }

        value = value.trim();

        try {
            if (value.contains("-")) {
                SimpleDateFormat formatter;
                if (value.contains(":")) {
                    formatter = new SimpleDateFormat(dateFormat);
                } else {
                    formatter = new SimpleDateFormat(shortDateFormat);
                }
                return formatter.parse(value);
            } else if (value.matches(timeStampFormat)) {
                Long lDate = new Long(value);
                return new Date(lDate);
            }
        } catch (Exception e) {
            throw new RuntimeException(String.format("parser %s to Date fail", value));
        }
        throw new RuntimeException(String.format("parser %s to Date fail", value));
    }
}

扩展:

@RestControllerAdvice = @ControllerAdvice + @ResponseBody


参考:

  1. Spring Boot 系列(八)@ControllerAdvice 拦截异常并统一处理
  2. SpringMVC重要注解(二)@ControllerAdvice
  3. 精通Spring Boot —— 第十五篇:使用@ControllerAdvice处理异常
  4. @InitBinder
  5. SpringMVC中利用@InitBinder来对页面数据进行解析绑定