Java Web 开发:Spring AOP

2691 字
13 分钟
Java Web 开发:Spring AOP

[TOC]

前言#

学习目标#

  1. 理解AOP的设计思想
  2. 掌握切入点表达式(execution、@annotation、@target)→ 找方法
  3. 掌握几种通知的执行时间
  4. 熟悉AspectJ的使用

前置知识准备#

  • IOC和DI

  • Spring生命周期的BeanPostProcessor(实现机制)

  • 动态代理Proxy(原理) (参考附录)

    • JDK动态代理、CGlib动态代理

介绍AOP#

Aspect Oriented Programming

在软件业,面向切面编程,是指通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP(面向对象编程)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容。

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP和OOP#

OOP 面向对象编程,是Object Oriented Programming的简称

OOP:通过继承来增强

AOP:通过切面来增强

AOP特点#

AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码。

Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类(委托类)织入增强代码。

思考:SpringAOP使用的是JDK动态代理还是CGlib动态代理?

全都要,如果有实现接口,使用的就是JDK动态代理,如果没有实现接口使用的就是CGlib动态代理

应用场景#

事务管理、性能监视、安全检查、缓存 、日志等 这些场景通常是多次反复出现而又相对来说比较繁琐的部分

想象一下如果每个组件都单独去实现这些系统功能?

改变这些关注点的逻辑,修改各个模块当中的实现,方法的调用就会重复出现在各个模块中组件会因为那些与自身核心业务无关的代码而变得混乱 组件会因为那些与自身核心业务无关的代码而变得混乱

AOP编程术语#

  • Target 目标类 (需要被代理的类,委托类)
  • Proxy 代理类 (动态代理生成的)
  • JoinPoint 连接点 指被代理对象里那些可能会被增强的点 (方法) 如所有方法(候选的可能被增强候选点)
    • 执行增强代码过程中可以获取信息(增强)的一个对象
  • PointCut 切入点 已经被增强的连接点。
    • AOP的目标 → 容器中的组件能够增强
    • 谁做增强(能够细化到方法级别) → Pointcut切入点圈定增强范围 做记号
    • 做什么样的增强 → Advice通知 → 指导切入点指定的方法做一个什么样的增强
  • Advice 通知(具体的增强的代码)。代理对象执行到Joinpoint所做的事情。
  • Aspect 切面 是切入点和通知的结合 (切面是一个特殊的面👉一个切入点和一个通知组成一个特殊的面)
  • weaver 织入 (植入)是指把advice应用到目标对象来创建新的代理对象的过程

AOP实现#

AOP采用动态代理的方式来实现的,使用的是JDK动态代理和CGLIB动态代理。

如果有实现接口的话,使用的是JDK动态代理;如果没有实现接口的话,使用的Cglib动态代理。

AOP实战#

  • 动态代理 image-20230413151523244#

  • SpringAOP → Spring官方文档提供的方式,但是它建议你使用另一种方式AspectJ

    image-20230413151546263
    image-20230413151546263

  • AspectJ

    动态代理#

SpringAOP#

在容器中注册3个组件:委托类组件、通知组件、代理组件(ProxyFactoryBean)

使用注解注册委托类组件和通知组件

//委托类组件
@Service
public class UserServiceImpl implements UserService{
@Override
public void sayHello() {
System.out.println("hello xiaowu");
}
}
//通知组件
@Component
public class CustomAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("正道的光");
//执行委托类的代码
Object proceed = methodInvocation.proceed();
//额外去做增强的业务
System.out.println("照在了大地上");
return proceed;
}
}

通过代理组件注册代理组件

@Bean
public ProxyFactoryBean userServiceProxy(UserService userService) {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
// 告知其委托类组件是谁
proxyFactoryBean.setTarget(userService);
// 告知其通知组件是谁 → 提供的是通知组件的名称(id)
proxyFactoryBean.setInterceptorNames("customAdvice");// beanFactory.getBean("customAdvice")
// 它里面的getObject会根据我们提供的值生成代理对象
return proxyFactoryBean;
}

单元测试要从容器中指定代理组件取出

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application.xml")
public class MyTest {
@Autowired
@Qualifier("userServiceProxy")
UserService userService;
@Test
public void mytest1(){
userService.sayHello();
}
}

★AspectJ#

