目录
1. Exception自定义基类
2. 异常捕获的Controller
3. 原理剖析
3.1 @RestControllerAdvice + @ControllerAdvice
3.2 @RestController 与 @Controller的区别
3.3 @ResponseBody 注解的意思
3.4 @ControllerAdvice的其他用法
3.4.1 @InitBinder
3.4.2 @ModelAttribute
3.5 HttpMessageConverter / InternalResourceViewResolver
自定义的异常需要继承RuntimeException类、
然后需要去重写:
public RuntimeException(String message) {
super(message);
}
那么一个简单的自定义业务异常代码如下:
public class ResultException extends RuntimeException {
public ResultException(RespCode respCode, String result){
super(FastJsonUtils.toJsonString(new RespResult(respCode, result)));
}
public ResultException(int code, String msg, String result){
super(FastJsonUtils.toJsonString(new RespResult(code, msg, result)));
}
}
其实就是定义一个重写RuntimeException 的返回字符串方法,而且因为前端好多时候需要Json的字符串。这样返回前端还可以接受。
对于转json的Utils的code:
public class FastJsonUtils {
public static final String toJsonString(Object object){
if(null == object){
return null;
}
return JSON.toJSONString(object,
SerializerFeature.PrettyFormat,
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteNullNumberAsZero,
SerializerFeature.WriteNullBooleanAsFalse,
SerializerFeature.WriteNullListAsEmpty);
}
}
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;
@Slf4j
@RestControllerAdvice
public class ExceptionController {
/*算数异常类:ArithmeticException
空指针异常类型:NullPointerException
类型强制转换类型:ClassCastException
数组负下标异常:NegativeArrayException
数组下标越界异常:ArrayIndexOutOfBoundsException
违背安全原则异常:SecturityException
文件已结束异常:EOFException
文件未找到异常:FileNotFoundException
字符串转换为数字异常:NumberFormatException
操作数据库异常:SQLException
输入输出异常:IOException
方法未找到异常:NoSuchMethodException
下标越界异常:IndexOutOfBoundsExecption
系统异常:SystemException
创建一个大小为负数的数组错误异常:NegativeArraySizeException
数据格式异常:NumberFormatException
安全异常:SecurityException
不支持的操作异常:UnsupportedOperationException
网络操作在主线程异常:NetworkOnMainThreadException
请求状态异常: IllegalStateException (extends RuntimeException , 父类:IllegalComponentStateException 在不合理或不正确时间内唤醒一方法时出现的异常信息。换句话说,即 Java 环境或 Java 应用不满足请求操作)
网络请求异常:HttpHostConnectException
子线程Thread更新UI view 异常:ViewRootImpl$CalledFromWrongThreadException
证书不匹配的主机名异常: SSLExceptionero
反射Method.invoke(obj, args...)方法抛出异常:InvocationTargetException
EventBus使用异常:EventBusException
非法参数异常:IllegalArgumentException
参数不能小于0异常:ZeroException*/
@ExceptionHandler(value = {ResultException.class})
@ResponseBody
public String resultException(HttpServletRequest req, Exception e) {
return e.getMessage();
}
@ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class, ClassCastException.class,
ArrayIndexOutOfBoundsException.class, FileNotFoundException.class, NumberFormatException.class,
SQLException.class, IOException.class, RuntimeException.class})
@ResponseBody
public RespResult defaultErrorHandler(HttpServletRequest req, Exception e) {
RespResult respResult = new RespResult(RespCode.UNKNOWN.getCode(), RespCode.UNKNOWN.getMsg(), null);
log.error("[ExceptionController] 异常 xxxRequestId = {}; message = {}; 响应接口 = {}", req.getAttribute("xxxRequestId"),
e.getMessage(), JSON.toJSONString(respResult));
return respResult;
}
// http状态码 = 400
@ExceptionHandler(value = {MissingRequestHeaderException.class})
@ResponseBody
public RespResult missingRequestHeaderException(HttpServletRequest req, Exception e) {
RespResult respResult = new RespResult(RespCode.ERROR_2_.getCode(), RespCode.ERROR_2_.getMsg(), null);
log.error("[ExceptionController] 异常 xxxRequestId = {}; message = {}; 响应接口 = {}", req.getAttribute("xxxRequestId"),
e.getMessage(), JSON.toJSONString(respResult));
return respResult;
}
// http状态码 = 415
@ExceptionHandler(value = {HttpMediaTypeNotSupportedException.class})
@ResponseBody
public RespResult httpMediaTypeNotSupportedException(HttpServletRequest req, Exception e) {
RespResult respResult = new RespResult(RespCode.ERROR_3_.getCode(), RespCode.ERROR_3_.getMsg(), null);
log.error("[ExceptionController] 异常 xxxRequestId = {}; message = {}; 响应接口 = {}", req.getAttribute("xxxRequestId"),
e.getMessage(), JSON.toJSONString(respResult));
return respResult;
}
@ExceptionHandler(value = {ConstraintViolationException.class})
@ResponseBody
public RespResult constraintViolationException(HttpServletRequest req, Exception e) {
return new RespResult(RespCode.ERROR_4_, null);
}
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
@ResponseBody
public RespResult methodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {
return new RespResult(RespCode.ERROR_4_.getCode(), e.getBindingResult().getFieldError().getDefaultMessage(),null);
}
@ExceptionHandler(value = {Exception.class})
@ResponseBody
public RespResult defaultErrorHandler1(HttpServletRequest req, Exception e) {
log.error("[ExceptionController] 异常 uuid = {}; message = {}", req.getAttribute("uuid"), e.getMessage());
return new RespResult(RespCode.UNKNOWN.getCode(), RespCode.UNKNOWN.getMsg(), null);
}
}
自定义的返回基类:就是个简单的POJO
public class RespResult {
private int code;
private String msg;
private T result;
public RespResult() {
}
public RespResult(int code, String msg, T result) {
this.code = code;
this.msg = msg;
this.result = result;
}
public RespResult(RespCode respCode, T result) {
this.code = respCode.getCode();
this.msg = respCode.getMsg();
this.result = result;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
}
因为异常类中加入了这个注解,所以所有通过Controller进去的请求发生了异常都会被捕获。
首先是@RestControllerAdvice 源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.web.bind.annotation;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class>[] basePackageClasses() default {};
Class>[] assignableTypes() default {};
Class extends Annotation>[] annotations() default {};
}
这里面的核心是@ControllerAdvice
@ControllerAdvice源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.web.bind.annotation;
import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class>[] basePackageClasses() default {};
Class>[] assignableTypes() default {};
Class extends Annotation>[] annotations() default {};
}
在Spring 3.2中,新增了@ControllerAdvice、@RestControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping、@PostMapping, @GetMapping注解中。
所以在我的业务使用中,使用 @ControllerAdvice + @ExceptionHandler 来处理异常。
其实这个注解应该说是Spring的注解,所以不管是MVC还是spring boot 都可以来进行使用这个注解来实现对Exception的Handler
================================================================================================
但是这个注解远远不止这点用途。
@ControllerAdvice是在类上声明的注解,其用法主要有三点:
结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的;
结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的;
结合方法型注解@ModelAttribute,表示其标注的方法将会在目标Controller方法执行之前执行。
@ControllerAdvice的用法基本是将其声明在某个bean上,然后在该bean的方法上使用其他的注解来指定不同的织入逻辑。不过这里@ControllerAdvice并不是使用AOP的方式来织入业务逻辑的,而是Spring内置对其各个逻辑的织入方式进行了内置支持。本文将对@ControllerAdvice的这三种使用方式分别进行讲解。
如下是我们使用@ExceptionHandler捕获RuntimeException异常的例子:
@ControllerAdvice(basePackages = "mvc")
public class SpringControllerAdvice {
@ExceptionHandler(RuntimeException.class)
public ModelAndView runtimeException(RuntimeException e) {
e.printStackTrace();
return new ModelAndView("error");
}
}
那么这个和@RestControllerAdvice有什么区别,从源码中可以看出来其实@RestControllerAdvice这个是被@ControllerAdvice修饰的。但是@RestController的时候还会被@ResponseBody修饰。
注解@ControllerAdvice的类可以拥有@ExceptionHandler, @InitBinder或 @ModelAttribute注解的方法,并且这些方法会被应用到控制器类层次的所有@RequestMapping方法上。
@RestControllerAdvice 类似于 @RestController 与 @Controller的区别
@RestController注解相当于@ResponseBody + @Controller合在一起的作用。
@RestControllerAdvice这个是被@ControllerAdvice修饰的。但是@RestController的时候还会被@ResponseBody修饰。
都表示这个类可以接受Http请求,
不同:@Controller标识一个Spring类是Spring MVC controller处理器
@RestController是@Controller和@ResponseBody的结合体,两个标注合并起来的作用。
所以以往都是这样用:
@Controller
@ResponseBody
public class MyController { }
@RestController
public class MyController { }
所以看一下这个注解到底是什么意思呢?
可以简单的理解为这个注解表名Controller是返回的数据,而不是找对应的页面URI地址~
返回数据是什么格式的,取决于你的request的header中规定是什么格式的
也就是Content-Type所指定的比如说application/json
如果只是使用@RestController注解Controller,则Controller中的方法无法返回jsp页面,或者html,配置的视图解析器 InternalResourceViewResolver不起作用,返回的内容就是Return 里的内容。(返回数据啦)
如果需要返回到指定页面,则需要用 @Controller配合视图解析器InternalResourceViewResolver才行。
所以该注解:通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。
PS:可以结合异常@ExceptionHandler,自定义返回得浏览器Code
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //自定义浏览器返回状态码
该注解的主要作用是绑定一些自定义的参数。一般情况下我们使用的参数通过@RequestParam,@RequestBody或者@ModelAttribute等注解就可以进行绑定了,但对于一些特殊类型参数,比如Date,它们的绑定Spring是没有提供直接的支持的,我们只能为其声明一个转换器,将request中字符串类型的参数通过转换器转换为Date类型的参数,从而供给@RequestMapping标注的方法使用。如下是@InitBinder的声明:
PS:貌似我都是用String来接收,然后自己去service中在处理Date = =
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InitBinder {
// 这里value参数用于指定需要绑定的参数名称,如果不指定,则会对所有的参数进行适配,
// 只有是其指定的类型的参数才会被转换
String[] value() default {};
}
如下是使用@InitBinder注册Date类型参数转换器的实现:
@ControllerAdvice(basePackages = "mvc")
public class SpringControllerAdvice {
@InitBinder
public void globalInitBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
}
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/detail", method = RequestMethod.GET)
public ModelAndView detail(@RequestParam("id") long id, Date date) {
System.out.println(date);
ModelAndView view = new ModelAndView("user");
User user = userService.detail(id);
view.addObject("user", user);
return view;
}
}
那么亲自试一试呗~
实验结果就是,如果想用date类型接受的话,是会抛出异常的!
异常如下:
JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2019-07-15 10:22:99": not a valid representation (error: Failed to parse Date value '2019-07-15 10:22:99': Cannot parse date "2019-07-15 10:22:99": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-07-15 10:22:99": not a valid representation (error: Failed to parse Date value '2019-07-15 10:22:99': Cannot parse date "2019-07-15 10:22:99": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))
那么加上这个@InitBinder来处理时间!
但是,经过测试,其实这个WebDataBinder 其实进来的都是Header中的内容+拼接的参数(不支持Json)
@Component
public class BaseController {
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
dataBinder.registerCustomEditor(
Date.class,
new PropertyEditorSupport() {
@Override
public void setAsText(String value) {
try {
setValue(new SimpleDateFormat("yyyy-MM-DD HH:mm:ss").parse(value));
} catch (ParseException e) {
setValue(null);
}
}
});
}
}
而且网传,加上这个在controller中就可以把用类来接受的参数进行转换,但是测试之后并不能将body中的JSON入参进入这里面进行转换。
@InitBinder
public void initDateFormate(WebDataBinder dataBinder) {
dataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
}
代码3.4.1-code1
对于json的入参应该用
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")
就可以将String的json入参转成了Date,而且也可以用第自定义序列化和反序列工具来实现(可以自行百度)
但是到现在有点跑题,那么@InitBinder 到底可以对日期进行格式刷吗?
猜测是只能对RequestParam那种类型进行操作,实际在测试一下~
确实是这样,也就是只能对加上上面的代码3.4.1-code1 , 这个代码需要对每个controller都加上,而且只能对requestParam来使用,不能对JSON用
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
对于这个@InitBinder 就如它的名字,初始化数据绑定器。
会对所有的接口都先进入这个数据绑定器,但是这个数据绑定器并不能对json进行处理,只能对Header和RequestParam来进行处理!
/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
* @param binder
*/
@InitBinder
public void initWebBinder(WebDataBinder binder){
//对日期的统一处理
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
//添加对数据的校验
//binder.setValidator();
}
/**
* 把值绑定到Model中,使全局@RequestMapping可以获取到该值
* @param model
*/
@ModelAttribute
public void addAttribute(Model model) {
model.addAttribute("attribute", "The Attribute");
}
这个有点像 定义一个抽象的BaseCtrl 然后将里面塞入xxxRquestId 或者类似的访问属性,然后扔到线程池里面;
但是request中放入属性可能是进入接口之后才做的;至少我们的代码是这样;
比如我们定义的BaseCtrl,将代码简单的脱敏了~
@Slf4j
@Component
public abstract class BaseCtrl {
protected String setGlobalRequestId(HttpServletRequest request, String xxxRequestId) {
if (StringUtils.isBlank(xxxRequestId)) {
xxxRequestId= Tools.getUUID();
}
request.setAttribute("xxxRequestId", xxxRequestId);
return xxxRequestId;
}
}
看一下ModelAttribute
https://www.jianshu.com/p/cf9acf314a4c?utm_source=oschina-app
====之后补一下一次MVC的请求流程=====
参考:
https://blog.csdn.net/zxfryp909012366/article/details/82955259
https://yq.aliyun.com/articles/647428