AOP思想的体现主要分为两个方面:
我们最初对用户登录验证的实现是这样的:
package com.example.demo.controller;
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 method1(HttpServletRequest request){
//有 session 就创建,没有 session 就不会创建
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("userinfo") != null){
//说明已经登录,执行业务处理
return true;
}else {
//未登录
return false;
}
}
//执行的方法2
@RequestMapping("/m1")
public Object method2(HttpServletRequest request){
//有 session 就创建,没有 session 就不会创建
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("userinfo") != null){
//说明已经登录,执行业务处理
return true;
}else {
//未登录
return false;
}
}
//其他需要执行的方法...
}
从上述代码可以看出,每个方法的执行都有用户登录验证权限,它的缺点如下:
所以接下来我们提供一个公共的AOP方法来进行统一的用户登录权限验证.
使用SpringAOP的具体实现代码如下:
package com.example.demo.common;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
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
对象。那么我们应该如何解决呢?
针对以上问题,Spring中提供了具体的实现拦截器:HandlerInterceptor
.拦截器的实现分为两个部分:
HandlerInterceptor
接口的preHandle
(执行具体方法之前的预处理)方法.WebMvcConfigurer
的addInterceptors
方法中.具体实现如下:
实现一个UserInterceptor
用户拦截器类,在该类中实现HandlerInterceptor
接口,再重写preHandle
方法
package com.example.demo.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
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("userinfo")!=null){
return true;
}
response.setStatus(401);//返回一个404
return false;
}
}
getAttribute
方法是Object
类中的方法,用于获取对象的指定属性值,它接受一个参数,即要获取的属性的名称,并返回该属性的值,如果对象中不存在指定名称的属性,则返回null
。该方法可以用于获取对象的任意属性,包括实例变量和静态变量。
实现一个AppConfig
类用来配置,实现WebMvcConfigurer
接口,然后重写其中的addInterceptor
方法.
package com.example.demo.config;
import com.example.demo.interceptor.UserInterceptor;
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
方法接受一个参数,就是要添加的拦截器对象。可以通过该方法添加一个或多个拦截器。
我们可以写一个UserController1
测试类看一下运行结果:
package com.example.demo.controller;
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 {
@RequestMapping("/reg")
public String reg(){
return "do reg.";
}
@RequestMapping("/login")
public String login(){
return "do login.";
}
@RequestMapping("/test")
public String test(){
return "do Test.";
}
}
由于我们的拦截器中拦截了所有的请求但是除了/user/reg
/user/login
方法,所以这两个方法可以执行成功.而我们的Test方法则被拦截,返回401
状态码.
当我们想要排除所有的静态文件,静态文件包含图片文件,前端的JS和CSS等文件,这个时候我们不可能将每种格式的文件都手动进行排除,这样工作量也太大了(图片文件存在几十种格式),想要将这些文件排除掉我们可以将这个静态文件放入项目的static目录中,然后针对这个目录中的子目录(图片,css文件,js文件)进行排除。
excludePathPatterns("/image/**")//表示排除image目录下的所有图片
没有实现拦截器的时候,用户发送的请求直接被控制层接收到,进而在相应的URL中进行登录校验,这种方式代码的可维护性较低。
但是使用拦截器,用户发送的请求首先会被拦截器接收到,拦截器进行预处理,符合条件才会进一步调用Controller层的方法。
我们之前处理异常的方法就是使用try-catch
,或者是将异常抛出去给更上一层处理,这种方式处理异常的方式通常是分散在代码的各个部分中的,当应用程序出现异常时,开发需要在每个可能抛出异常的地方编写相应的异常处理代码,这样做会导致代码冗余,可读性差,并且难以维护。
而使用统一的异常处理就可以:
在Spring Boot中,可以使用@RestControllerAdvice
注解和@ExceptionHandler
注解来实现统一异常处理。这两个注解搭配使用表示的是全局异常处理,可以捕获并处理全局范围内的异常。当控制器中抛出异常时,会根据异常类型匹配对应的@ExceptionHandler
方法进行处理。
Exception
类是Java中所有异常类的父类。
@RestControllerAdvice
注解用在一个类上,表示该类是一个全局的控制器增强器,可以对所有的控制器进行统一的处理。这个注解提供了一种集中管理和统一处理全局范围内操作的方式,在引用程序中起到了很好的代码复用和统一管理的作用。@ExceptionHandler
注解,用于定义一个方法,**该方法用于处理控制器中发生的异常。**当控制器中的方法抛出异常时,@ExceptionHandler注解标记的方法将被调用来处理该异常。这样可以集中处理控制器中的异常。package com.example.demo;
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
@ResponseBody
public class MyExpectionAdvice {
@ExceptionHandler(NullPointerException.class)
public HashMap<String,Object> doNullPointerExpection(NullPointerException e){
HashMap<String,Object> result = new HashMap<>();
result.put("code",-1);
result.put("msg","空指针"+e.getMessage());
result.put("data",null);
return result;
}
}
写个UserController
测试一下:
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public int login() {
//空指针异常
Object obj = null;
System.out.println(obj.hashCode());
return 1;
}
}
此时我们就可以告诉前端异常的类型.上述代码我们处理了空指针异常,通常情况下,我们无法预测代码会抛出什么异常.所以我们可以使用所有异常的父类Expection
来处理:
//默认的异常处理
@ExceptionHandler(Exception.class)
public HashMap<String, Object> doException(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", -300);
result.put("msg", "Exception:" + e.getMessage());
result.put("data", null);
return result;
}
那么上述doException
方法也可以处理空指针异常,当上述两个处理异常的方式同时存在时,首先采用的是doNullPointerExpection
:(有子类先开始处理子类,再处理父类)
总结起来,统一数据返回格式可以提高接口的规范性、可读性和可维护性,方便异常处理,支持扩展和版本控制,并增强系统的兼容性。这些优点都有助于提高开发效率、减少错误和提升用户体验。
统一的数据返回格式可以使用@ControllerAdvice+ResponseBodyAdvice
的方式实现,实现步骤如下:
实现ResponseBodyAdvice
接口.并重写其中的方法supports
beforeBodyWrite
方法:
值得注意的是在此类中不需要加入@ResponseBody
注解,这是因为在该类中只是对返回值进行转换.
package com.example.demo.controller;
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 {
//是否执行 beforeBodyWrite 方法,true=执行,重写返回结果
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
//返回数据之前进行数据重写
//@param body :原始返回值
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// HashMap -> code,msg,data
if (body instanceof HashMap) {
return body;
}
// 重写返回结果,让其返回一个统一的数据格式
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", body);
result.put("msg", "");
return result;
}
}
写个测试方法测试一下:
@RequestMapping("/test")
public int test(){
return 666;
}
但是上述返回值有一个问题,即如果返回的类型是String类型时会报错:
测试方法:
@RequestMapping("/test1")
public String test1(){
return "dotest1";
}
要注意其返回的流程:
- 方法返回的是String
- 统一数据返回之前处理-> String Convert HashMap
- 将 HashMap 转换成 application/json 字符串给前端(接口)
显然,Exception:java.util.HashMap cannot be cast to java.lang.String
是第三步出现了问题.第三步在执行的时候会判断Body
的类型,如果是String
类型,那么执行StringHttpMessageConverter
进行类型转换;如果不是String类型,那么执行HttpMessageConverter
进行类型转换.问题就出在了将HashMap
转换成 application/json
字符串给前端(接口).针对以上问题,有两种解决方式:
StringHttpMessageConverter
去掉:import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class MyConfig implements WebMvcConfigurer {
//移除 StringHttpMessageConverter
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
}
}
String
类型,让其返回一个String
字符串,而非 HashMap
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// HashMap -> code,msg,data
if (body instanceof HashMap) {
return body;
}
// 重写返回结果,让其返回一个统一的数据格式
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", body);
result.put("msg", "");
if(body instanceof String){
// 返回一个 将对象转换成 JSON String 字符串
try {
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
return result;
}
注意在上述代码中使用import com.fasterxml.jackson.databind.ObjectMapper
包下的objectMapper
方法需要引入Maven-jackson依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
自定义拦截器的实现:使用HandlerInterceptor
接口+WebMvcConfigurer
接口实现。
统一异常的处理:使用@RestControllerAdvice
注解+@ExceptionHandler
注解实现。
统一数据返回格式:使用@ControllerAdvice
注解+ResponseBodyAdvice
接口实现。