• 全栈之路-杂篇-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

  • 相关阅读:
    20145231第九周学习笔记
    20145231第八周学习笔记
    20145231《Java程序设计》第三次实验报告
    20145231第七周学习笔记
    20145231《Java程序设计》第二次实验报告
    测试「20200912测试总结」
    题解「Luogu4774 [NOI2018]屠龙勇士」
    总结「斯坦纳树」
    题解「AT1226 電圧」
    题解「AT1983 [AGC001E] BBQ Hard」
  • 原文地址:https://www.cnblogs.com/ssh-html/p/12333897.html
Copyright © 2020-2023  润新知