学习springAOP

第三章 Spring AOP

第一节 AOP 简介

1. 概念

AOP全称为Aspect Oriented Programming,表示面向切面编程。何为切面呢?

由此可以得出,切面是一种将那些与业务无关,但业务模块都需要使用的功能封装起来的技术。这样便于减少系统的重复代码,降低模块之间的耦合度。

2. AOP 基本术语

  • 连接点( Joinpoint ):

    连接点就是被拦截到的程序执行点,因为Spring只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法。连接点由两个信息确定:

    • 方法( 表示程序执行点,即在哪个目标方法)
    • 相对点(表示方位,即目标方法的什么位置,比如调用前,后等)
  • 切入点(Pointcut):

    切入点是对连接点进行拦截的条件定义。切入点表达式如何和连接点匹配是AOP的核心,Spring缺省使用AspectJ切入点语法。 一般认为,所有的方法都可以认为是连接点,但是我们并不希望在所有的方法上都添加通知,而切入点的作用就是提供一组规则来匹配连接点,给满足规则的连接点添加通知。

  • 通知、增强(Advice):

    可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知、最终通知等。

  • 目标对象(Target):

    目标对象指将要被增强的对象,即包含主业务逻辑的类对象。或者说是被一个或者多个切面所通知的对象。

  • 织入(Weaving):

    织入是将切面和业务逻辑对象连接起来, 并创建通知代理的过程。织入可以在编译时,类加载时和运行时完成。在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理

  • 代理(Proxy):

    被AOP织入通知后,产生的结果类。

  • 切面(Aspect):

    切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。切面由切入点和通知组成,它既包含了横切逻辑的定义,也包括了切入点的定义。 Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。

第二节 AOP 应用

AOP应用场景有许多,最典型的应用场景就是日志和事务。这里以事务实现为例进行讲解。

xml 复制代码
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.11</version>
</dependency>
<!-- 切面相关的包 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.9</version>
</dependency>

1. 编写业务层

java 复制代码
public interface UserService {

    int saveUser(Map<String,Object> params);
}

public class UserServiceImpl implements UserService {

    @Override
    public int saveUser(Map<String, Object> params) {
        System.out.println("保存用户信息" + params);
        return 0;
    }
}

2. 配置业务层

AOP 功能的实现是基于 IOC 的,因此,业务层对象应该纳入 IOC 容器来管理。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!-- xmlns = xml namespace-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 业务层对象-->
    <bean id="userService" class="com.qf.spring.aop.service.impl.UserServiceImpl" />
</beans>

3. 编写通知类

通知分为前置通知、后置通知、异常抛出通知、环绕通知、最终通知五种。首先实现前置通知。

前置通知接口

java 复制代码
public interface MethodBeforeAdvice extends BeforeAdvice {

	/**
	 * Callback before a given method is invoked.
	 */
	void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}
java 复制代码
public class BeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        String methodName = method.getName();
        String className = method.getDeclaringClass().getName();
        System.out.println("准备执行方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args));
    }
}

4. 配置通知

通知的实现也是基于 IOC 容器的,因此需要将通知对象纳入 IOC 容器管理。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!-- xmlns = xml namespace-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 业务层对象-->
    <bean id="userService" class="com.qf.spring.aop.service.impl.UserServiceImpl" />
    <!--配置通知对象-->
    <bean id="before" class="com.qf.spring.aop.advice.BeforeAdvice" />
</beans>

5. AOP 配置

当通知对象和业务层对象都纳入 IOC 容器管理之后,需要将通知对象作用在业务层对象上。Spring 提供了 aop 标签来完成这一功能。

xml 复制代码
<aop:config>
    <!--
            pointcut表示切点,也就是通知会在哪些位置触发
            expression表示切点表达式,切点表达式必须是execution(), execution()方法中的参数必须配置到方法上
            比如 * com.qf.spring.aop.service..*(..)
            第一个 * 表示任意访问修饰符
            com.qf.spring.aop.service.. 最后的两个..表示service包下面的所有子包中的类
            *(..) 表示任意方法, 如果()中没有..,则表示不带参数的方法;有,就表示带任意参数
        -->
    <aop:pointcut id="切点ID" expression="切点表达式"/>
    <aop:advisor advice-ref="通知ID" pointcut-ref="切点ID" />
