目录
1. AOP的概念
1.1 什么是AOP?
AOP(Aspect Oriented Programming):⾯向切⾯编程,它是⼀种思想,它是对某⼀类事情的 集中处理。
1.2 什么是SpringAOP?
⽽ AOP 是⼀种思想,⽽ Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和 IoC 与 DI 类似。
1.3 为什要⽤ AOP?
在开发中,我们对于公共方法的处理分为三个阶段:
- 每个方法都去实现(初级)
- 抽取公共方法(中级)
- 采用AOP的方式,对代码进行无侵入是实现(高级)
因此,对于这种功能统⼀,且使⽤的地⽅较多的功能,在之前我们会抽取公共方法去实现,但是现在有更好的一个办法来处理这个问题,就是AOP。
也就是使⽤ AOP 可以扩充多个对象的某个能⼒,所以 AOP 可以说是 OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。
1.4 AOP的作用?
提供声明式事务;允许用户自定义切面
2. AOP的组成
AOP由下边四部分组成:
切面(Aspect) | 由切点(Pointcut)和通知(Advice)组成,既包含了横切逻辑的定义,也包含了连接点的定义。 |
连接点(Join Point) | 应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。 |
切点(Pointcut) | Pointcut的作用就是提供一组规则 来匹配Join Point,给满足规则的Join Point 添加Advice. |
通知(Advice) | 切面也有目标-他必须要完成的工作。在AOP中,切面的工作被称为通知。 |
那么是不是感觉难以理解呢?
不着急我们简单说一下, AOP使用来对某一类事情进行集中处理的,那么处理事情,这个整体就是一个切面,处理事情的范围就是切点,范围中具体的事物就是连接点,怎么处理就是通知。
简单的体系图如下:
3. 通知
通知又名拦截器,它定义了切面是做什么以及何时使用,即在某个特定的连接点上执行的动作,它是切面的具体实现。以目标方法为参照点,根据放置位置的地方不同,通知分为如下5种类型通知:
- 前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
- 后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。
- 返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
- 抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
- 环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为。
4. AOP的实现
实现步骤如下:
1. 添加 Spring AOP 框架⽀持。
2. 定义切⾯和切点。
3. 定义通知。
具体操作如下:
4.1 添加 Spring AOP 框架⽀持。
首先我们建立一个新的Springboot项目,在配置文件中添加AOP框架:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4.2 定义切面和切点。
4.3 定义通知。
定义一个切面:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class LoginAspect {
//定义切点
@Pointcut("execution(* com.example.springaopdemo.controller.UserController.*(..))")
public void pointcut(){}
//定义通知
@Before("pointcut()")
public void before(){
log.info("do before,,,,");
}
@After("pointcut()")
public void after(){
log.info("do after.....");
}
@AfterReturning("pointcut()")
public void afterReturning(){
log.info("do afterReturning....");
}
@AfterThrowing("pointcut()")
public void afterThrowing(){
log.info("do afterThrowing....");
}
//这里注意,环绕式通知必须自己写返回结果
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint){
Object oj=null;
log.info("之前");
try{
oj=joinPoint.proceed();//调用目标方法
}catch(Throwable e){
throw new RuntimeException(e);
}
log.info("之后");
return oj;
}
@Aspect是aop框架中提供的,表示是一个切面。切面类中最先定义的是切点,用@Pointcut注解表示是一个切点,括号里边是切点表达式。后边的是五大类型的通知。分别是用五大类名称进行注解。
4.3.1 切点表达式:
AspectJ ⽀持三种通配符
* | 匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数) |
.. | 匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。 |
+ | 表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的 所有⼦类包括本身 |
切点表达式由切点函数组成,execution是最常用的切点函数,语法为:
修饰符和异常可以省略。
4.3.2 AOP演示
以上AOP部分的代码就已经完成,可以实现AOP的功能了,我们简单演示一下:
定义一个类来试一下它集中处理的实现:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/login")
public String login(){
log.info("login..");
return "login...";
}
@RequestMapping("set")
public String setup(){
log.info("setup...");
return "setup....";
}
@RequestMapping("/logout")
public String logout(){
log.info("logout...");
return "logout.....";
}
}
在浏览器执行过后就会发现执行结果:(任意执行一个方法,这里执行第三个方法)
可以看到五类通知的执行顺序如上,但是还有一个AfterThrowing没有看到打印,为什么呢?
这是因为AfterThrowing是在出现异常后才会进行打印的,在diamagnetic中插入一条异常代码,我们就会看到他的打印了:
以上代码能看出来了,AOP的确实一类事情的集中处理,且处理的时候不会破坏原有的代码,且允许用户自定义切面。
5.下面应用我们的AOP,怎么进行方法耗时的获取呢?
首先我们先来看一下普通的获取实践的方法,对比一下就更体会aop的方便了。
5.1 普通方法
就是在每个方法内部去进行操作。
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/login")
public String login(){
// log.info("login..");
// return "login...";
long start=System.currentTimeMillis();
log.info("login..");
long end=System.currentTimeMillis();
log.info("login方法耗时:"+(end-start));
return "login....";
}
}
结果显示(这里环绕通知关了):
5.2 使用aop
定义一个环绕通知,在方法调用前记录,在方法调用完打印方法耗时。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) {
Object oj=null;
long start=System.currentTimeMillis();
try {
oj=joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
//获取当前方法的名称并打印
log.info(joinPoint.getSignature().toString()+"耗时:"+(System.currentTimeMillis()-start));
return oj;
}
}
结果显示:
使用aop只需写一次代码,要获取那个方法的耗时就调用就可以了,大大提高了效率,也可以明显看出aop的思想:是对某一类事情的集中处理。
注:这里的耗时可能每次获取的结果不是相同的,那是因为耗时受当前环境的影响,例如cpu,当前正在进行进程的多少。。所以不一样是正常的,并不是说结果就不正确。
6. AOP的实现原理
Spring AOP 是构建在动态代理(代理模式是什么)基础上,因此 Spring 对 AOP 的⽀持局限于⽅法级别的拦截。 Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。
6.1 织⼊(Weaving):代理的⽣成时机
织入就是什么时候把代理的代码放进运行的代码中。
在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:
- 编译期(编译阶段):切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就 是以这种⽅式织⼊切⾯的。
- 类加载期:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器 (ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载 时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
- 运⾏期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。
7. 拦截器
那么,上边所学的AOP很方便,但是上边的AOP在我们实现复杂一点的功能的时候很难处理,我们没办法获取到 HttpSession 对象,还有时候我们要对⼀部分⽅法进⾏拦截,⽽另⼀部分⽅法不拦截,如注册⽅法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。所以,引入拦截器。
7.1 拦截器实现原理
正常情况下的调用顺序
加入拦截器后的调用顺序
Spring 中的拦截器也是通过动态代理和环绕通知的思想实现的,大体流程:
7.2 拦截器代码实现
Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:
- 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的 preHandle(执⾏具体⽅法之前的预处理)⽅ 法。
- 将⾃定义拦截器加⼊ WebMvcConfigurer 的 addInterceptors ⽅法中。
自定义拦截器:(就是写什么拦截哪些内容并内容进行哪些操作)
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
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("username")!=null){
//不拦截
return true;
}
//拦截后进行的操作
response.setStatus(401);
return false;
}
}
将自定义拦截器加入到系统中:
import com.example.springaopdemo.interceptor.LoginInterceptor;
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 logConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login");
}
}
其中:
- addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意⽅法(也就是所有⽅法)。
- excludePathPatterns:表示需要排除的 URL。
说明:以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)
实现类还是我们上边的:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/login")
public Boolean login(HttpServletRequest request, String username,String password){
log.info("login....");
// if(username!=null && "".equals(username) && password!=null && "".equals((password))){
//
// }
if(!StringUtils.hasLength(username)||!StringUtils.hasLength(password)){
return false;
}
if(!"admin".equals(username)||!"admin".equals(password)){
return false;
}
HttpSession session= request.getSession(true);
session.setAttribute("username",username);
return true;
}
@RequestMapping("set")
public String setup(){
log.info("setup...");
return "setup....";
}
@RequestMapping("/logout")
public String logout(){
log.info("logout...");
return "logout.....";
}
}
8. 使用AOP实现常用的统一功能处理
8.1 ⽤户统⼀登录验证的问题
实现就是上边的拦截器的例子,但是我们可以对登录类再进行优化。
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/login")
public Boolean login(HttpServletRequest request, String username,String password){
log.info("login....");
// if(username!=null && "".equals(username) && password!=null && "".equals((password))){
//
// }
if(!StringUtils.hasLength(username)||!StringUtils.hasLength(password)){
return false;
}
if(!"admin".equals(username)||!"admin".equals(password)){
return false;
}
HttpSession session= request.getSession(true);
session.setAttribute("username",username);
return true;
}
8.2 统⼀异常处理
统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执⾏某个通知, 也就是执⾏某个⽅法事件,具体实现代码如下:
实现类:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ExceptionController {
@RequestMapping("/test1")
public boolean test1(){
int a=10/0;
return true;
}
@RequestMapping("/test2")
public boolean test2(){
String str=null;
System.out.println(str.length());
return true;
}
@RequestMapping("/test3")
public String test3(){
throw new RuntimeException("test手动抛出异常");
}
}
异常控制类:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@ResponseBody
@ControllerAdvice//控制器通知
public class ErrorHandler {
@ExceptionHandler
public Object error1(Exception e){
HashMap<String,Object> map=new HashMap<>();
map.put("success",0);
map.put("code",-1);
map.put("msg","内部异常");
return map;
}
@ExceptionHandler
public Object error2(ArithmeticException e){
HashMap<String,Object> map=new HashMap<>();
map.put("success",0);
map.put("code",2);
map.put("msg","算数异常");
return map;
}
@ExceptionHandler
public Object error3(NullPointerException e){
HashMap<String,Object> map=new HashMap<>();
map.put("success",0);
map.put("code",2);
map.put("msg","算数异常");
return map;
}
}
统一异常处理,就是在异常控制类中,类注解@ControllerAdvice和方法注解@ExceptionHandler合在一起,表示在调用的实现类发生异常的时候,异常控制类就会去匹配异常的类型,然后去执行异常控制类中对应的异常类型的操作。
异常控制类里边我们对异常进行了类型划分,匹配的时候会根据他们的类型去自动执行相应的异常中的操作,但是环境是个很重要的问题,在我们单独是实现上边的时候,都可以自动匹配到正确的异常类型并返回,但是当多个环境相叠加的时候就可能会出现匹配错误的情况了。例如当我们叠加一个环绕通知的时候,返回的异常信息就不是正确的异常信息了。
这是因为在环绕通知的时候,方法的调用在中间,后边我们会返回的异常去进行处理,所以,最后的异常结果就是环绕通知最后异常处理的结果。那么怎么解决呢?
我们在环绕通知中不对异常进行处理,再次抛出就好了。
8.3 统一数据返回
一般为了方便接口的处理,我们会返回对象,返回对象就需要统一格式,统一格式处理会包含一下几点,业务处理是否成功,成功的话执行结果是什么,失败的话失败的原因是什么。
统⼀数据返回格式的优点有很多,⽐如以下⼏个:
- ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据。
- 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就⾏了,因为所有接⼝都是这样返回 的。
- 有利于项⽬统⼀数据的维护和修改。
- 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容。
代码实现,在返回结果错误的结果处理对应上边异常处理部分,这里我们只实现成功的时候的结果怎么进行统一格式返回,具体代码如下:
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
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.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@ControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows//帮我们实现了try-catch
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String,Object> map=new HashMap<>();
map.put("success",1);
map.put("date",body);
map.put("errMsg","");
//对字符串进行特殊处理
if(body instanceof String){
ObjectMapper mapper=new ObjectMapper();
return mapper.writeValueAsString(map);
}
return map;
}
}
注意:如果数据是String类型那么SpringMVC内部就会进行匹配,用hashmap的格式去匹配String类型,就会报错,所以在是String的时候我们不需要去匹配,直接输出就好了。