Spring AOP【转】

  以下转载和参考自:装配AOP - 廖雪峰的官方网站

1、代理模式和AOP

    前面说过Proxy(代理)模式,比如下面的Foo类中的func1和func2在执行前后需要验证权限和记录日志,那么实际上适合将这些动作提取到一个代理类中去,因为验证权限、日志记录这些功能实际上并不属于Foo类主逻辑功能,这样能降低代码耦合度。

public class Foo {
    public void func1() {
        securityCheck(); //权限检查
        ......
        log("log");//日志记录
    }

    public void func2() {
        securityCheck(); //权限检查
        ......
        log("log");//日志记录
    }
}

public class FooProxy implements Foo{
    private final Foo target;

    public FooProxy(Foo target) {
        this.target = target;
    }

    public void func1() {
        securityCheck(); //权限检查
        target.func1();
        log("log");//日志记录
    }

    public void func2() {
        securityCheck(); //权限检查
        target.func2();
        log("log");//日志记录
    }
}

    在Spring中,可以通过AOP(Aspect Oriented Programming),即面向切面编程来实现代码插入的功能。在AOP中,上面的Foo类中的func1、func2称为“核心逻辑”,权限验证、记录日志这些称为“切面逻辑”,AOP要实现的就是把切面插入到核心逻辑中,通过Proxy模式来实现。

2、实现AOP

    如下通过AOP实现了调用Foo中任何方法之前会先执行MyAspect 类中的doAccessCheck()方法,调用Foo中任何方法之后会执行MyAspect 类中的doLogging()方法,调用Bar中任何方法之前之后会执行MyAspect中的beforeBarFun()、afterBarFun()方法。实现AOP的方法是在切面类中添加@Component和@Aspect注解,在@Configuration类(Ioc配置类)加上一个@EnableAspectJAutoProxy注解:

@Component
public class Foo {
    public void fun(){
        System.out.println("Foo func()");
    }
}

@Component
public class Bar {
    public void fun(){
        System.out.println("Bar func()"); //err.print会以红色显示输出
    }
}

@Aspect
@Component
public class MyAspect {
    // 在执行Foo的每个方法前执行doAccessCheck()
    @Before("execution(public * xsl.Foo.*(..))")
    public void doAccessCheck() {}

    // 在执行Foo的每个方法后执行doLogging()
    @After("execution(public * xsl.Foo.*(..))")
    public void doLogging() {}

    // 在执行Bar的每个方法前执行beforeBarFun()、每个方法后执行afterBarFun
    @Around("execution(public * xsl.Bar.*(..))")
    public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
        beforeBarFun();
        Object retVal = pjp.proceed();
        afterBarFun();

        return retVal;
    }

    private void beforeBarFun(){}
    private void afterBarFun(){}
}

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(App.class);

        Bar b = context.getBean(Bar.class);
        b.fun();
        
        Foo f = context.getBean(Foo.class);
        f.fun();
    }
}

  AOP的相关依赖:

    <properties>
        <project.build.srouceEncoding>UTF-8</project.build.srouceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>

        <spring.version>5.2.1.RELEASE</spring.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
    </dependencies>

  

  AOP中的拦截器有以下类型:

  • @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;

  • @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;

  • @AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;

  • @AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;

  • @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。

  拦截器中的注解值使用AspectJ注入语法,其它的示例还有:

@Before("execution(public * xsl.Foo.fun1(..))") //只有Foo类的fun1()方法才会被插入

@Before("execution(public * xsl.*.*(..))") //xsl包下的所有方法都会被插入

@Around("execution(public * update*(..))") //update开头的方法都会被插入

  可以只对使用了指定注解的方法才进行Before、Around等插入操作。比如下面@Around的注解值表示只有在使用了@MetricTime注解的方法调用的时候才会插入执行metric()中代码:

@Target(METHOD)
@Retention(RUNTIME)
public @interface MetricTime { //自定义的注解
    String value();
}

