在做项目的过程中,异常信息的处理总是无法避免的,不管多么完美的系统,总是会有异常情况出现的,当然,异常处理的好与坏本身也是对这个系统的一个评判标准,看一下在七月老师的做项目的过程中,是如何认识异常以及如何处理项目中的异常的,可以看做是相对标准的一个异常处理手段,以后在做项目中是可以进行借鉴的,总归只有一点,做出一个不那么差劲的系统,最起码是令别人看着系统的代码是整洁舒服的,系统用起来是弹性比较大的,就是一个特点,好用!
一、全局异常处理
1、统一捕获异常
做个全局的异常捕获机制,就是在异常抛出的时候,我们将异常信息进行拦截,转化成我们自己定义的异常信息的格式,这样方便前端的工程师来处理异常,那这个是如何做的呢,背后有什么原理呢?
(1)新建全局异常处理通知类
在创建的全局异常处理通知类中需要创建异常处理的方法,在Java中针对不同的异常,都需要做一个异常处理的方法,下面会继续深入探究Java中的异常分类,这里只是简单的做一个认识,如何来创建全局异常处理通知类
1 @ControllerAdvice 2 public class GlobalExceptionAdvice { 3 4 @ExceptionHandler(value = Exception.class) 5 public voidhandleException(HttpServletRequest req, Exception e){ 6 System.out.println("这里出错了!"); 7 } 8 }
这个只是一个简单的例子,当然后期开发中肯定会继续完善的,只是学会这种通知类的构建,具体的业务逻辑处理,稍后再完善,简单说明一下这个需要注意的点:
## @ControllerAdvice注解,这个注解是必要添加的,告诉spring容器,这是一个异常的通知类
## @ExceptionHandler(value = Exception.class) 注意这个注解中的value属性,这个Exception.class 说明这个是处理Exception异常的,抛出的Exception都会在这里进行处理
## handleException(HttpServletRequest req, Exception e) 这个方法的两个参数,注意第二个参数,这个是跟注解的属性中的Exception.class是对应的
## 当我们在controller中抛出异常的时候,会首先经过这里进行处理然后才会发送给前端页面中
2、Java中异常的分类
Java中的异常基类是Throwable,所有的异常都是继承自这个最基础的异常类的,在这个最基础的异常类之上是又有两种:
# Error (这个更严格来说,是错误,操作系统级别的错误或者是JVM虚拟机上的发生的错误,这个错误是比较致命的)
# Exception (这个才是异常,这个异常我们是可以通过代码进行处理的)
Exception再接下来拆分,是可以分为以下两种的
## CheckedException (必须要求我们在代码中进行处理)
## RuntimeException (运行时异常,并不是强制要求处理的)
注意:当我们自定义Exception的时候,加入extends Exception其实是checkedException,extends RuntimeException那么就是运行时异常,在web开发中,最好做一个全局异常处理机制,这样代码比较健壮
补充说明:异常的另一个分类角度是已知异常和未知异常,已知异常就是我们在代码中进行处理的异常,未知异常顾名思义,就是我们在写代码时候没有考虑到的异常
二、自定义异常
我们以http中异常处理来看一下自定义异常中需要有哪些我们值得注意的点
1、新建基础的HttpException类
我们让这个http自定义异常的基础类来实现RuntimeException,并且我们在该类中定义两个基础的属性,一个是我们自定义的错误码code,一个是http的状态码httpStatusCode
1 public class HttpException extends RuntimeException { 2 protected Integer code; 3 protected Integer httpStatusCode = 500; 4 }
2、创建子类来继承基类
## NotFoundException类(未找到资源异常类)
1 public class NotFoundException extends HttpException { 2 3 public NotFoundException(int code){ 4 this.httpStatusCode = 404; 5 this.code = code; 6 } 7 }
## ForbiddenException类(没有权限访问的异常类)
1 public class ForbiddenException extends HttpException { 2 3 public ForbiddenException(int code){ 4 this.code = code; 5 this.httpStatusCode = 403; 6 } 7 }
3、同时监听Exception和HttpException
如果我们在全局异常处理类中同时监听这两种异常,那么我们在出现异常的情况,会如何处理呢?如果我们指定HttpException异常,那么在监听Exception异常的方法中会监听到吗?
1 @ControllerAdvice 2 public class GlobalExceptionAdvice { 3 4 @ExceptionHandler(value = Exception.class) 5 public UnifyResponse handleException(HttpServletRequest req, Exception e){ 6 System.out.println("这里报错了!Exception!"); 7 } 8 9 @ExceptionHandler(value = HttpException.class) 10 public void handleHttpException(HttpServletRequest req, HttpException e){ 11 System.out.println("这里报错了!HttpException!"); 12 } 13 }
这里我们监听处理的就是两种异常,当我们 throw new NotFoundException(10001); 的时候,程序会执行全局异常中的监听的HttpException的方法,我么需要进一步处理监听Exception的方法。来达到向前端发送异常信息响应的统一回复格式,优雅的格式,信息明确,不拖泥带水。看一下我们返回信息的统一格式(简单demo,json格式):
1 { 2 code:10001, 3 message:xxxx, 4 request:GET url 5 }
4、定义统一格式UnifyResponse类
1 public class UnifyResponse { 2 private int code; 3 private String message; 4 private String request; 5 6 public UnifyResponse(int code, String message, String request) { 7 this.code = code; 8 this.message = message; 9 this.request = request; 10 } 11 12 public int getCode() { 13 return code; 14 } 15 16 public String getMessage() { 17 return message; 18 } 19 20 public String getRequest() { 21 return request; 22 } 23 }
5、完善全局异常处理类的方法
这个是存在挺多问题的,一点点的进行排查解决,重点看一下这个排查问题的方法,并且着重看一下这个问题是怎么解决的,先看第一版代码:(这个前提是我们在访问controller的接口的时候,直接抛出一个异常)
# 看一下访问接口的方法
1 @GetMapping("/test") 2 public String test() { 3 throw new RuntimeException(); 4 }
# 第一版的定义全局异常处理的方法代码
说明一下,为什么会写出这样的代码,因为我们想的是返回一个UniftyResponses实体信息类,来给页面端一个提示,所以在这里直接就写出这个代码,但是是存在问题的,当我们在浏览器访问上面那个接口地址的时候,会报错的
1 @ExceptionHandler(value = Exception.class) 2 public UnifyResponse handleException(HttpServletRequest req, Exception e){ 3 UnifyResponse message = new UnifyResponse(9999, "服务器异常", "url"); 4 return message; 5 }
具体的报错信息,大致是在浏览器中堆栈信息:
# 第二版 我们复原一下原来的代码(直接让其返回String类型的字符串,看结果)
1 @ExceptionHandler(value = Exception.class) 2 public String handleException(HttpServletRequest req, Exception e){ 3 UnifyResponse message = new UnifyResponse(9999, "服务器异常", "url"); 4 return "String"; 5 }
然而,结果还是原来的错误,我们进一步回想在之前的我们加上@RespouseBody之后是可以返回字符串的,就有了第三版代码
# 第三版 加上@ResponseBody注解
1 @ExceptionHandler(value = Exception.class) 2 @ResponseBody 3 public String handleException(HttpServletRequest req, Exception e){ 4 UnifyResponse message = new UnifyResponse(9999, "服务器异常", "url"); 5 return "String"; 6 }
这样的话,String类型的字符串是可以正确返回的,我们换做UnifyResponse对象试试
# 第四版 将返回结果String类型的字符串换做UnifyResponse对象
1 @ExceptionHandler(value = Exception.class) 2 @ResponseBody 3 public UnifyResponse handleException(HttpServletRequest req, Exception e){ 4 UnifyResponse message = new UnifyResponse(9999, "服务器异常", "url"); 5 return message; 6 }
说明:这里需要注意的是UnifyResponse这个对象的属性是私有的,我们需要对这些属性添加get方法,这样的话,浏览器页面才能准确的获取到返回结果!!!
6、继续改善全局异常处理类
主要是继续完善,http响应code码,这个在postman中测试,是不正确的,可以通过添加注解@ResponseStatus来实现这个功能,再一个就是完善返回信息的提示,具体的代码:
1 @ExceptionHandler(value = Exception.class) 2 @ResponseBody 3 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 4 public UnifyResponse handleException(HttpServletRequest req, Exception e){ 5 String requestUrl = req.getRequestURI(); 6 String method = req.getMethod(); 7 System.out.println(e); 8 UnifyResponse message = new UnifyResponse(9999, "服务器异常", method + " " +requestUrl); 9 return message; 10 }
这个未知异常的处理基本上就是完成了,接下来来处理HttpException异常方法
(1)整体的改造
1 @ExceptionHandler(value = HttpException.class) 2 public ResponseEntity<UnifyResponse> handleHttpException(HttpServletRequest req, HttpException e){ 3 String requestUrl = req.getRequestURI(); 4 String method = req.getMethod(); 5 UnifyResponse message = new UnifyResponse(e.getCode(), "xxxxxx", method + "" + requestUrl); 6 HttpHeaders headers = new HttpHeaders(); 7 headers.setContentType(MediaType.APPLICATION_JSON); 8 HttpStatus httpStatus = HttpStatus.resolve(e.getHttpStatusCode()); 9 10 ResponseEntity<UnifyResponse> r = new ResponseEntity<>(message, headers, httpStatus); 11 return r; 12 }
这里用了返回对象是ResponseEntity,至于为什么用这个对象,我想应该是更加规范一点吧,之前写springMVC的时候,在开发接口的时候,都是用这个作为返回对象的,这里又一次见到,真的感觉是很亲切的,这个改造还有需要改造的地方,就是message的提示,应该写到配置文件中,使得代码更加的健壮,好维护
(2)异常信息message写到配置文件
至于写到配置文件中的错误码对应的错误信息提示,是那种一一对应起来的,这个在现在的公司中的项目中也是那样处理的,如果做的是国际化处理的话,会分别创建多个语种的配置文件,这样方便代码提示信息的修改,在springboot中配置文件是可以和实体类很好的结合在一起的,这得归功于springboot中强大的注解,可以很好的利用注解来实现配置文件向实例类的转换
1 @ConfigurationProperties(prefix = "lin") 2 @PropertySource(value = "classpath:config/exception-code.properties") 3 @Component 4 public class ExceptionCodeConfiguration { 5 6 private Map<Integer, String> codes = new HashMap<>(); 7 8 public String getMessage(int code){ 9 String message = this.codes.get(code); 10 return message; 11 } 12 13 public Map<Integer, String> getCodes() { 14 return codes; 15 } 16 17 public void setCodes(Map<Integer, String> codes) { 18 this.codes = codes; 19 } 20 }
新建了一个properties文件,用来存放错误信息对应的键值对(举例说明一下,就是全是下面这种键值对,这也是为啥要在加上一个@ConfigurationProperties注解,添加上prefix属性):
1 lin.codes[10001] = 通用参数错误
注意这里还有一个问题没有解决,那就是中文乱码的问题!
7、补充内容
(1)springboot中自动发现机制
spring中的主动发现机制和思想,这个是简化了开发的,对于这个思想,我并不是很明白,没有其他语言框架的使用经验,所以没有对比,主要是springboot中的自动完成注册的功能,就是将controller类自动注册到application上下文中,省去了开发人员手动注册的功能,提高了开发人员的开发效率,并且同时还简化了代码,但是带来的缺点也存在,那就是开发人员看代码的时候不容易懂。
(2)自动生成路由前缀
这个是什么意思呢?主要就是针对controller类中的访问路径前缀的问题,就是在一些controller类中拥有共同的前缀,也可以说在同一个包下的controller类,我们自动获取它的前缀路径,举例子(我们自动获取这个"v1"):
1 @RestController 2 @RequestMapping(value = "/v1/banner") 3 public class BannerController { 4 5 }
## 首先新建一个类,继承RequestMappingHandlerMapping类,重写getMappingForMethod()方法
1 public class AutoPrefixUrlMapping extends RequestMappingHandlerMapping { 2 3 @Value("${missyou.api-package}") 4 private String apiPackagePath; 5 6 @Override 7 protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { 8 RequestMappingInfo mappingInfo = super.getMappingForMethod(method, handlerType); 9 if (mappingInfo != null) { 10 String prefix = this.getPrefix(handlerType); 11 RequestMappingInfo newMappingInfo = RequestMappingInfo.paths(prefix).build().combine(mappingInfo); 12 return newMappingInfo; 13 } 14 return mappingInfo; 15 } 16 17 private String getPrefix(Class<?> handlerType) { 18 String packageName = handlerType.getPackage().getName(); 19 String dotPath = packageName.replaceAll(this.apiPackagePath, ""); 20 return dotPath.replace(".", "/"); 21 } 22 }
注意:那个apiPackagePath是controller的根包名,这里是写在配置文件中的
1 #所有controller的根包名 2 missyou.api-package=com.lin.missyou.api
## 然后新建一个配置类,将这个重写的方法,让springboot在启动的时候进行读取到,注入到spring IOC容器中
1 @Component 2 public class AutoPrefixConfiguration implements WebMvcRegistrations { 3 4 @Override 5 public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { 6 return new AutoPrefixUrlMapping(); 7 } 8 }
这里是通过实现接口的形式来让springboot实现自动发现机制的,也就是通知springboot在启动的时候执行这个类中的方法,在做全局异常处理类的时候,利用注解是另一种实现方法,这里两种不同的实现思路
说明:这样的好处是我们不用管那个controller公共的那个路径了,我们只需要在@RequestMapping中说明这个controller功能的那个路径就行了,举个例子进行解释一下:
// 当前的controller类在com.lin.missyou.api.v1包下 // 改造前 @RestController @RequestMapping(value = "/v1/banner") public class BannerController { } // 改造后 @RestController @RequestMapping(value = "/banner") public class BannerController { }
总结:改造前后 我们的访问接口的路径是没有变化的,但是第二种更加简便了,代码更加灵活,可维护性更加强了,但是也相对第一种难以理解了
补充:解决乱码问题
这个问题是由于我们读取properties文件,这个文件默认的编码格式不是UTF-8,我们在IDEA中设置一下这个以.properties后缀名结尾的文件的编码格式,就能够解决这个问题了。Editor--->File Encoding中进行设置
内容出处:七月老师《从Java后端到全栈》视频课程