切面组件

  • 增加AspectJ的注解开关 → 配置类上增加一个注解**@EnableAspectJAutoProxy**
  • 把组件标记为切面组件 → @Aspect

切入点表达式#

引入Aspectjweaver依赖

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>

在切面组件中使用**@Pointcut**

  • value属性:切入点表达式
  • 方法名:作为切入点id
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Pointcut {
String value() default "";
String argNames() default "";
}

execution#

  • 能否省略

  • 能否通配

  • 特殊用法

修饰符:可以省略,省略代表任意修饰符

返回值:不能省略,POJO类要写全类名;可以是用*作为通配符

包名、类名、方法名:使用..部分省略,头和尾不能省略,中间的任意一部分都可以省略;可以使用*作为通配符

形参:可以使用..或* 来进行通配,..代表任意数量任意类型的参数,*代表任意类型的参数;POJO类要写全类名

<!--execution 👉 aop:pointcut 👉 指定的是容器中的组件中的方法-->
<!--语法:execution(访问修饰符 返回值 包名.类名.方法名(参数列表))
1、能否省略不写
2、能否通配
3、是否有特殊用法
访问修饰符:可以省略、省略不写代表任意修饰符
返回值:不可以省略、可以使用*来通配、JavaBean要写全类名(基本类型、包装类以及String可以直接写)
包名、类名、方法名:可以部分省略,头和尾不能省略,中间的任意一部分都可以省略 👉 ..来进行省略
*来通配 com.*com.cskao*,头和尾也可以使用*作为通配符 *.cskaoyan.service.UserService.say*
参数列表:可以省略 👉 代表的是无参的方法
如果想要写多个参数,写参数的全类名 👉 第一参数是String,第二个参数是int 👉 (String,int)
可以通配:* 👉 代表的单个任意参数
.. 👉 任意参数 👉 任意数量的任意类型的参数
JavaBean要写全类名(基本类型、包装类以及String可以直接写)
-->
/**
* execution(修饰符 返回值 包名、类名、方法名(形参))
* 可以增强多个方法
* 越具体,匹配范围越小;越宽泛,匹配范围越大。
* 通配符
* - 修饰符: 可以省略不写,如果省略不写代表任意修饰符
* - 返回值: 不能省略,可以使用通配符* 。*代表任意值
* 如果是引用类型,要写全限定类名;全限定类名中也可以出现通配符*
* - 包名、类名、方法名: 可以使用*来通配,也可以使用..来通配
* 头和尾的位置不能使用.. 但是可以使用*
* - 形参: 省略不写代表无参方法
* 可以使用*来通配 → 代表单个任意类型的参数
* 也可以使用..来通配 → 代表任意参数 → 数量任意,类型也任意
* 如果是引用类型,要写全限定类名;全限定类名中也可以出现通配符*
*/
//@Pointcut("execution(public void com.cskaoyan.demo3.service.UserServiceImpl.sayHello(java.lang.String))")
//@Pointcut("execution(public * com.cskaoyan.demo3.service.UserServiceImpl.sayHello(java.lang.String))")
//@Pointcut("execution(public com.cskaoyan.demo3.bean.User com.cskaoyan.demo3.service.UserServiceImpl.query(Integer))")
//@Pointcut("execution(public com.cskaoyan.demo3.bean.* com.cskaoyan.demo3.service.UserServiceImpl.query(Integer))")
//@Pointcut("execution(public com.cskaoyan.demo3.bean.* com.cskaoyan.*.ser*.*ServiceImpl.query(Integer))")
//@Pointcut("execution(public com.cskaoyan.demo3.bean.* com..*ServiceImpl.query(Integer))")
// 增强service层 以say作为开头的方法(暂时先没管参数)
//@Pointcut("execution(* com..service.*ServiceImpl.say*(String))")
@Pointcut("execution(* com..service.*ServiceImpl.*(..))")
public void pointcut1(){}
@Pointcut("execution(public void com.cskaoyan.service.UserServiceImpl.sayHello(String))")
public void mypointcut1(){}

@annotation#

写上自定义注解的全类名

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CountTime {
}
@Pointcut("@annotation(com.cskaoyan.CountTime)")
public void mypointcut2(){}

@target#

写上自定义注解的全类名

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.Type)
public @interface CountTimeAllMethod {
}
@Pointcut("@target(com.cskaoyan.CountTimeAllMethod)")
public void mypointcut3(){}