@Component
public class Foo {

    @MetricTime("TimeCost") //使用@MetricTime注解
    public void fun(){
        System.out.println("Foo func()");
    }
}

@Aspect
@Component
public class LoggingAspect {
    @Around("@annotation(metricTime)")
    public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
        String name = metricTime.value(); //注解的value值"TimeCost"
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long t = System.currentTimeMillis() - start;
            System.out.println(name + ": " + t + "ms");
        }
    }
}


@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(App.class);

        Foo f = context.getBean(Foo.class);
        f.fun();
    }
}

  

 3、AOP原理

   AOP的原理其实就是编写核心逻辑类的子类,并持有原始实例的引用,如下所示。使用AOP,实际上就是让Spring自动为我们创建一个Proxy,使得调用方能无感知地调用指定方法,但运行期却动态“织入”了其他逻辑,因此,AOP本质上就是一个代理模式

public FooProxy extends Foo {
private Foo target;
private LoggingAspect aspect;

public FooProxy(Foo target, LoggingAspect aspect) {
        this.target = target;
        this.aspect = aspect;
        }

public void fun() {
        // 先执行Aspect的代码:
        aspect.doAccessCheck();
        // 再执行UserService的逻辑:
        return target.fun();
        }
}

  在Java平台上,对于AOP的织入,有3种方式:

  1. 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
  2. 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
  3. 运行期:目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。

最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。关于动态代理,可以参见JAVA之反射_milanleon的博客-CSDN博客_类的反射 中相关的内容。JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB或者Javassist这些第三方库实现。Spring中也是支持AspectJ,但仅仅是支持AspectJ的切点解析和匹配,即使用@AspectJ注解来做AOP,但并没有使用AspectJ其中的实现AOP的方法,Spring具体的AOP实现还是JDK动态代理与CGLIB(Spring对接口类型使用JDK动态代理,对普通类使用CGLIB创建子类。如果一个Bean的class是final,Spring将无法为其创建子类)。

 4、AOP避坑

  ①、访问使用AOP的Bean时,总是调用方法而非直接访问成员。

  ②、如果Bean有可能使用AOP的话,不要编写public final成员方法。

  如下所示的Foo类,在使用了AOP后,从ApplicationContext中获取的Foo实例实际上是代理类FooProxy对象,而代理类的构造方法中并没有调用super()来执行父类Foo的构造方法,所以成员zoneId实际上并没有被初始化。虽然Java语言规定,任何类的构造方法,第一行必须调用super(),如果没有,编译器会自动加上,但Spring使用CGLIB构造的FooProxy类是直接生成字节码文件,绕过了编译器编译字节码这个步骤。所以直接引用Foo类成员获得的是null。调用final成员方法获得的zoneId成员也为null,因为FooProxy类无法覆写final方法,调用该方法就相当于是直接访问ZoneId成员。

@Component
public class Foo {
    public  ZoneId zoneId = ZoneId.systemDefault(); //该初始化实际上是在Foo类的构造方法中执行
    public final ZoneId getFinalZoneId() { return zoneId; }

    public ZoneId getZoneId() { return zoneId; }
}

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(public * xsl.Foo.*(..))") //对Foo类使用AOP:执行其方法前先调用metric()方法
    public void metric() {
        System.out.println("metric func()");
    }
}

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class App {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(App.class);

        Foo f = context.getBean(Foo.class);
        ZoneId zoneId = f.zoneId; // zoneId为null
        zoneId = f.getFinalZoneId(); // zoneId为null
        zoneId = f.getZoneId(); //zoneId不为null
    }
}

5、总结

   AOP称为“面向切面编程”,使用AOP可以实现在指定类的方法执行前或执行后插入其它操作,比如想要在Foo的func()方法执行前进行一些其它操作的话,那么可以定义一个切面类,在切面类中定义要插入的动作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值