上一个博客中说到AOP的主要作用就是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点,减少对业务代码的侵入。增强代码的可读性和可维护性。本章博客是通过代码(Spring Boot的统一功能处理模块)来展现AOP思想。
目录
前言
一、用户登录权限校验
1、使用原生Spring AOP实现统一登录验证的问题
2、Spring 拦截器
2.1、自定义拦截器
2.2、将自定义拦截器设置到当前的项目中
2.3、拦截器的实现原理
二、统一的异常处理
三、统一数据返回格式
1、统一数据返回格式的优点
2、统一数据返回格式的实现
四、总结
之前在小编的【JavaEE】博客系统前后端交互_奋斗べ青年.的博客-CSDN博客中完成用户登录校验需要在博客系统需要完成校验的页面中都添加上校验方法,这样代码看起来非常的冗余,而其修改起来也是非常的繁琐。但是在上一个博客中了解到了AOP,我们可以使用AOP的前置通知和环绕通知来实现统一校验。
package com.example.demo.common;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
//定义切点,@Pointcut注解的参数中定义了具体的拦截规则
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
//定义前置通知
@Before("pointcut()")//表示这个通知是针对pointcut方法的
public void doBefore(){
System.out.println("执行了前置通知");
}
//定义环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
System.out.println("环绕通知之前");
Object result = null;
try {
//执行拦截方法
result = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("环绕通知之后");
return result;
}
}
虽然使用原生Spring AOP的切面中能过实现用户登录权限的校验的功能,但是存在以下两个问题。
- 没有办法获取到HtttpSession对象
- 我们要对一部分方法进行拦截,而另一部分方法不拦截,如注册方法和登录方法是不拦截的,这样的话排除方法的规则很难定义,甚至没有办法定义。
拦截器是面向切面编程——AOP的一种具体实现。我们可以使用拦截器来执行某些任务,例如在Controller处理访问页面的请求之前,进行用户登录校验。拦截器可以实现日志记录、权限校验、性能监控、通用行为等功能。
对于上面我们说到的使用原生Spring AOP实现统一验证的问题,我们可以使用拦截器来解决。我们需要创建一个实现了HandlerInterceptor接口的类,并更具需要实现的业务需求来重写接口中的这三个方法:preHandler、postHandler、afterCompletion。
- preHandler方法相当于我们AOP中说的前置通知。它是在请求处理之前被调用。该方法在我们创建的拦截器来中最先执行,用来进行一些前置初始化操作或是对当前请求做预处理,也可以进行一些判断来决定请求是否要继续执行下去。该方法的返回值是Boolean类型,当它返回值为false,表示请求结束,后续的拦截器和Controller都不要在执行了;当它返回值为true是会继续执行调用下一个拦截器的preHandler方法,如果已经是最后一个拦截器的时候就会调用当前请求的Controller方法。
- postHandler方法相当于我们AOP中说的后置通知。他是在当前请求处理完成之后,也就是Cotroller方法调用之后执行。
- afterCompletion方法需要在当前对应的拦截器类的postHandler方法返回值为true时才会执行,也就是说该方法在整个请求结束之后。这个方法主要用来进行资源清理。
✨拦截器的实现分为两步
- 创建自定义拦截器,实现HandlerInterceptor接口的preHandle(执行具体方法之前的预处理)方法。
- 将自定义拦截器加入WebMvcConfigurer的addInterceptors方法中。
创建一个普通类实现HandlerInterceptor接口,来实现自定义拦截器。
import com.example.demo.common.AppVar;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Component
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//业务方法
//从请求中取session,如果有session,直接获取到,但是没有,这里设置为false,也不会新创建一个session。
HttpSession session = request.getSession(false);//这里添加false表示不会新创建session。方法中默认的是true。
if(session!=null && session.getAttribute(AppVar.SESSION_KEY)!=null){
return true;
}
return false;
}
}
getAttribute方法是Object类中的方法,用于获取对象的指定属性值,它接受一个参数,即要获取的属性的名称,并返回该属性的值,如果对象中不存在指定名称的属性,则返回null。该方法可以用于获取对象的任意属性,包括实例变量和静态变量。
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
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 AppConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor)
.addPathPatterns("/**")// /**表示拦截所有的请求
.excludePathPatterns("/user/reg")//排除不拦截的url
.excludePathPatterns("/user/login")//排除不拦截的url
;
}
}
这里的addInterceptor方法接受一个参数,就是要添加的拦截器对象。可以通过该方法添加一个或多个拦截器。
✨提示
当我们想要排除所有的静态文件,静态文件包含图片文件,前端的JS和CSS等文件,这个时候我们不可能将每种格式的文件都手动进行排除,这样工作量也太大了(图片文件存在几十种格式),想要将这些文件排除掉我们可以将这个静态文件放入项目的static目录中,然后针对这个目录中的子目录(图片,css文件,js文件)进行排除。
excludePathPatterns("/image/**")//表示排除image目录下的所有图片
没有实现拦截器的时候,用户发送的请求直接被控制层接收到,进而在相应的URL中进行登录校验,这种方式代码的可维护性较低。
但是使用拦截器,用户发送的请求首先会被拦截器接收到,拦截器进行预处理,符合条件才会进一步调用Controller层的方法。
我们之前处理异常的方法就是使用try-catch,或者是将异常抛出去给更上一层处理,这种方式处理异常的方式通常是分散在代码的各个部分中的,当应用程序出现异常时,开发需要在每个可能抛出异常的地方编写相应的异常处理代码,这样做会导致代码冗余,可读性差,并且难以维护。
使用统一异常处理可以将所有的异常情况几种处理,提高代码的可维护性和可读性。此外,还能够实现统一的异常相应,为前端或其他服务提供友好的错误信息。
- 在Spring Boot中,可以使用@RestControllerAdvice注解和@ExceptionHandler注解来实现统一异常处理。这两个注解搭配使用表示的是全局异常处理,可以捕获并处理全局范围内的异常。当控制器中抛出异常时,会根据异常类型匹配对应的@ExceptionHandler方法进行处理。
- @RestControllerAdvice注解用在一个类上,表示该类是一个全局的控制器增强器,可以对所有的控制器进行统一的处理。这个注解提供了一种集中管理和统一处理全局范围内操作的方式,在引用程序中起到了很好的代码复用和统一管理的作用。
- @ExceptionHandler注解,用于定义一个方法,该方法用于处理控制器中发生的异常。当控制器中的方法抛出异常时,@ExceptionHandler注解标记的方法将被调用来处理该异常。这样可以集中处理控制器中的异常。
1️⃣统一返回结果
首先需要定义一个类,在这个类中设置属性,用来定义一个统一返回结果格式。
package com.example.demo.common;
import lombok.Data;
@Data
public class ResultAjax {
private int code;//状态码
private String msg;//状态码的描述
private Object data;//返回数据
}
2️⃣创建一个全局异常处理类
使用@RestControllerAdvice注解来标记它。也可以使用@ControllerAdvice+@ResponseBody注解来定义这个类,@ResponseBody注解表示返回的结果为数据而不是页面。在这个@RestControllerAdvice标记的类中用@ExceptionHandler注解标记的方法是一个处理器,用来集中处理控制器中的异常
import com.example.demo.common.ResultAjax;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public ResultAjax doNullPointerException(Exception e){
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(-1);
resultAjax.setMsg("异常信息:"+e.getMessage());
resultAjax.setData(null);
return resultAjax;
}
}
这里由于异常的情况存在很多种,我们使用Exception,所有的异常都可以使用它捕获到。
- 方便前端程序员更好的接收和解析后端数据接口返回的数据。
- 降低前端程序员和后端程序员的沟通成本,按照某个个格式实现就行了,因为所有返回接口都是这样返回的
- 有利于项目统一数据的维护和修改
- 有利于后端技术部门的统一规范的标准指定,不会出现稀奇古怪的返回内容。
统一的数据返回格式可以使用@ControllerAdvice+ResponseBodyAdvice的方式实现,实现步骤如下。
- 创建一个类,并添加@ControllerAdvice注解,让这个类对所有的控制器可以统一处理。
- 实现ResponseBodyAdvice接口,并重写supports和beforeBodyWrite方法。
代码如下
1️⃣可以单独设置一个类,用来定义返回数据的格式。
import lombok.Data;
/*
* 统一对象
* */
@Data
public class ResultAjax {
private int code;//状态码
private String msg;//状态码的描述
private Object data;//返回数据
/*
* 返回成功对象
* */
public static ResultAjax succ(Object data){
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMsg("");
resultAjax.setData(data);
return resultAjax;
}
public static ResultAjax succ(String msg,Object data){
ResultAjax resultAjax = new ResultAjax();
resultAjax.setCode(200);
resultAjax.setMsg(msg);
resultAjax.setData(data);
return resultAjax;
}
}
在这个类中定义的这些方法,已经可以简单实现数据的统一返回,但是还存在,有的程序员在写业务代码的时候,并没有按照规定的格式来返回数据,这个时候前端就得不到数据了。所以我们需要一个统一返回值的保底实现。
2️⃣统一返回值的保底实现
这里由于String类型的数据渲染时机和其他类型的数据不同,需要我们使用Jackson进行单独处理。
package com.example.demo.config;
import com.example.demo.common.ResultAjax;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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;
/*
* 统一返回值的保底实现类
* */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//当这个方法的返回值为true时,才会继续向下调用beforeBodyWrite方法
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
//判断返回的对象的类型是否为ResultAjax,如果是,则表示这个对象是已经包装好的格式
if(body instanceof ResultAjax){
return body;
}
//对字符串进行判断和处理
if(body instanceof String){
ResultAjax resultAjax = ResultAjax.succ(body);
try {
return objectMapper.writeValueAsString(resultAjax);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return ResultAjax.succ(body);
}
}
- 自定义拦截器的实现:使用HandlerInterceptor接口+WebMvcConfigurer接口实现。
- 统一异常的处理:使用@RestControllerAdvice注解+@ExceptionHandler注解实现。
- 统一数据返回格式:使用@ControllerAdvice注解+ResponseBodyAdvice接口实现。