统一功能处理的作用,也就是 AOP 的主要作用,主要如下:
如果不使用 AOP 的话,
最初用户登录验证,要在每个方法类名获取 session 和 session 当中的信息:
@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;
}
}
// 其他⽅法...
}
像这样每个方法都有相同的用户登录验证权限,所有就导致很繁琐,而且添加的控制器越多,调用用户验证的方法也就越多,这样就增加了后期的修改成本和维护成本。
使用 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;
}
}
但是这样的话,没办法获取到 Session 对象,而且如果要对一部分进行拦截,一部分不拦截的话,是很难做到的。所以就通过 Spring 拦截器来实现。
Spring 拦截器是基于 AOP 实现的,因为原生的 AOP 没有办法获取 HttpSession 和 Request 对象,而且拦截规则也很难定义。而 拦截器 当中,封装了很多对象,对象里面专门提供了专门的方法来解决这样的问题。
重写 HandlerInterceptor 接口的 preHandle 方法 :preHandle 返回是 true,则表示通过了 拦截器的验证,可以继续 执行,调用 目标方法了。反之,验证没有通过,直接返回一个错误信息。
使用拦截器之后 :就是前端先访问 拦截器,执行里面的 preHandle 方法,如果方法返回 true,则继续执行后面的代码。如果返回的是 false,直接返回一个错误信息,后面的代码就不执行了。示例代码如下:
@Component
public class LoginIntercept implements HandlerInterceptor {
/**
* 返回 true 表示拦截判断通过,可以访问后面的接口
* 返回 false 表示拦截判断未通过,直接返回后面结果给前端
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//得到 HttpSession 对象
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
//表示已经登录
return true;
}
//如果未登录,然后就跳转到登陆页面
response.sendRedirect("/login.html");
return false;
}
}
把上一步的自定义拦截器加入到框架配置中,代码如下:
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
private LoginIntercept loginIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginIntercept).addPathPatterns("/**").//拦截所有的 url
excludePathPatterns("/user/login").//不拦截登录接口
excludePathPatterns("/user/reg").//不拦截注册接口
excludePathPatterns("/login.html").
excludePathPatterns("/reg.html").
excludePathPatterns("/**/*.js").
excludePathPatterns("/**/*.css").
excludePathPatterns("/**/*.png");
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);//对所有的地址都要加上 api 前缀才能访问
}
}
因为默认的配置文件都是叫做 Spring MVC Configurer 的一个文化。所以也要去实现 Web MVC Configurer 接口。然后在类上,添加 Configuration 注解,使其成系统的配置类。当前类实现 WebMvcConfigurer 接口。重写 WebMvcConfigurer 接口中的 addInterceptors 方法。就实现了 Spring 拦截器的流程。
访问 index 页面,运行结果如下:
因为我们代码当中设置了,如果未登陆的话,就跳转到登录界面:
设置 Session 然后再访问就可以访问到页面了。
在不使用拦截器的时候,访问方式如下:
有了拦截器之后,在调用 Controller 之前会进行相应的业务处理,流程如下:
从项目执行的日志来看,所有的 Controller 执行都会通过一个 调度器:DispatcherServlet 来实现:
每次执行的时候,都会去调用 DispatcherServlet ,因为 Dispatcher 的中文意思是 调度器。也就是说所有的请求来了之后,会先进入调度器,然后由调度器进行分发。就像我们买东西,快递都会先到当地的转运中心,然后才会分发,最后到我们手上。
拦截器也是通过 动态代理 和 环绕通知实现的,大概的调用流程如下:
然后,原本的 Spring AOP 的 切面类(代理对象) 换成了 DispatcherServlet 。一个是我们自定义的,一个 Spring 框架 自带的。不过执行原理并没有变,用户 想要与 目标对象 直接交互,必须要通过 代理对象(拦截器)的验证。通过之后,才可以访问目标对象。
也就是对所有请求地址添加 api 前缀:
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true);//对所有的地址都要加上 api 前缀才能访问
}
对于异常的处理,按道理来说,我们是要给前端返回一个 JSON 格式的信息,但是如果不小心写了一个 bug :
@RequestMapping("/index")
public String index() {
int num = 10 / 0;
return "hello index";
}
也就是如过返回的话,前端会报 500 异常的。如果加上 try catch 也可以完成异常的捕获,但是就不能进行数据的回滚了。
实现统一异常处理有两步 :
因为是算数异常,所有就可以直接加算术异常:
@RestControllerAdvice
public class MyExceptionAdvice {
@ExceptionHandler(ArithmeticException.class)
public HashMap<String, Object> arithmeticExceptionAdvice(ArithmeticException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state", -1);
result.put("data", null);
result.put("msg", "算术异常:" + e.getMessage());
return result;
}
}
代码如下:
@RequestMapping("/index2")
public String index2() {
Object obj = null;
System.out.println(obj.hashCode());
return "hello index2";
}
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> nullPointerExceptionAdvice(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state", -1);
result.put("data", null);
result.put("msg", "空指针异常:" + e.getMessage());
return result;
}
代码如下:
@RequestMapping("/index3")
public String index3() {
String obj = "aaa";
System.out.println(Integer.valueOf(obj));
return "hello index2";
}
也就是异常很多很多,我们不能穷举,所以通过 所有异常的 父类来实现:
@ExceptionHandler(Exception.class)
public HashMap<String, Object> exceptionAdvice(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("state", -1);
result.put("data", null);
result.put("msg", "异常:" + e.getMessage());
return result;
}
有了这样的异常处理之后,就不用像上面这样挨个写异常了。不过上面写的异常时优先级最高的。
统一数据返回格式的优势是:
实现统一数据返回的时候,还是通过两个步骤:
supports 方法返回一个 boolean 值。true 就表示返回数据之前对数据进行重写,也就是会进入 beforeBodyWrite 方法。如果返回 false 表示对结果不进行任何处理,直接返回:
@ControllerAdvice
public class MyResponseAdvice 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("state", 1);
result.put("data", body);
result.put("msg", "");
return result;
}
}
然后我们通过 登录 和 注册 来查看是否属于统一格式:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public boolean login(HttpServletRequest request, String username, String password) {
boolean result = false;
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
if (username.equals("admin") && password.equals("admin")) {
HttpSession session = request.getSession();
session.setAttribute("userinfo", "userinfo");
return true;
}
}
return result;
}
@RequestMapping( "/reg")
public int reg() {
return 1;
}
}