上一节中介绍了配置文件以及配置类的内容,详见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个注解:@RestControllerAdvice与@ExceptionHandler
@RestControllerAdvice注解主要用于统一处理在所有 @Controller 或 @RestController 类中抛出的异常,并进行全局的异常处理和响应处理。在全局异常处理器类上使用。
@ExceptionHandler 注解可用于一个具体的控制器中,也可以结合 @ControllerAdvice 和 @RestControllerAdvice 使用。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler
public Result exceptionHandler(BaseException baseException){
log.error("异常信息:{}", baseException.getMessage());
return Result.error(baseException.getMessage());
}
}
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信息,说明出现了重复字段;按照规则返回错误信息即可;
该项目中,将配置信息写在了application.yml中,而一些类需要读取配置信息,因此可以将需要的不同种类的配置信息单独设置成一些类放在common模块下,以便需要的时候读取,比如:
以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;
}
拦截器,在请求访问资源之前,先进行拦截,主要用于登录检验,查看用户是否已经登录,如果已经登录,可以直接放行;如果未登录,则不能放行;主要逻辑在于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);
}
}