美文网首页
0x07.Spring AOP基本使用

0x07.Spring AOP基本使用

作者: 0x70e8 | 来源:发表于2018-08-10 15:51 被阅读0次

    [TOC]

    概述

    在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern),一般而言这些横切关注点与具体的业务逻辑是相互分离的,如记录日志、控制事务等,往往被嵌入到业务代码中。AOP就是为了将这些横切关注点和业务逻辑分离。

    在IOC容器中,DI实现了对象之间的依赖解耦,aop可以实现横切关注点和与他们所影响的对象之间的解耦。日志是应用切面的常见范例,除此之外切面还适用于多个场景,如声明式事务、安全和缓存。

    AOP术语

    切面能帮助我们模块化横切关注点,并使之能够复用。在面向对象的设计里,复用功能的实现方式有继承和委托。但是如果在整个应用中使用相同的基类,继承往往会导致一个脆弱的对象体系,而委托可能需要对委托对象进行复杂的调用。

    面向切面编程(AOP)的切面,就是分散在应用中的横切关注点,AOP的目的是将横切逻辑从业务逻辑中抽离出来,再通过声明的方式定义这个功能要在何处以何种方式使用,而无需修改受影响的类。

    这样做有两个好处,一方面是横切逻辑代码能够集中于一处,便于维护,另外业务代码可以专注业务逻辑。

    描述切面的常用术语:

    • 通知(advice)
    • 切点(pointcut)
    • 连接点(joinpoint)

    实际上在AOP中,本质上只是要定义一个通用的行为以及此行为要切入的位置和时机。通知描述了一个切面的动作和触发时机,切点和连接点则描述切入的位置。(Spring AOP只支持方法级别的切入,所以位置只有方法级别的)

    通知(advice)

    通知,advice,就是动作。通知定义了切面是什么,以及何时使用,除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。Spring切面可以使用五种类型的通知:

    • 前置通知(Before):在目标方法被调用之前调用advice
    • 后置通知(After): 在目标方法调用之后调用advice,不会关心方法的输出是什么
    • 返回通知(After-returning):在目标方法成功执行之后调用advice
    • 异常通知(After-throwing):在方法抛出异常之后调用通知
    • 环绕通知(Around):advice包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为,这是权限最高的advice

    连接点(joinpoint)

    连接点是应用执行过程中能够插入一个切面的一个点,这个点可以是调用方法时、抛出异常时、甚至是修改一个字段时,切面代码可以利用这些点插入到应用的正常流程之中,来添加新的行为。

    连接点似乎和切面切入的时机相关,和位置似乎无关,其实不是的,程序中代码的执行时机和位置是相互关联的,只有执行中的应用才有切入的意义,所以连接点定义一个时机,实际上也就是代码的位置,只是在运行时位置和时机就等价了。

    切点(pointcut)

    切点的定义会匹配所要织入的一个或多个连接点。我们通常使用明确的类或者方法名称,或是利用正则表达式定义所要匹配的类和方法名称来指定这些切点。

    切面(aspect)

    通知和切点的结合就是一个切面。切点和通知定义了位置、执行时机和行为。

    引入(introduction)

    引入允许我们向现有的类添加新方法或属性。

    织入(weaving)

    织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多少个点可以进行织入:

    • 编译期

    切点在目标类编译时被织入,此方式需要特殊的编译器,AspectJ的织入编译器就是以这种方式织入切面的。

    • 类加载期

    切面在目标诶加载到JVM时被织入,这种方式需要特殊的类加载器,它可以在目标了被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持这种方式织入切面。

    • 运行期

    切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,aop容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。

    AOP代理

    由AOP框架创建的用来实现切面逻辑(切面切入目标执行)的对象,在Spring AOP中,这个对象是jdk动态代理或CGLIB生成的代理对象。

    Spring对AOP的支持

    Spring提供了4种类型的AOP支持:

    • 基于代理的经典SpringAOP
    • 纯pojo切面
    • @AspectJ注解驱动的切面
    • 注入式AspectJ切面(使用AspectJ扩展来实现AOP)

    Spring AOP的局限:

    • Spring AOP构建在动态代理的基础之上,因此Spring对AOP的支持局限于方法拦截。
    • Spring通知是java编写的
    • Spring在运行时通知对象,直到应用需要被代理的bean时,Spring才创建代理对象。

    Spring AOP in action

    定义切面

    切面由advice和pointcut组成。

    声明Aspect

    使用@Aspect声明一个POJO为aspect类,但是使用时还需加上@Component来交给Spring管理,或者使用xml方式,总之要注册成bean,否则不能被Spring识别。

    • 基于注解风格的Aspect
    @Component
    @Aspect
    public class UserServiceAspect {
        // ...
    }
    

    声明advice

    对应五种advice,有五种注解:

    • @Before
    • @AfterReturning
    • @AfterThrowing
    • @After
    • @Around

    Advice的注解是标注在方法上的,这个方法的逻辑是切入到切点的横切逻辑。注解的含义是发生的时机,如before、after等,注解内需要注明此advice对于的PointCut,即切点的定义,主要一个完整的横切逻辑的定义才完整(动作、时机、位置)。

        // 引用类中定义的pointcut方法
        @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
        public void doAccessCheck() {
            // ...
        }
     
        @Around("execution(* com.springinaction.aop.DemoService.print(String))")
        public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
            // start stopwatch
            Object retVal = pjp.proceed();
            // stop stopwatch
            return retVal;
        }
    

    Around的优先级高于其他的advice,如果在Around的advice中没有调用proceed方法,目标方法不会被调用。故Around的advice方法第一个参数需要设置为ProceedingJoinPoint。

    定义切点

    Spring借助AspectJ的切点表达式语言来定义Spring切点:

    AspectJ指示器 描述
    args() 限制匹配的方法的参数,更常用的是绑定形式,将方法参数传递给advice
    @args() 限制匹配方法的参数由指定注解标注
    execution() 匹配方法
    this() 匹配目标对象的类型
    target() 匹配指定类型
    @target() 匹配目标对象且类上有指定类型的注解
    within() 匹配对象的类
    @within() 匹配注解标注的类型
    @annotation() 匹配指定注解

    除以上之外的AspectJ指示器都不可用,会抛出IllegalArgumentException。
    另外Spring引入了一个bean()指示器,运行切点通过beanid来匹配bean。

    例:

    • execution(* com.test.Test.method(..) ):匹配任意参数的任意返回值的 com.test.Test类的method()方法
    • execution(* com.test.Test.method(..) && within(com.*)):匹配com.test.Test.method方法和com包下任意方法。

    使用多个指示器时,可以使用and,or,not或者对应的&&,||,!进行连接(not是条件取反)。

    advice方法和Joinpoint交互

    通过给advice方法声明JoinPoint 参数来获取当前的joinpoint,另外Around类型的advice使用ProceedingJoinPoint(它是JoinPoint类的子类,是@Around方法的必选参数)。

    传递方法参数到advice

    关于前述的多个指示器,官方reference有这些说明:

    • 'this' is more commonly used in a binding form
    • 'target' is more commonly used in a binding form
    • 'args' is more commonly used in a binding form
    • '@target' can also be used in a binding form
    • '@within' can also be used in a binding form
    • '@annotation' can also be used in a binding form
    • '@args' can also be used in a binding form

    也就是说除了within()和execution(),其余的都可以用来绑定参数。

    @Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
    private void accountDataAccessOperation(Account account) {}
    
    @Before("accountDataAccessOperation(account)")
    public void validateAccount(Account account) {
        // ...
    }
    

    or

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
    public void validateAccount(Account account) {
        // ...
    }
    

    or @annotation

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Auditable {
        AuditCode value();
    }
    
    @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
    public void audit(Auditable auditable) {
        AuditCode code = auditable.value();
        // ...
    }
    

    or generic type

    @Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
    public void beforeSampleMethod(MyType param) {
        // Advice implementation
    }
    @Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
    public void beforeSampleMethod(Collection<MyType> param) {
        // Advice implementation
    }
    

    启用AOP

    • JavaConfig方式
    @Configuration
    @ComponentScan // 配置自动扫描,默认是本类所在的包及子包
    @EnableAspectJAutoProxy // 启用Aspectj自动代理 ,不开启AOP无效
    public class RootConfig {
    
    }
    

    Sample

    // configuration 
    @Configuration
    @ComponentScan // 配置自动扫描,默认是本类所在的包及子包
    @EnableAspectJAutoProxy // 启用Aspectj自动代理 ,不开启AOP无效
    @PropertySource("classpath:/conf.properties")
    public class SpringAopConfig {
        @Autowired // 可以将Property注入到env中来
        private Environment env;
    
        @Bean
        public PrintStream stream() {
            return System.out;
        }
    
        /**
         * Java中配置占位符的方式,需要此Bean和上面的@PropertySource("classpath:/conf.properties")注解支持,
         * 而后可以在Component中使用{@value使用占位符注入属性值}
         * 
         * @return
         */
        @Bean
        public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
            return new PropertySourcesPlaceholderConfigurer();
        }
    
        @Bean
        public Environment env() {
            return env;
        }
    }
    
    // Service
    @Service
    public class DemoService {
    
        @Autowired
        private PrintStream stream;
    
        public void print(String msg) {
            stream.println(msg);
        }
    }
    
    // Aspect
    @Component
    @Aspect
    public class DemoAspect {
    
        @Before("execution(* com.springinaction.aop.DemoService.print(String))")
        public void beforePrint() {
            System.out.println("--beforePrint--");
        }
    
        @Before("execution(* com.springinaction.aop.DemoService.print(String)) && args(msg)")
        public void beforePrintwithArgs(String msg) {
            System.out.println("--beforePrintwithArgs--");
            System.out.println("in advice get msg:" + msg);
        }
    
        @Pointcut("execution(* com.springinaction.aop.DemoService.print(String)) && args(msg)")
        public void print(String msg) {
    
        }
    
        @Around("print(msg)")
        public void holdPrint(/* ProceedingJoinPoint jp, */String msg) throws Throwable {
            System.out.println("holdPrint");
            // jp.proceed();  // 注掉就不会进入目标方法
            // jp.proceed(new Object[] { "what" });
            System.out.println("holdPrint end");
        }
    }
    
    //  Test
        @Test
        public void testPrint() {
            ApplicationContext context = new AnnotationConfigApplicationContext(SpringAopConfig.class);
            DemoService service = context.getBean(DemoService.class);
            System.out.println(service);
            service.print("hello aop");
        }
    

    Around方法的执行内会环绕在before、after等方法的执行之外。

    给目标对象引入新功能(introduction)

    使用@DeclareParents注解为bean引入接口方法

    // 增强接口
    public interface Enhance {
        public void enhance();
    }
    // 增强接口的默认实现
    public class DefaultEnhance implements Enhance {
    
        @Override
        public void enhance() {
            System.out.println("this is an enhance");
    
        }
    
    }
    // 增强Aspect
    @Component
    @Aspect
    public class Introducer {
        @DeclareParents(value = "com.springinaction.aop.DemoService", defaultImpl = DefaultEnhance.class)
        public Enhance defaultEnhance;
    }
    // Test
        @Test
        public void testIntroducer() {
            ApplicationContext context = new AnnotationConfigApplicationContext(SpringAopConfig.class);
            Enhance service = (Enhance) context.getBean(DemoService.class);
            service.enhance();
            ((DemoService) service).print("tets");
        }
    

    小结

    Spring AOP的原理是动态代理;
    Spring AOP只能拦截方法;
    Spring AOP的使用方法包括启动aop支持、定义横切逻辑两大部分。定义横切逻辑是定义切面和advice,用于拦截目标方法。在定义横切逻辑方面,需求大致在于方法调用前、执行正常返回、执行抛异常、执行返回(finally)来添加与方法内实质逻辑无关的逻辑,另外还有一个强大的Around advice能够包裹目标方法,可以干涉方法的执行过程。

    Advice在拦截目标方法时,可以获取到方法的参数,Around方法甚至可以对参数进行操作来修改参数。

    相关文章

      网友评论

          本文标题:0x07.Spring AOP基本使用

          本文链接:https://www.haomeiwen.com/subject/djmcbftx.html