《苍穹外卖》知识梳理P4-异常处理

《苍穹外卖》知识梳理P4

   上一节中介绍了配置文件以及配置类的内容,详见P3
   在本节中,将会对本项目中使用的异常处理机制进行说明。

一.自定义异常

   为了方便定位错误,使用自定义异常;自定义异常会存放在common模块中,存放在common模块下的com.sky.exception包下;

/**
 * 业务异常,基类异常继承运行时异常,提供2个构造函数,其中有参构造可以将自	    
 * 定义的异常信息传入;
 */
public class BaseException extends RuntimeException {

    public BaseException() {
    }

    public BaseException(String msg) {
        super(msg);
    }

}

   随后会同样的定义一些业务异常,各个业务异常分别对应不同的异常情况;比如AccountNotFoundException对应账号不存在的业务情况;另外,其他的业务异常都继承自BaseException。

/**
 * 账号不存在异常,提供2个构造函数,其中有参构造可以将自	    
 * 定义的异常信息传入;
 */
public class AccountNotFoundException extends BaseException {

    public AccountNotFoundException() {
    }

    public AccountNotFoundException(String msg) {
        super(msg);
    }

}

  同理,也可以自定义一些常见的业务异常按照上边的格式,比如登陆失败异常,账号锁定异常,密码修改失败异常等;

二.全局异常处理器

2.1业务异常的处理(最终都隶属于运行时异常)

  在上边添加了若干自定义异常之后,在业务层可能出现的位置进行条件判断之后直接抛出异常即可,由于抛出异常后会由调用业务层的方法进行接收,而控制器并没有直接对异常的处理,因此会继续抛出,此时异常会被全局异常处理器接收,并进行处理;
全局异常处理器中常用用的2个注解:@RestControllerAdvice与@ExceptionHandler
  @RestControllerAdvice注解主要用于统一处理在所有 @Controller 或 @RestController 类中抛出的异常,并进行全局的异常处理和响应处理。在全局异常处理器类上使用。
  @ExceptionHandler 注解可用于一个具体的控制器中,也可以结合 @ControllerAdvice 和 @RestControllerAdvice 使用。

  • 直接使用:当一个控制器方法抛出异常时,可以在同一控制器中使用 @ExceptionHandler 注解的方法来处理该异常。这允许你在局部范围内定义异常处理逻辑。
  • 结合@ControllerAdvice 和 @RestControllerAdvice 使用:在全局范围内处理异常。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @ExceptionHandler
    public Result exceptionHandler(BaseException baseException){
        log.error("异常信息:{}", baseException.getMessage());
        return Result.error(baseException.getMessage());
    }
    
}
2.2SQL异常的处理

  SQL异常(SQLException )并不属于运行时异常,而是属于受检查异常,而我们在插入数据时,可能因为某个字段设置了“唯一”而导致后边插入相同该字段的数据时出现SQLIntegrityConstraintViolationException异常,该异常继承自SQLException ,因此我们单独处理这一异常。

@ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        //Duplicate entry 'zhangsana' ... 表示用户名重复;
        String message=ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split=message.split(" ");
            String username=split[2];
            String msg=username+ MessageConstant.ACCOUNT_EXISTS;
            return Result.error( MessageConstant.ACCOUNT_EXISTS);
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }

在上述代码中,首先获取异常信息,如果异常信息中包含了Duplicate entry信息,说明出现了重复字段;按照规则返回错误信息即可;

三.拦截器

3.1配置信息的读取;

  该项目中,将配置信息写在了application.yml中,而一些类需要读取配置信息,因此可以将需要的不同种类的配置信息单独设置成一些类放在common模块下,以便需要的时候读取,比如:

  • JwtProperties:Jwt使用的一些配置信息
  • AliOssProperties:阿里云OSS使用的一些配置信息
  • WeChatProperties:微信小程序使用的一些配置信息

以JwtProperties为例:
  1.通过@ConfigurationProperties(prefix = “sky.jwt”)注解读取applicaiotn.yml中对应位置的配置信息,自动为同一字段的变量赋值;
例如:adminSecretKey将会被赋值为admin-secret-key:对应的值itcast;
  2.通过@Component注解将JwtProperties注册为Bean,交给spring容器管理,默认为单例模式(因为实际上属性对象只需要一个就够了)
  3.通过@Data注解提供get,set,toString()等方法,以便后续读取属性对象的值;

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;

    /**
     * 用户端微信用户生成jwt令牌相关配置
     */
    private String userSecretKey;
    private long userTtl;
    private String userTokenName;

}

3.2 拦截器

  拦截器,在请求访问资源之前,先进行拦截,主要用于登录检验,查看用户是否已经登录,如果已经登录,可以直接放行;如果未登录,则不能放行;主要逻辑在于preHandler中,返回true放行,返回flase不放行;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        try {
            String token = request.getHeader(jwtProperties.getAdminTokenName());
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            //一个小bug,解析出来的载荷中的有数字的部分都是Integer,然而设置的时候不一定是数据部分,所以
            //可以先转换为String,再按照需要转换为其他类型;
            Long employeeId=Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());

            //取出id之后,将id存放在该线程的内存空间之中,由于这是同一个请求时同一个线程,所以后续Controller与Service
            //可以单独取出该线程使用;
            BaseContext.setCurrentId(employeeId);

            log.info("解析到的员工ID为:{}",employeeId);
            if(employeeId!=null){
                //如果有登录数据,代表一登录,放行
                return true;
            }else{
                //否则,发送未认证错误信息
                response.setStatus(401);
                return false;
            }
        } catch (NumberFormatException e) {
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }

    }
}

  配置类统一放在config包下,都需要加上@Configuration注解,以声明这是一个配置类;从而被加载到spring的容器中;将需要用到的属性,拦截器等注入进来;
  这里主要说明web层相关组件配置类,主要是addInterceptors方法,添加拦截器,将Jwt拦截器注入进来,注册拦截器时拦截所有以/admin开头的请求,但是放行/admin/employee/login,/admin/employee/logout即登陆与登出,因为登陆和登出都应当是可以直接操作的而无需登陆校验。其余配置都是对Swagger的配置;

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login","/admin/employee/logout");
    }
	
    /**
     * 通过knife4j生成接口文档
     * @return
     */
    @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端接口")
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

    @Bean
    public Docket docketUser() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户端接口")
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

    /**
     * 设置静态资源映射
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    /**
     * 扩展Spring MVC的消息转化器,统一对后端返回给前端的数据进行处理;
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器......");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //为消息转换器设置对象转换器,可以将Java对象序列化为JSON;
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转换器加入容器之中;并设置优先使用自己的消息转换器;
        converters.add(0,converter);
    }
}

你可能感兴趣的:(《苍穹外卖》实操总结系列,java,spring,maven,spring,boot,mybatis)