全栈之路-杂篇-Java异常深度剖析

  在做项目的过程中,异常信息的处理总是无法避免的,不管多么完美的系统,总是会有异常情况出现的,当然,异常处理的好与坏本身也是对这个系统的一个评判标准,看一下在七月老师的做项目的过程中,是如何认识异常以及如何处理项目中的异常的,可以看做是相对标准的一个异常处理手段,以后在做项目中是可以进行借鉴的,总归只有一点,做出一个不那么差劲的系统,最起码是令别人看着系统的代码是整洁舒服的,系统用起来是弹性比较大的,就是一个特点,好用!

一、全局异常处理

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后端到全栈》视频课程

七月老师课程链接:https://class.imooc.com/sale/javafullstack

posted @ 2020-02-22 21:37  ssc在路上  阅读(345)  评论(0编辑  收藏  举报