Version: Next

AOP

1. 什么是AOP

AOP(Aspect Oriented Programming)是指面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各个部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

image-20200403152457862

2. AOP在Spring中的作用

提供生声明式事务;允许用户自定义切面

  • 横切关注点:横跨应用程序多个模块的方法或功能。即,与业务逻辑无关,但是需要关注的部分,就是横切关注点。如:日志、安全、缓存、事务等
  • 切面(ASPECT):横切关注带你,被模块化的特殊对象。即,它是一个(例如Log)
  • 通知(Advice):切面必须完成的工作,即,它是类中的一个方法 (例如Log中的方法)
  • 目标(Target):被通知的对象
  • 代理(Proxy):向目标对象应用通知之后创建的对象
  • 切入点(PointCut):面向通知执行的"地点"定义
  • 连接点(JointPoint):与切入点匹配的执行点

在SpringAOP中,通过Adivce定义横切逻辑,Spring中支持5中类型的Advice

通知类型连接点实现接口
前置通知方法方法前org.springframework.aop.MethodBeforeAdvice
后置通知方法后org.springframework.aop.AfterReturningAdvice
环绕通知方法前后org.aopalliance.intercept.MethodInterceptor
异常抛出通知方法抛出异常org.springframework.aop.ThrowAdvice
引介通知类中增加新方法属性org.springframework.aop.IntroductionInterceptor

3. 使用Spring实现AOP

【重点】使用AOP织入,导入依赖包

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
  • 新建maven模块 spring-09-aop

3.1 方式一: 使用Spring的API接口

  • Log类

    • method:要执行的目标对象的方法
    • object:参数args
    • o:目标对象
    public class Log implements MethodBeforeAdvice {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
    System.out.println("前置日志");
    }
    }
  • AfterLog类

    public class AfterLog implements AfterReturningAdvice {
    public void afterReturning(Object returnValue, Method method, Object[] args,
    Object target) throws Throwable {
    System.out.println("执行了" + method.getName() + "返回了" + returnValue);
    }
    }

把上面两个日志类注册到Spring里

  • 新建applicationContext.xml

  • 在里面注册两个日志类和UserServiceImpl

  • 配置aop

    • 切入点:在什么地方执行
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/aop
    https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id = "userService" class="com.bsx.service.UserServiceImpl"/>
    <bean id = "log" class="com.bsx.log.Log"/>
    <bean id = "afterLog" class="com.bsx.log.AfterLog"/>
    <!--方式一:使用原生Spring API接口-->
    <!-- 配置aop-->
    <aop:config>
    <!-- 切入点 表达式 execution(要执行的位置)-->
    <!-- 切入 UserServiceImpl中所有的方法,匹配所有的参数-->
    <aop:pointcut id="pointCut"
    expression="execution(* com.bsx.service.UserServiceImpl.*(..))"/>
    <!-- 执行环绕增加,把log类切入到切入点表达式里写的方法上-->
    <aop:advisor advice-ref="log" pointcut-ref="pointCut"/>
    <aop:advisor advice-ref="afterLog" pointcut-ref="pointCut" />
    </aop:config>
    </beans>

测试

@Test
public void testAOP1(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//动态代理返回的是接口
UserService userService = context.getBean("userService", UserService.class);
userService.add();
userService.delete();
userService.update();
userService.query();
}
  • 结果

    com.bsx.service.UserServiceImpl的add被执行了
    增加了一个用户
    执行了add返回了null
    com.bsx.service.UserServiceImpl的delete被执行了
    删除了一个用户
    执行了delete返回了null
    com.bsx.service.UserServiceImpl的update被执行了
    更新了一个用户
    执行了update返回了null
    com.bsx.service.UserServiceImpl的query被执行了
    查询了一个用户
    执行了query返回了null

3.2 使用自定义类实现AOP

  • 自定义切入点类

    /***
    * 自定义切入点类
    */
    public class DiyPointCut {
    public void before(){
    System.out.println("-------方法执行前-------");
    }
    public void after(){
    System.out.println("-------方法执行后-------");
    }
    }
  • applicationContext.xml

    <!-- 方式二 : 自定义类,给它注册bean-->
    <bean id="diy" class="com.bsx.diy.DiyPointCut"/>
    <aop:config>
    <!-- 自定义切面,ref为要引用的类-->
    <aop:aspect ref="diy" >
    <!-- 切入点-->
    <aop:pointcut id="point"
    expression="execution(* com.bsx.service.UserServiceImpl.*(..))"/>
    <!-- 通知,类中的一个方法-->
    <aop:before method="before" pointcut-ref="point"/>
    <aop:after method="after" pointcut-ref="point"/>
    </aop:aspect>
    </aop:config>
  • 测试类不变

tip

方式一比方式二功能更强大

3.3 注解实现AOP

  • 切面类 用@Aspect标注一个类为切面

    @Aspect
    public class AnnotationPointCut {
    @Before("execution(* com.bsx.service.UserServiceImpl.*(..))")
    public void before() {
    System.out.println("------方法执行前------");
    }
    @After("execution(* com.bsx.service.UserServiceImpl.*(..))")
    public void after() {
    System.out.println("------方法执行后------");
    }
    @Around("execution(* com.bsx.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("环绕前");
    System.out.println("即将执行方法:" + joinPoint.getSignature() + "参数:" + joinPoint.getArgs());
    joinPoint.proceed();
    System.out.println("环绕后");
    }
    }
  • applicationContext.xml

    <bean id="annotationPointCut" class="com.bsx.diy.AnnotationPointCut"/>
    <!-- 开启注解-->
    <aop:aspectj-autoproxy/>

