【Spring】 AOP简谈

一、代理模式

在说AOP之前,首先绕个弯路去谈一谈代理模式。代理模式分为静态代理和动态代理,网上经常用租房子这件事来形象化代理模式这件事,这里也画一个简单的关系图。
在这里插入图片描述

角色分析:

  • 抽象角色:共同完成的一件事,比如租房这件事,一般使用接口和抽象类来解决
  • 真实角色:被代理的角色,一般是个具体的类
  • 代理角色:代理真实角色,但是一般代理角色代理真实角色后,会增加一些附属操作
  • 客户(租客):访问代理角色的人

1、静态代理

  1. 代码实例:
// 抽象角色 ==> 租房的行为
public interface Rent {
    void rent();
}
// 真实角色 ==> 房东,需要被中介代理,常见的场景是原有的,或者纯粹的业务代码
public class Host implements Rent {
    @Override
    public void rent() {
        System.out.println("房东要租房子了");
    }
}
// 代理角色 ==> 代理真实角色,代理了真实角色之后,一般会有附属的操作(看房子,收中介费)
// 比如在业务代码之前增加操作日志
public class HostProxy implements Rent {

    private Host host;

    public HostProxy() {

    }

    public HostProxy(Host host) {
        this.host = host;
    }

    @Override
    public void rent() {
        // 代理角色的附属操作1
        this.beforeRent();

        this.host.rent();
        
        // 代理角色的附属操作2
        this.afterRent();
    }

    private void beforeRent() {
        System.out.println("带租客看房子");
    }

    private void afterRent() {
        System.out.println("收房租");
    }
}
// 租客 ==> 访问代理角色的人
// 实际的业务场景:对直接的老代码进行封装,在不改变直接代码的前提下,增加一些新的代码逻辑
public class Client {
    public static void main(String[] args) {
        Host host = new Host();
        // 代理
        HostProxy hostProxy = new HostProxy(host);
        // 租客不需要直接面对房东,直接找中介租房即可
        hostProxy.rent();
    }
}
  1. 代理模式的好处:
  • 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务
  • 公共的操作交给了代理角色,实现了业务分工
  • 公共业务在扩展的时候,方便集中管理
  1. 缺点
  • 一个代理角色只能代理一个真实角色

2、动态代理

  1. 在静态代理中一个代理角色只能代理一个真实角色,这其实就大大增加了代理的成本。而动态代理的出现就是为了解决上述这个问题,动态代理的代理对象是动态生成的,而不是一个具体的对象。

动态代理分为两大类:

  • 基于接口的动态代理 – JDK的动态代理
  • 基于类的动态代理 – cglib
  1. 动态代理中两个关键: Proxy类 :代理/ InvocationHandler接口:调用处理程序

    InvocationHandler --> 调用处理程度并返回一个结果

    Proxy --> 生成动态代理实例

在这里插入图片描述

不管代理被调用的是何种方法,处理器被调用的一定是invoke()方法,具体的工作方式如下:

(1)利用Proxy生产代理对象

(2)假如proxyrent()方法被调用,假如rent()方法有一个参数

(3)proxy紧接着会调用InvocationHandlerinvoke()方法

(4)handler会根据反射,去调用RealSubject中的具体方法

在这里插入图片描述
在这里插入图片描述

  1. 上具体的代码

    // 代理接口
    public interface Rent {
        void rent(String s);
    }
    
