文章目录
- 前言
- 一、用户登录权限效验
- 1、最初用户登录验证
- 2、Spring AOP 用户统⼀登录验证的问题
- 3、Spring 拦截器
- ① 自定义拦截器
- ② 将自定义拦截器加入到系统配置
- 4、拦截器实现原理
- ① 实现原理源码分析
- ② 拦截器小结
- 5、扩展:统⼀访问前缀添加
- 二、统⼀异常处理
- 1、使用方法
- 2、异常分类
- 三、统⼀数据返回格式
- 1、为什么需要统⼀数据返回格式?
- 2、统⼀数据返回格式的实现
- 3、@ControllerAdvice 源码分析(了解)
前言
之前我们学习了 Spring AOP,我们就可以通过 AOP 在 Spring Boot 中进行统⼀功能处理模块:
- 统⼀⽤户登录权限验证;
- 统⼀数据格式返回;
- 统⼀异常处理。
一、用户登录权限效验
1、最初用户登录验证
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 某⽅法 1
*/
@RequestMapping("/m1")
public Object method(HttpServletRequest request) {
// 有 session 就获取,没有不会创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,业务处理
return true;
} else {
// 未登录
return false;
}
}
/**
* 某⽅法 2
*/
@RequestMapping("/m2")
public Object method2(HttpServletRequest request) {
// 有 session 就获取,没有不会创建
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
// 说明已经登录,业务处理
return true;
} else {
// 未登录
return false;
}
}
// 其他⽅法...
}
上述代码中,每个⽅法中都有相同的⽤户登录验证权限,这样就会很麻烦,即使将该方法封装为公共方法,还是需要在方法中进行判断,这样就增加了维护成本。
所以我们就需要提供一个 AOP 方法来进行统一验证。
2、Spring AOP 用户统⼀登录验证的问题
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
// 定义切点⽅法 controller 包下、⼦孙包下所有类的所有⽅法
@Pointcut("execution(* com.example.demo.controller..*.*(..))")
public void pointcut(){ }
// 前置⽅法
@Before("pointcut()")
public void doBefore(){
}
// 环绕⽅法
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object obj = null;
System.out.println("Around ⽅法开始执⾏");
try {
// 执⾏拦截⽅法
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around ⽅法结束执⾏");
return obj;
}
}
如果要在以上 Spring AOP 的切⾯中实现⽤户登录权限效验的功能,有以下两个问题:
- 没办法获取到 HttpSession 对象。
- 我们要对⼀部分⽅法进⾏拦截,⽽另⼀部分⽅法不拦截,如注册⽅法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。
3、Spring 拦截器
对于上述问题,Spring 官方就提供了一个拦截器来解决这个问题。
① 自定义拦截器
实现 HandlerInterceptor 类,重写其 preHandle 方法:
package com.example.springbootaop.config;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 登录拦截器
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登录的判断业务
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
return true;
}
response.setStatus(401);
return false;
}
}
② 将自定义拦截器加入到系统配置
实现 WebMvcConfigurer ,重写其 addInterceptors 方法:
package com.example.springbootaop.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration // 一定不要忘记
public class MyConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/reg"); // 排除不拦截的 url
}
}
其中:
- addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意⽅法(也就是所有方法)。
- excludePathPatterns:表示需要排除的 URL。
排除所有的静态资源
package com.example.springbootaop.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration // 一定不要忘记
public class MyConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/**/*.js")
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.jpg")
.excludePathPatterns("/login.html")
.excludePathPatterns("/**/login"); // 排除不拦截的 url
}
}
4、拦截器实现原理
有无拦截器业务执行的流程对比:
① 实现原理源码分析
所有的 Controller 执⾏都会通过⼀个调度器 DispatcherServlet 来实现:
而 DispatcherServlet 这个调度器里边有一个叫 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;
}
// 执⾏ Controller 中的业务
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 di spatch 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:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
在 applyPreHandle 中会获取所有的拦截器 HandlerInterceptor 并执⾏拦截器中的 preHandle ⽅法,与我们之前自定义拦截器时相同:
② 拦截器小结
通过上⾯的源码分析,我们可以看出,Spring 中的拦截器也是通过动态代理和环绕通知的思想实现的:
5、扩展:统⼀访问前缀添加
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 所有的接⼝添加 api 前缀
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);
}
}
二、统⼀异常处理
1、使用方法
统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的:
package com.example.springbootaop.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
/**
* 统一处理异常
*/
@ControllerAdvice
public class ErrorAdive {
@ExceptionHandler(Exception.class)
@ResponseBody
public HashMap<String,Object> exAdvie(Exception e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg",e.getMessage());
return result;
}
}
在 UserController 中设置⼀个空指针异常:
访问结果为:
2、异常分类
我们可以针对不同的异常,返回不同的结果:
package com.example.springbootaop.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
/**
* 统一处理异常
*/
@ControllerAdvice
public class ErrorAdive {
@ExceptionHandler(Exception.class)
@ResponseBody
public HashMap<String,Object> ExceptionAdvie(Exception e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-1");
result.put("msg",e.getMessage());
return result;
}
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public HashMap<String,Object> ArithmeticExceptionAdvie(ArithmeticException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code","-2");
result.put("msg",e.getMessage());
return result;
}
}
在 UserController 中设置⼀个空指针异常:
访问结果为:
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配
三、统⼀数据返回格式
1、为什么需要统⼀数据返回格式?
统⼀数据返回格式的优点有很多,主要就是为了统一格式,降低沟通成本。
2、统⼀数据返回格式的实现
统⼀的数据返回格式可以使⽤ @ControllerAdvice + ResponseBodyAdvice 的⽅式实现
package com.example.springbootaop.config;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 构造统⼀返回对象
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data", body);
return result;
}
}
postman 测试:
3、@ControllerAdvice 源码分析(了解)
从上述源码可以看出 @ControllerAdvice 派⽣于 @Component 组件,⽽所有组件初始化都会调⽤ InitializingBean 接⼝:
这个⽅法中有⼀个 initControllerAdviceCache ⽅法:
这个⽅法在执⾏是会查找使⽤所有的 @ControllerAdvice 类,这些类会被注册在容器中,但发⽣某个事件时,调⽤相应的 Advice ⽅法。