4. AOP的实际应用

4.1 全局登录验证

在使用网站时,我们经常在使用一项功能时发现页面跳转回登录页面要求我们登录,如何将这个功能在任意需要用户登录的功能上进行实现?难道要在每一个功能前加上登录验证? 使用AOP我们可以定义一次登录验证,然后让它切入到所有需要用户登录才能使用的功能前

@Around("execution(* com.bsx.controller.TopicController.*(..)) || execution(* com" +
".bsx.controller.WeiboController.*(..))")
public ModelAndView loginRequired(ProceedingJoinPoint joint) throws Throwable {
// 在 TodoController 下面所有方法被执行的时候调用
// @Around 能在执行方法之前和之后处理。由 loginRequired 决定什么时候调用 controller 的方法。
// execution 匹配方法执行。Aspect 功能非常多,除了能在方法执行的时候插入,还能在其他地方插入。
// * com.bsx.controller.TodoController.*(..)
// 第一个 *,匹配任意的方法返回值
// 第二个 *,匹配 TodoController 下的任意方法,可以把 * 换成具体方法名。这里有自动补全。
// (..) 匹配任意参数签名
// 简写 @Around("within(com.bsx.controller.TodoController)")
// ProceedingJoinPoint 正在被调用的方法
// 返回值类型要和被处理的控制器方法类型一样。所以 TodoController 的所有方法返回值都要是 ModelAndView
Utility.log("loginRequired 正在访问的 url %s", request.getRequestURI());
Utility.log("loginRequired 正在执行的方法 %s %s", joint.getSignature(), joint.getArgs());
Integer userID = (Integer) request.getSession().getAttribute("user_id");
if (userID == null) {
// 跳转回登陆页面
Utility.log("loginRequired 没有 session");
return new ModelAndView("redirect:/login");
} else {
UserModel u = userService.findById(userID);
if (u == null && u.getRole().equals(UserRole.guest)) {
// 跳转回登陆页面
Utility.log("loginRequired 用户不存在 %s", userID);
return new ModelAndView("redirect:/login");
} else {
// 执行被切入的方法
return (ModelAndView) joint.proceed();
}
}
}

4.2 全局所有者验证

类似的,有些功能需要验证所有者,比如我们在贴吧上发了贴,自己有权限进行修改,别人只能看不能修改,这就需要在修改更能前进行所有者验证

@Around("execution(* com.bsx.controller.TopicController.edit(..)) || execution(* " +
"com.bsx.controller.TopicController.delete(..)) || execution(* com.bsx.controller.TopicController.edit(..)) ")
public ModelAndView ownerRequird(ProceedingJoinPoint joint) throws Throwable {
// 在 TodoController 下面所有方法被执行的时候调用
// @Around 能在执行方法之前和之后处理。由 loginRequired 决定什么时候调用 controller 的方法。
// execution 匹配方法执行。Aspect 功能非常多,除了能在方法执行的时候插入,还能在其他地方插入。
// * com.bsx.controller.TodoController.*(..)
// 第一个 *,匹配任意的方法返回值
// 第二个 *,匹配 TodoController 下的任意方法,可以把 * 换成具体方法名。这里有自动补全。
// (..) 匹配任意参数签名
// 简写 @Around("within(com.bsx.controller.TodoController)")
// ProceedingJoinPoint 正在被调用的方法
// 返回值类型要和被处理的控制器方法类型一样。所以 TodoController 的所有方法返回值都要是 ModelAndView
Utility.log("ownerRequird 正在访问的 url %s", request.getRequestURI());
Utility.log("ownerRequird 正在执行的方法 %s %s", joint.getSignature(), joint.getArgs());
Integer userID = (Integer) request.getSession().getAttribute("user_id");
Integer topicId = Integer.valueOf(request.getParameter("id"));
if (userID == null) {
// 跳转回登陆页面
Utility.log("loginRequired 没有 session");
return new ModelAndView("redirect:/login");
} else {
UserModel u = userService.findById(userID);
Utility.log("user after find", u);
if (u == null && u.getRole().equals(UserRole.guest)) {
// 跳转回登陆页面
Utility.log("loginRequired 用户不存在 %s", userID);
return new ModelAndView("redirect:/login");
} else {
TopicModel topic = topicService.findById(topicId);
Utility.log("topic before route: %s", topic);
Utility.log("user before route: %s", u);
if (u.getId().equals(topic.getUserId())) {
return (ModelAndView) joint.proceed();
} else {
return new ModelAndView("redirect:/login");
}
}
}
}

5. AOP 的全部通知顺序 | SpringBoot 1 和 SpringBoot 2 对 AOP 执行顺序的影响

从 Spring4 到 Spring5 版本升级,会遇到的坑

Spring4

正常异常
调用前一行 -> @Before -> 调用 -> 调用后一行 -> @After @AfterReturning调用前一行 -> @Before -> 调用 -> @After @AfterThrowing

Spring5

正常异常
调用前一行 -> @Before -> 调用 -> @AfterReturning -> @After -> 调用后一行调用前一行 -> @Before -> 调用 -> @AfterThrowing -> @After