// 真实对象,需要被代理的具体对象
public class Host implements Rent {
    @Override
    public void rent(String s) {
        System.out.println("房东要租房子了");
        System.out.println(s);
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 动态代理的生产类
// 这段代码基本上固定的动态代码生成模板,是不用改造的
public class ProxyInvocationHandler implements InvocationHandler {
    // 要代理的对象
    private Object target;

    // 利用Proxy的静态方法产生代理对象
    public Object getProxy() {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    public void setRent(Rent rent){
        this.target = rent;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log(method.getName());
        Object result = method.invoke(target, args);
        return result;
    }

    private void log(String msg) {
        System.out.println("执行了" + msg + "方法");
    }
}
// 实际要调用的对象
public class Client {
    public static void main(String[] args) {
        // 真实角色
        Host host = new Host();
        // 代理角色
        ProxyInvocationHandler pih = new ProxyInvocationHandler();

        // 通过调用程序处理角色来处理我们要调用的接口对象
        pih.setRent(host);

        Rent proxy = (Rent)pih.getProxy();
        proxy.rent("租金为3k");
    }
}

二、进入AOP

讲完了代理模式之后,就可以更好的来看看AOP是怎么回事,AOP的本质就是利用动态代理来实现对整个spring代码的无侵入式的修改增强,把和主业务无关的代码逻辑完全剥离出去了。
在这里插入图片描述

1.AOP在spring中的作用

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

下面列出一些关键词,帮助理解,记不住这个术语也无关紧要

  • 横切关注点:跨越程序多个模块的方法或功能。也就是和业务无关的,但是需要关注的部分。比如操作日志,参数校验
  • 切面:横切关注点被模块化的特殊对象,是一个类。比如LOG
  • 通知:切面要完成的工作,就是要执行的方法
  • 目标:要通知的对象
  • 切入点:切面通知要执行的位置

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

  • 前置通知【MethodBeforeAdvice】:before advice 在方法执行前执行
  • 后置通知【AfterReturningAdvice】:after returning advice 在方法执行后返回一个结果后执行。
  • 异常抛出通知【AfterThrowing】:after throwing advice 在方法执行过程中抛出异常的时候执行。
  • 环绕通知【AspectJAroundAdvice】:around advice 在方法执行前后和抛出异常时执行,相当于综合了以上三种通知。

2.使用spring实现AOP

使用AOP织入,需要导入的依赖包:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.9.4</version>
</dependency>
2.1方式一: 使用springAPI接口

API定义了每种类型的Advice接口,通过实现不同的Advice接口来实现不同的通知方式,下面的例子中使用了前置通知和后置通知

  1. 配置AOP,需要在spring启动的时候扫描导入AOP的配置文件

    (1)方式一的AOP配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <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
               http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
               http://www.springframework.org/schema/aop
               http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
    
        <!--方式一:使用spring的API接口-->
        <!-- 配置AOP: 需要导入AOP约束-->
        <aop:config>
            <!--切入点-->
            <!--id:在下面的配置中使用定位切入点-->
            <!--expression: 表达式,指定切入点作为的具体位置,具体表达式语法可google-->
            <aop:pointcut id="pointcut" expression="execution(* com.example.demo.service.aopservice..*(..))"/>
    
            <!--指定切入方式-->
            <!--advice-ref 具体切入的方法,取值必须是spring管理的bean. 比如这里的 beforeLog, afterLog-->
            <!--pointcut-ref: 哪个切入点,对应aop:pointcut 的id-->
            <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
            <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
        </aop:config>
    </beans>
    

    (2)spring启动时导入配置文件

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

@Configuration
@ComponentScan("com.example")
@ImportResource(locations = {"classpath:aopLogAspect.xml"})
public class AppConfig {
}
  1. 定义具体的处理器(这里以前置处理器和后置处理器为例),需要注意的是,这里定义的处理器是要交给spring管理的bean

    import org.springframework.aop.MethodBeforeAdvice;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    @Component
    public class BeforeLog implements MethodBeforeAdvice {
    
        /**
         * before 前置执行
         *
         * @param method 要执行的目标对象的方法
         * @param args   方法的参数
         * @param target 目标对象
         */
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println(target.getClass().getName() + "的" + method.getName() + "被执行了");
        }
    }
    
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class AfterLog implements AfterReturningAdvice {

    /**
     * afterReturning 后置通知
     *
     * @param returnValue 方法执行的放回值
     * @param method      要执行的目标对象的方法
     * @param args        方法的参数
     * @param target      目标对象
     */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("执行了" + method.getName() + "返回结果为:" + returnValue);
    }
}
  1. 具体的业务代码
import org.springframework.stereotype.Component;

@Component
public class UserService {
    public void create() {System.out.println("创建一个用户");}
    public void delete() {System.out.println("删除一个用户");}
    public void update() {System.out.println("更新一个用户");}
    public void read() {System.out.println("查询一个用户");}
}
  1. 调用具体的业务代码以及返回的结果