</aop:config>

要使用 aop 标签,必须要在 beans 标签上添加 aop 命名空间

xml 复制代码
xmlns:aop="http://www.springframework.org/schema/aop"

然后在 xsi:schemaLocation 属性值中添加 aop 命名空间和约束文档

xml 复制代码
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
xml 复制代码
<aop:config>
    <aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/>
    <aop:advisor advice-ref="before" pointcut-ref="pc" />
</aop:config>

6. 测试

java 复制代码
public class AopTest {

    @Test
    public void saveUserTest(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = context.getBean("userService", UserService.class);
        Map<String,Object> params = new HashMap<>();
        params.put("name", "刘德华");
        params.put("sex", "男");
        userService.saveUser(params);
    }
}

7. 后置通知

后置通知接口

java 复制代码
public interface AfterReturningAdvice extends AfterAdvice {
	/**
	 * Callback after a given method successfully returned.
	 */
	void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;
}
java 复制代码
public class AfterAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        String methodName = method.getName();
        String className = method.getDeclaringClass().getName();
        System.out.println("执行完方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",得到返回值:" + returnValue);
    }
}
xml 复制代码
<bean id="after" class="com.qf.spring.aop.advice.AfterAdvice" />
<aop:config>
    <!--
            pointcut表示切点,也就是通知会在哪些位置触发
            expression表示切点表达式,切点表达式必须是execution(), execution()方法中的参数必须配置到方法上
            比如 * com.qf.spring.aop.service..*(..)
            第一个 * 表示任意访问修饰符
            com.qf.spring.aop.service.. 最后的两个..表示service包下面的所有子包中的类
            *(..) 表示任意方法, 如果()中没有..,则表示不带参数的方法;有,就表示带任意参数
        -->
    <aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/>
    <aop:advisor advice-ref="before" pointcut-ref="pc" />
    <aop:advisor advice-ref="after" pointcut-ref="pc" />
</aop:config>

8. 异常抛出通知

异常抛出通知接口

java 复制代码
/**
 * There are not any methods on this interface, as methods are invoked by
 * reflection. Implementing classes must implement methods of the form:
 * void afterThrowing([Method, args, target], ThrowableSubclass);
 * public void afterThrowing(Exception ex)</pre>
 * public void afterThrowing(RemoteException)</pre>
 * public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
 * public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
 */
public interface ThrowsAdvice extends AfterAdvice {
}
java 复制代码
public class ExceptionAdvice implements ThrowsAdvice {

    public void afterThrowing(Method method, Object[] args, Object target, Exception ex){
        String methodName = method.getName();
        String className = method.getDeclaringClass().getName();
        System.out.println("执行方法时:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",发生了异常:" + ex.getMessage());
    }
}

public class UserServiceImpl implements UserService {

