我们要实现以下3个目标:
- 统一用户登录权限
- 统一数据格式返回
- 统一异常处理
1.用户的登录权限校验
1.1Spring AOP用户统一登录验证问题
@Aspect
@Component
public class UserAspect {// 定义切点controller包下、子孙包下所有类的所有方法@Pointcut("execution(* com.example.demo.controller..*.*(..))")public void pointcut(){}//前置方法@Before("pointcut()")public void doBefore() {System.out.println("Before开始执行!");}//环绕方法@Around("pointcut()")public Object doAround(ProceedingJoinPoint joinPoint) {Object obj = null;System.out.println("Around方法开始执行");try {obj=joinPoint.proceed();} catch (Throwable e) {e.printStackTrace();}System.out.println("Around方法执行结束");return obj;}}
在以上Spring AOP的切面中实现用户登录权限校验的功能,有以下几个问题:
- 没办法获取到HttpSession对象
- 我们要对一部分方法进行拦截,而另一部分方法不拦截,我们是很难定义的。
1.3Spring拦截器
对于以上问题Spring中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下连两个步骤:
- 创建自定义拦截器:实现HandlerInterceptor接口的preHandle方法(执行具体方法之前预处理)
- 自定义拦截器加入WebMvcConfigurer的addInterceptors方法中
1.3.1自定义拦截器
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {HttpSession session = request.getSession(false);if(session != null && session.getAttribute("userinfo") != null) {return true;}response.setStatus(401);return false;}
}
1.3.2将自定义拦截器加入到系统配置
@Configuration
public class AppConfig implements WebMvcConfigurer {//添加拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") //拦截所有接口.excludePathPatterns("/art/param11"); //排除一些接口}
}
- addPathPatterns:表示需要拦截的URL,**表示拦截任意方法
- excludePathPatterns:表示需要排除的URL
说明:以上拦截规则可以拦截此项目中使用的URL,包括静态文件
排除所有的静态资源
@Configuration
public class AppConfig implements WebMvcConfigurer {//添加拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") //拦截所有接口.excludePathPatterns("/art/param11") //排除一些接口.excludePathPatterns("/**/*.js").excludePathPatterns("/**/*.css").excludePathPatterns("/**/*.jpg").excludePathPatterns("login.html").excludePathPatterns("/**/login"); }
}
1.4拦截器实现原理
有了拦截器之后,执行流程如下图所示:
1.4.1实现源代码分析
所有的Controller执行都会通过一个调度器DispatcherServlet来实现,这一点可以从Spring Boot控制台的打印信息看出来,如下图所示(我们必须触发拦截功能):
而所有的方法都会执行DispatcherServlet中doDispatch调度方法,doDispatch源码如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {try {ModelAndView mv = null;Object dispatchException = null;try {processedRequest = this.checkMultipart(request);multipartRequestParsed = processedRequest != request;mappedHandler = this.getHandler(processedRequest);if (mappedHandler == null) {this.noHandlerFound(processedRequest, response);return;}HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());String method = request.getMethod();boolean isGet = HttpMethod.GET.matches(method);if (isGet || HttpMethod.HEAD.matches(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {return;}}if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}this.applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);} catch (Exception var20) {dispatchException = var20;} catch (Throwable var21) {dispatchException = new NestedServletException("Handler dispatch failed", var21);}this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);} catch (Exception var22) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);} catch (Throwable var23) {this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));}} finally {if (asyncManager.isConcurrentHandlingStarted()) {if (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}} else if (multipartRequestParsed) {this.cleanupMultipart(processedRequest);}}}
我们发现在开始执行Controller之前,会先调用预处理方法applyPreHandle,而这个方法就是获取实现HandlerInterceptor接口的所有所有类,并调用preHandler方法。
2.统一异常处理
统一异常处理使用的是@ControllerAdvice + @ExceptionHandler来实现的,@ControllerAdvice表示控制器通知类,@ExceptionHandler是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行了某个方法事件,具体代码实现如下:
@ControllerAdvice
public class ErrorAdive {@ExceptionHandler(Exception.class) @ResponseBodypublic Object handler(Exception e) {HashMap<String, Object> map = new HashMap<>();map.put("state", 0);map.put("data", null);map.put("msg", e.getMessage());return map;}
}
PS:方法名和返回值可以自定义,重要的是@ControllerAdvice和ExceptionHandler()
我们可以针对不同的异常设置不同的注解,这将返回不同的结果。
@ControllerAdvice
public class ErrorAdive {@ExceptionHandler(Exception.class)@ResponseBodypublic Object handler(Exception e) {HashMap<String, Object> map = new HashMap<>();map.put("state", 0);map.put("data", null);map.put("msg", e.getMessage());return map;}@ExceptionHandler(NullPointerException.class)@ResponseBodypublic Object nullPointerExceptionAdvice(NullPointerException e) {HashMap<String, Object> ret = new HashMap<>();ret.put("state", 0);ret.put("msg", "空指针异常");ret.put("data", null);return ret;}}
当有多个异常通知的时候,匹配顺序为当前类及其子类向上依次匹配。
3.统一数据返回格式
3.1为什么需要统一数据返回格式?
统一数据返回格式有以下优点:
- 方便前端程序员更好的接收和解析后端数据接口返回的数据
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就行了
- 有利于项目统一的数据的维护和修改
- 有利于后端技术部门的统一规范的标准制定
3.2统一数据返回格式的实现
统一的数据格式返回可以使用@ControllerAdvice + @ResponseBodyAdvice的方法实现,具体实现代码如下:
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {/****判断内容是否需要重写,我们这里默认需要重写**/@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//构造统一的返回格式HashMap<String, Object> result = new HashMap<>();result.put("state", 1);result.put("msg", "");result.put("data", body);return result;}
}
4.总结
- 登录校验使用WebMvcConfigurer + HandlerInterceptor实现,
- 统一异常处理使用@ControllerAdvice + @ExceptionHandler 来实现
- 统一的返回值处理使用@ControllerAdvice + ResponseBodyAdvice来实现