    import com.example.demo.config.AppConfig;
    import com.example.demo.service.aopservice.UserService;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    
    @SpringBootApplication
    public class DemoApplication {
       
       public static void main(String[] args) {
          AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
          UserService userService = ac.getBean(UserService.class);
          userService.create();
       }
    }
    

在这里插入图片描述

2.2方式二:自定义类实现AOP

自定义类作为切面,在配置文件中引入切面,aop:aspect,引入切面后,相关配置会有具体的通知方式来选择

  1. 方式二的AOP配置文件
<?xml version="1.0" encoding="UTF-8"?>
<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
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

    <!--方式二:自定义类实现`AOP`-->
    <!-- 配置AOP: 需要导入AOP约束-->
    <aop:config>
        <!--自定义切面-->
        <!--ref 引入一个切面(spring管理的bean)-->
        <aop:aspect ref="diyPointAspect">
            <!--切入点-->
            <!--id:在下面的配置中使用定位切入点-->
            <!--expression: 表达式,指定切入点作为的具体位置,具体表达式语法可google-->
            <aop:pointcut id="pointcut" expression="execution(* com.example.demo.service.aopservice..*(..))"/>

            <!--通知-->
            <!--在aop:aspect标签下,会出现子标签,来定义具体的通知方式,比如:aop:before/aop:after -->
            <aop:before method="before" pointcut-ref="pointcut"/>
            <aop:after method="after" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>
  1. 自定义切面
import org.springframework.stereotype.Component;

@Component
public class DiyPointAspect {
    public void before() { System.out.println("==========方法执行前===========");}
    public void after() { System.out.println("==========方法执行后===========");}
}
  1. 具体业务代码(和方式一相同,略)

  2. 调用具体的业务代码以及返回的结果(略)

  3. 小彩蛋(嘻嘻嘻嘻嘻)

    (1)AOP-execution表达式后面支持注解的形式,比如aop的配置文件中的aop:pointcut可以修改为:

    <aop:pointcut id="pointcut" expression="@annotation(com.example.demo.service.aopservice.LogPointCut)"/>
    

    (2)定义一个注解

    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Documented
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LogPointCut {
    }
    

    (3)将注解作用到对应的方法上

    @Component
    public class UserService {
        @LogPointCut
        public void create() { System.out.println("创建一个用户"); }
    
        @LogPointCut
        public void delete() { System.out.println("删除一个用户");}
    
        @LogPointCut
        public void update() { System.out.println("更新一个用户");}
    
        @LogPointCut
        public void read() { System.out.println("查询一个用户");}
    }
    
2.3方式三:使用注解方式实现AOP

这种方式和方式二基本上是一致的,只不过是用注解来代替了配置文件

(1)注解配置的切面,通过@Aspect来告诉spring这是一个切面。通过@Before/@After来告诉spring具体的通知方式

注意:这里如果不扫描配置文件(因为不用写配置文件,所以肯定没的扫描了),就需要用注解的方式告诉spring这是配置类,可以通过@Configuration注解,当然你也可以通过配置文件的方式

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Configuration;

@Configuration  // 告诉spring这是配置类
@Aspect         // 告诉spring这是一个切面
public class AnnotationPointAspect {
    @Before("execution(* com.example.demo.service.aopservice..*(..))")
    public void before() { System.out.println("==========方法执行前===========");}

    @After("execution(* com.example.demo.service.aopservice..*(..))")
    public void after() {System.out.println("==========方法执行后===========");}
}
  1. 具体业务代码(和方式一相同,略)

  2. 调用具体的业务代码以及返回的结果(略)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值