    @Override
    public int saveUser(Map<String, Object> params) {
        System.out.println("保存用户信息" + params);
        throw new RuntimeException("异常抛出演示");
    }
}
xml 复制代码
<bean id="exception" class="com.qf.spring.aop.advice.ExceptionAdvice" />
<aop:config>
    <!--
            pointcut表示切点,也就是通知会在哪些位置触发
            expression表示切点表达式,切点表达式必须是execution(), execution()方法中的参数必须配置到方法上
            比如 * com.qf.spring.aop.service..*(..)
            第一个 * 表示任意访问修饰符
            com.qf.spring.aop.service.. 最后的两个..表示service包下面的所有子包中的类
            *(..) 表示任意方法, 如果()中没有..,则表示不带参数的方法;有,就表示带任意参数
        -->
    <aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/>
    <aop:advisor advice-ref="before" pointcut-ref="pc" />
    <aop:advisor advice-ref="after" pointcut-ref="pc" />
    <aop:advisor advice-ref="exception" pointcut-ref="pc" />
</aop:config>

9. 环绕通知

环绕通知接口

java 复制代码
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {

	/**
	 * Implement this method to perform extra treatments before and
	 * after the invocation.
	 */
	@Nullable
	Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
java 复制代码
public class AroundAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod(); //获取被拦截的方法对象
        Object[] args = invocation.getArguments();//获取方法的参数
        Object target = invocation.getThis(); //获取代理对象
        String methodName = method.getName();
        String className = method.getDeclaringClass().getName();
        try {
            System.out.println("准备执行方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args));
            Object returnValue = method.invoke(target, args);
            System.out.println("执行完方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",得到返回值:" + returnValue);
            return returnValue;
        } catch (Throwable t){
            System.out.println("执行方法时:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",发生了异常:" + t.getMessage());
            throw t;
        }
    }
}
xml 复制代码
<!--配置通知对象-->
<!--  <bean id="before" class="com.qf.spring.aop.advice.BeforeAdvice" />
    <bean id="after" class="com.qf.spring.aop.advice.AfterAdvice" />
    <bean id="exception" class="com.qf.spring.aop.advice.ExceptionAdvice" />-->
<bean id="around" class="com.qf.spring.aop.advice.AroundAdvice" />
<aop:config>
    <!--
            pointcut表示切点,也就是通知会在哪些位置触发
            expression表示切点表达式,切点表达式必须是execution(), execution()方法中的参数必须配置到方法上
            比如 * com.qf.spring.aop.service..*(..)
            第一个 * 表示任意访问修饰符
            com.qf.spring.aop.service.. 最后的两个..表示service包下面的所有子包中的类
            *(..) 表示任意方法, 如果()中没有..,则表示不带参数的方法;有,就表示带任意参数
        -->
    <aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/>
    <!--   <aop:advisor advice-ref="before" pointcut-ref="pc" />
        <aop:advisor advice-ref="after" pointcut-ref="pc" />
        <aop:advisor advice-ref="exception" pointcut-ref="pc" />-->
    <aop:advisor advice-ref="around" pointcut-ref="pc" />
</aop:config>

第三节 AspectJ

1. AspectJ 简介

AspectJ是一个面向切面的框架,它扩展了Java语言,定义了AOP 语法,能够在编译期提供代码的织入。Spring通过集成AspectJ实现了以注解的方式定义增强类,大大减少了配置文件中的工作量

2. AspectJ 注解

  • @Aspect 切面标识
  • @Pointcut 切入点
  • @Before 前置通知
  • @AfterReturning 后置通知
  • @Around 环绕通知
  • @AfterThrowing 异常抛出通知

3. AspectJ 应用

3.1 通知类编写
java 复制代码
@Aspect
public class AspectJAdvice {

    @Before(value = "execution(* com.qf.spring.aop.service..*(..))")
    public void before(JoinPoint jp){
        Object[] args = jp.getArgs(); //获取方法参数
        Signature signature = jp.getSignature(); //获取签名
        if(signature instanceof MethodSignature){ //如果签名是方法签名
            Method method = ((MethodSignature) signature).getMethod(); //获取方法
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();
            System.out.println("准备执行方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args));
        }
    }

    @AfterReturning(value = "execution(* com.qf.spring.aop.service..*(..))", returning = "returnValue")
    public void after(JoinPoint jp, Object returnValue){
        Object[] args = jp.getArgs(); //获取方法参数
        Signature signature = jp.getSignature(); //获取签名
        if(signature instanceof MethodSignature){ //如果签名是方法签名
            Method method = ((MethodSignature) signature).getMethod(); //获取方法
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();
            System.out.println("执行完方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",得到返回值:" + returnValue);
        }
    }

    @AfterThrowing(value = "execution(* com.qf.spring.aop.service..*(..))", throwing = "t")
    public void exception(JoinPoint jp, Throwable t){
        Object[] args = jp.getArgs(); //获取方法参数
        Signature signature = jp.getSignature(); //获取签名
        if(signature instanceof MethodSignature){ //如果签名是方法签名
            Method method = ((MethodSignature) signature).getMethod(); //获取方法
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();
            System.out.println("执行方法时:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",发生了异常:" + t.getMessage());
        }
    }

    @Around("execution(* com.qf.spring.aop.service..*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();//获取方法的参数
        Object target = pjp.getTarget(); //获取代理对象
        Signature signature = pjp.getSignature(); //获取签名
        if(signature instanceof MethodSignature) { //如果签名是方法签名
            Method method = ((MethodSignature) signature).getMethod(); //获取被拦截的方法对象
            String methodName = method.getName();
            String className = method.getDeclaringClass().getName();
            try {
                System.out.println("准备执行方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args));
                Object returnValue = method.invoke(target, args);
                System.out.println("执行完方法:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",得到返回值:" + returnValue);
                return returnValue;
            } catch (Throwable t){
                System.out.println("执行方法时:" + className + "." + methodName + ",参数:" + Arrays.toString(args) + ",发生了异常:" + t.getMessage());
                throw t;
            }
        }
        return null;
    }
}
3.2 启用注解支持
xml 复制代码
<bean  class="com.qf.spring.aop.advice.AspectJAdvice" />
<!--配置通知对象-->
<!--  <bean id="before" class="com.qf.spring.aop.advice.BeforeAdvice" />
    <bean id="after" class="com.qf.spring.aop.advice.AfterAdvice" />
    <bean id="exception" class="com.qf.spring.aop.advice.ExceptionAdvice" />-->
<!--    <bean id="around" class="com.qf.spring.aop.advice.AroundAdvice" />-->
<!--<aop:config>
        &lt;!&ndash;
            pointcut表示切点,也就是通知会在哪些位置触发
            expression表示切点表达式,切点表达式必须是execution(), execution()方法中的参数必须配置到方法上
            比如 * com.qf.spring.aop.service..*(..)
            第一个 * 表示任意访问修饰符
            com.qf.spring.aop.service.. 最后的两个..表示service包下面的所有子包中的类
            *(..) 表示任意方法, 如果()中没有..,则表示不带参数的方法;有,就表示带任意参数
        &ndash;&gt;
        <aop:pointcut id="pc" expression="execution(* com.qf.spring.aop.service..*(..))"/>
     &lt;!&ndash;   <aop:advisor advice-ref="before" pointcut-ref="pc" />
        <aop:advisor advice-ref="after" pointcut-ref="pc" />
        <aop:advisor advice-ref="exception" pointcut-ref="pc" />&ndash;&gt;
        <aop:advisor advice-ref="around" pointcut-ref="pc" />
    </aop:config>-->

<!-- 启动AspectJ 注解  自动为类生成代理-->
<aop:aspectj-autoproxy proxy-target-class="true" />

第四节 代理模式

代理模式一共分为两种: 静态代理和动态代理

1. 静态代理

静态代理模式由三个部分构成:

  • 一个公共的接口

  • 一个代理角色

  • 一个被代理角色

其中代理角色和被代理角色均需要实现公共接口。

java 复制代码
/**
 * 人类接口,描述人类购物
 */
public interface Person {
    //购物
    void shopping();
}
/**
 * 被代理的角色
 */
public class ZhangSan implements Person{

    @Override
    public void shopping() {
        System.out.println("购物");
    }
}
/**
 * 代理角色
 */
public class PersonProxy implements Person{

    private ZhangSan zhangSan; //维护一个被代理的角色

    public PersonProxy(ZhangSan zhangSan) {
        this.zhangSan = zhangSan;
    }

    @Override
    public void shopping() {
        System.out.println("代理人准备代理购物");
        zhangSan.shopping();
        System.out.println("代理人代理购物完毕");
    }
}
/**
 * 静态代理测试
 */
public class StaticProxyTest {

    public static void main(String[] args) {
        Person p = new PersonProxy(new ZhangSan());
        p.shopping();
    }
}

思考:如果有多人想要代理购物,那么像PersonProxy这样的类就需要写多次;从编程的角度出发,这显然不合理。应该如何解决?

使用泛型进行解决。

java 复制代码
public class StaticProxy<T> implements Person {

    private T t;

    public StaticProxy(T t) {
        this.t = t;
    }

    @Override
    public void shopping() {
        //判断当前对象是否继承或者实现了Person接口
        if(!Person.class.isAssignableFrom(t.getClass()))
            throw new RuntimeException(t.getClass().getName() + "没有实现Person接口");
        System.out.println("代理人准备代理购物");
        ((Person)t).shopping();
        System.out.println("代理人代理购物完毕");
    }
}

/**
 * 静态代理测试
 */
public class StaticProxyTest {

    public static void main(String[] args) {
//        Person p = new PersonProxy(new ZhangSan());
//        p.shopping();
        Person p = new StaticProxy<>(new ZhangSan());
        p.shopping();
    }
}

思考:如果有多个接口想要代理,应该如何解决?

使用动态代理。

2. 动态代理

动态代理也分为两种:JDK 动态代理 和 CGLIB 动态代理

2.1 JDK 动态代理

JDK 动态代理是基于接口实现的,因此只能为实现了接口的类做代理。其实现原理是:在内存中生成一个代理类继承与 Proxy, 同时实现代理接口,然后在内存中进行编译,编译后使用类加载器将该代理类加载进来,从而创建一个代理对象。

java 复制代码
public class DynamicProxyTest {

    public static void main(String[] args) {
        Person p = new ZhangSan();
        Class<?> clazz = Person.class;
        Class[] interfaces = { clazz };
        ClassLoader loader = clazz.getClassLoader();
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("准备进行代理购物");
                return method.invoke(p, args);
            }
        };
        Person proxy = (Person) Proxy.newProxyInstance(loader, interfaces, handler);
        proxy.shopping();
    }
}
2.2 CGLIB 动态代理

CGLIB 动态代理是基于继承实现的,因此可以为任何类做代理。如果一个类实现了接口,通常会使用 JDK 动态代理,但也可以强制使用 CGLIB 动态代理。

java 复制代码
public class CgLibProxy {

    public static void main(String[] args) {
        final Person p = new ZhangSan();
        //创建字节码增强对象
        Enhancer enhancer = new Enhancer();
        //设置父类,需要继承
        enhancer.setSuperclass(p.getClass());
        //需要注意:这里的InvocationHandler是CGLIB提供的net.sf.cglib.proxy.InvocationHandler
        enhancer.setCallback(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
                System.out.println("准备进行代理购物");
                return method.invoke(p, params);
            }
        });
        //创建动态代理实例
        Person proxy = (Person) enhancer.create();
        proxy.shopping();
    }
}

Spring CGLIB

java 复制代码
public class SpringCgLibProxy {

    public static void main(String[] args) {
        final Person p = new ZhangSan();
        //创建字节码曾强对象
        Enhancer enhancer = new Enhancer();
        //设置父类,需要继承
        enhancer.setSuperclass(p.getClass());
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] params, MethodProxy methodProxy) throws Throwable {
                System.out.println("准备进行代理购物");
                return method.invoke(p, params);
            }
        });
        //创建动态代理实例
        Person proxy = (Person) enhancer.create();
        proxy.shopping();
    }
}
2.3 区别

JDK 动态代理只能为实现了接口的类做代理, CGLIB 动态代理能够为所有的类做代理。JDK 动态代理的创建代理从效率上来说要比 CGLIB 动态代理快。Cglib不能对声明final的方法进行代理,因为final关键字修饰的方法不可被重写。

相关推荐
西岸行者12 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意12 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码13 天前
嵌入式学习路线
学习
毛小茛13 天前
计算机系统概论——校验码
学习
babe小鑫13 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms13 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下13 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。13 天前
2026.2.25监控学习
学习
im_AMBER13 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J13 天前
从“Hello World“ 开始 C++
c语言·c++·学习