SpringBoot统一功能处理。也就是AOP的具体实现。
最原始的用户登录验证方法,我们通过封装了一个方法来判断用户是否登录,但如果实现的功能多了,那么每一个需要登录的功能都要在对应的接口中来调用这个函数来判读是否登录。
public class LoginStatus {
public static User getStatus(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
//当前用户未登录
return null;
}
User user = (User) session.getAttribute("user");
if (user == null) {
//当前用户未登录
return null;
}
return user;
}
}
上面的代码虽然已经封装成了方法,但是如果随着程序功能的增多,那么每一个控制器都要调用这个接口进行判断,就出现了代码的冗余,也增加了代码的维护成本。
这个时候就需要提供一个公共的方法来进行统一的用户登录权限验证了。
统一验证我们可以使用SpringAOP的前置通知或者是环绕通知来实现
@Aspect // 说明该类为一个切面
@Component
public class UserAspect {
// 定义切点,使用 AspectJ表达式语法,拦截UserController所有方法
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
// 前置通知
@Before("pointcut()")
public void doBefore() {
System.out.println("执行Before前置通知");
}
// 添加环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object result = null;
System.out.println("执行环绕通知的前置方法");
try {
// 执行(拦截的)业务方法
result = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("执行环绕通知的后置方法");
return result;
}
}
我们发现原生的SpringAOP的切面实现用户登录权限的校验功能,会有两个问题
那就可以使用Spring的拦截器
对于上面两个问题Spring中提供了解决方案,提供了具体实现的拦截器:Handlerlnterceptor,拦截器的实现分为两个步骤:
1.自定义拦截器
用户登录权限校验,自定义拦截器代码实现:
/**
* 定义自定义拦截器实现用户登录校验
*/
@Configuration
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) {
response.setStatus(200);
return true;
}
response.setStatus(403);
return false;
}
}
2.将自定义拦截器加入到系统配置中
将上一步自定义的拦截器加入到系统配置信息中,代码实现:
@Configuration
public class AppConfig implements WebMvcConfigurer {
//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()) // 添加自定义拦截器
.addPathPatterns("/**") //拦截所有接口
.excludePathPatterns("/**/login")//排除的接口
}
}
*”表示拦截任意⽅法(也就是所有⽅法)排除所有静态的资源
@Configuration
public class AppConfig implements WebMvcConfigurer {
//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()) // 添加自定义拦截器
.addPathPatterns("/**")
.excludePathPatterns("/**/*.html")//排除所有静态资源
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.js")
.excludePathPatterns("/**/img/*");
}
}
原本正常的调用流程是这样的:

但是添加了拦截器后,在调用Controller之前会进行相对应业务处理


⽽所有⽅法都会执⾏ 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);
//此处省略上面代码
// 调用预处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 执行Controller中的业务
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// ......后面代码省略
}
从上述源码可以看出在开始执⾏ Controller 之前,会先调⽤ 预处理⽅法 applyPreHandle,⽽
applyPreHandle ⽅法的实现源码如下
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 获取项⽬中使⽤的拦截器 HandlerIntercepto
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 ⽅法,这样就会咱们前⾯定义的拦截器对应上了,如下图所示 :

只有当我们重写的方法放回true的时候才会继续走调用Controller的业务代码,否则就是直接放回给前端

拦截器的实现原理:
该方法可以给所有接口添加一个访问前缀,让前端访问接口时都要加上blog,比如原来是/add,添加前缀后就是/blog/add
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("blog",pre->true);
}
}
其中第⼆个参数是⼀个表达式,设置为 true 表示启动前缀
同一异常处理是通过@ControllerAdvice+@ExceptionHandler两个注解结合实现的,@ControllerAdvice表示控制器通知类,@ExceptionHandler是异常处理,两个结合起来就表示出现异常的时候执行某一个通知,也就是执行某个方法,代码实现:
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public Object handler(Exception e) {
Map<String,Object> map = new HashMap<>();
map.put("success",1);
map.put("status",-1);
map.put("message","服务器接口异常");
return map;
}
}
注意:方法名和返回值可以任意,重要的是注解。
这里的代码表示的是发生任何异常都给前端返回一个HashMap,也可以指定异常进行处理代码如下
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public Object exceptionAdvice(Exception e) {
Map<String,Object> map = new HashMap<>();
map.put("success",1);
map.put("status",-1);
map.put("message","服务器接口异常");
return map;
}
@ExceptionHandler(NullPointerException.class)
@ResponseBody
public Object nullPointerExceptionAdvice(NullPointerException exception) {
Map<String,Object> map = new HashMap<>();
map.put("success",1);
map.put("status",-1);
map.put("message",exception.toString());
return map;
}
}
当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配
统一数据返回格式有很多好处
统一的数据返回格式可以使用@ControllerAdvice+ResponseBodyAdvice实现
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* 内容是否需要重写,此方法可以选择部分控制器和方法进行重写
* 返回 true表示重写
*/
@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) {
Map<String,Object> result = new HashMap<>();
result.put("success",200);
result.put("status",1);
result.put("data",body);
return result;
}
}