Aspect切面#

在切面类中配置切面组件和通知方法

AspectJ的注解实现,首先要标记前面开关

然后在切面类中使用AspectJ相关的注解,表达切面、切入点、通知

/**
* 增加对应通知的方法 : 将这样的一些方法配置为对应的通知方法
* 方法 👉 通知
* before
* after
* around
* afterReturning
* afterThrowing
*/
@Component
@Aspect
public class CustomAspect {
/**
* pointcut方法
* 返回值:void
* 方法名:任意写 👉 用于作为pointcut组件的id
* 形参:不用写
* 方法体:不用写
* @Pointcut的value属性写切入点表达式
*/
@Pointcut("execution(* com.cskaoyan.service..*(..))")
public void mypointcut1(){}
@Pointcut("@annotation(com.cskaoyan.CountTime)")
public void mypointcut2(){}
@Pointcut("@annotation(com.cskaoyan.CountTimeAllMethod)")
public void mypointcut3(){}
/**
* 通知注解@Before、@After、@Around、@AfterReturning、@AfterThrowing
* value属性:
* 1、对应的切入点表达式
* 2、引用的切入点表达式的方法名
*/
/**
* before通知
* 返回值:void
* 方法名:任意去写
* 形参:joinPoint连接点 (可写可不写)
*/
//@Before("execution(* com.cskaoyan.service..*(..))")
@Before("mypointcut()")
public void before(JoinPoint joinPoint){
System.out.println("before");
}
/**
* after通知
* 返回值:void
* 方法名:任意去写
* 形参:joinPoint连接点 (可写可不写)
*/
@After("mypointcut()")
public void after(){
System.out.println("after");
}
/**
* around通知
* 返回值:Object
* 方法名:任意去写
* 形参:ProceedingJoinPoint连接点 (必须写) 👉 提供了proceed方法,执行的是委托类的代码
*/
@Around("mypointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around的before");
Object proceed = proceed = joinPoint.proceed();//该方法执行的是委托类的方法
System.out.println("around的after");
return proceed;
}
/*public Object around(ProceedingJoinPoint joinPoint){
System.out.println("around的before");
Object proceed = null;//该方法执行的是委托类的方法
try {
proceed = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("around的after");
return proceed;
}*/
/**
* afterReturning通知
* 返回值:void
* 方法名:任意去写
* 形参:Object(委托类方法的执行结果)
*/
@AfterReturning(value = "mypointcut()",returning = "result")
public void afterReturning(Object result){
System.out.println("afterReturning");
}
/**
* afterThrowing通知
* 返回值:void
* 方法名:任意去写
* 形参:Exception/Throwable(委托类方法执行过程中抛出异常)
*/
@AfterThrowing(value = "mypointcut()",throwing = "exception")
public void afterThrowing(Exception exception){//Throwable
System.out.println("afterThrowing");
System.out.println(exception.getMessage());
}
}

JoinPoint连接点#

获取增强过程中的一些值

  • Signature 方法
  • Arguments 参数
  • This 代理对象
  • Target 委托类对象

在通知方法的形参中,传入JoinPoint形参,通过JoinPoint获得对应的一些参数

@Before("mypointcut()")
public void before(JoinPoint joinPoint){
//Signature 方法的描述
//This 代理对象
//Target 委托类对象
//Arguments 参数
Signature signature = joinPoint.getSignature();
Object proxy = joinPoint.getThis();
Object target = joinPoint.getTarget();
Object[] args = joinPoint.getArgs();
System.out.println("signature:" + signature.getName());
System.out.println(proxy.getClass().getName());
System.out.println(target.getClass().getName());
System.out.println(Arrays.asList(args));
}

机制#

执行生命周期的过程可以做一个狸猫换太子

在BeanPostProcessor的方法中,可以判断是否是指定的方法(判断方法的描述、类上是否有注解、方法上是否有注解)

然后返回代理对象,那么向容器中注册的就是代理对象组件

如果取出组件,取出的也是代理组件

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Java Web 开发:Spring AOP
https://firefly-mu-weld.vercel.app/posts/13-spring-aop/
作者
Daisy
发布于
2026-06-10
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Daisy
Hello, I'm Daisy.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签

文章目录