0x07.Spring AOP基本使用

2018-08-10  本文已影响0人  0x70e8

[TOC]

概述

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

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

AOP术语

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

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

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

描述切面的常用术语:

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

通知(advice)

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

连接点(joinpoint)

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

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

切点(pointcut)

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

切面(aspect)

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

引入(introduction)

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

织入(weaving)

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

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

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

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

AOP代理

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

Spring对AOP的支持

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

Spring AOP的局限:

Spring AOP in action

定义切面

切面由advice和pointcut组成。

声明Aspect

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

@Component
@Aspect
public class UserServiceAspect {
    // ...
}

声明advice

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

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。

例:

使用多个指示器时,可以使用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

@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方法甚至可以对参数进行操作来修改参数。

上一篇下一篇

猜你喜欢

热点阅读