美文网首页
Spring AOP

Spring AOP

作者: sunhaiyu | 来源:发表于2018-10-11 16:14 被阅读9次

探秘Spring AOP

使用AOP的好处

AOP(Aspect-Orientid Programming)面向切面编程,可以将遍布在应用程序各个地方的功能分离出来,形成可重用的功能组件。系统的各个功能会重复出现在多个组件中,各个组件存在于核心业务中会使得代码变得混乱。使用AOP可以将这些多处出现的功能分离出来,不仅可以在任何需要的地方实现重用,还可以使得核心业务变得简单,实现了将核心业务与日志、安全、事务等功能的分离。

AOP的简单使用

先看一个用户权限验证的例子,假设需求是:只允许管理员对商品进行添加和删除。

商品类

package com.shy.aopdemo.domain;


public class Product {
    private Long id;
    private String name;
    private Double price;
    // 省略getter和setter
}

存储当前线程的用户角色。

package com.shy.aopdemo.security;


public class CurrentUserHolder {
    private static final ThreadLocal<String> holder = new ThreadLocal<>();

    public static void setUserHolder(String user) {
        holder.set(user);
    }

    public static String getUserHolder() {
        return holder.get();
    }
}

验证用户角色,如果不是管理员,直接抛出异常

package com.shy.aopdemo.security;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;


@Component
public class AuthService {
    public void checkAccess() {
        String user = CurrentUserHolder.getUserHolder();
        if (!"admin".equals(user)) {
            throw new RuntimeException("操作不被允许!");
        }
    }
}

然后在ProductService中进行isnert和delete操作

package com.shy.aopdemo.service;

import com.shy.aopdemo.domain.Product;
import com.shy.aopdemo.security.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ProductService {
    @Autowired
    private AuthService authService;
    public void insert(Product product) {
        authService.checkAccess();
        System.out.println("insert a product");
    }

    public void delete(Long id) {
        authService.checkAccess();
        System.out.println("delete product " + id);
    }
}

可以发现,insert和delete方法中有重复代码出现了,而且这个验证动作本身和新增、删除商品的逻辑关系不大,使用如上的传统方式,代码侵入性强,使核心业务复杂。

改用AOP的方式

package com.shy.aopdemo.aspect;

import com.shy.aopdemo.security.AuthService;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AuthAspect {
    @Autowired
    private AuthService authService;
    @Pointcut("execution(* com.shy.aopdemo.service.ProductService.*(..))")
    public void adminOnly() {}

    @Before("adminOnly()")
    public void checkAccess() {
        authService.checkAccess();
    }
}

现在将ProductService中所有方法的authService.checkAccess();注释掉,再来测试。

package com.shy.aopdemo;

import com.shy.aopdemo.security.CurrentUserHolder;
import com.shy.aopdemo.service.ProductService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class AopdemoApplicationTests {
    @Autowired
    private ProductService productService;
    @Test
    public void checkDeleteTest() {
        CurrentUserHolder.setUserHolder("guest");
        productService.delete(1L);
    }
}

因为当前用户是游客guest,所以测试不通过,改成admin测试将通过。

切面表达式

  • *:匹配任意数量的字符
  • +:匹配指定的类及其子类
  • ..:用于匹配任意数的子包或参数
  • &&||!:与、或、非

within()

使用within()限定切点作用的范围

// 作用于service包下的所有类
@Pointcut("within(com.shy.aopdemo.service.*)")

this()

使用this(),匹配AOP代理类为指定类型

@Pointcut("this(com.shy.aopdemo.service.ProductService)")

target()

使用target(),匹配目标对象为指定类型的类。target(M)表示如果目标类按类型匹配于M,这目标类的所有方法都匹配切点。

@Pointcut("target(com.shy.aopdemo.service.ProductService)")

一般情况下,使用this()和target()来匹配定义切点,二者是等效的。区别在于如果要给代理引入新的接口,即DeclareParents(Introduction)。使用this可以匹配到,使用target不能匹配到。

bean()

使用bean()匹配指定的bean

@Pointcut("bean(productService)")

execution()

下面第这句,第一个*号表示任意返回类型,第二个*表示ProductService下的所有方法。

第二句表示service包及其子包下所有以Service结尾的类中的所有public方法。

@Pointcut("execution(* com.shy.aopdemo.service.ProductService.*(..))")

@Pointcut("execution(public * com.shy.aopdemo.service..*Service.*(..))")

args()

匹配参数为指定类型的方法,下面这句制定了service包下所有类中只有一个Long型参数的方法/第一个参数类型是Long的方法

@Pointcut("within(com.shy.aopdemo.service.*) && args(Long)")
// 匹配第一个参数是Long的方法
@Pointcut("within(com.shy.aopdemo.service.*) && args(Long,..)")

@annotation()

匹配指定注解类

@Pointcut("@annotation(com.shy.aopdemo.anno.AdminOnly)")

其中AdminOnly定义如下

package com.shy.aopdemo.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) // 只能在方法上声明注解
public @interface AdminOnly {
}

@within()/@target()

匹配指定注解类下的方法 ,要求RetentionPolicy级别为CLASS

@Pointcut("@within(com.shy.aopdemo.anno.NeedAuth) within(com.shy.aopdemo.service.*)")
package com.shy.aopdemo.anno;

import java.lang.annotation.*;


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited // 被修饰的类的子类可以自动继承该注解
public @interface NeedAuth {
}

注意:被@Inherited修饰的类,其子类也会继承该注解。

@target要求要求RetentionPolicy级别为RUNTIME(@within()要求是CLASS),除此之外在Spring上下文环境中,没有任何差别。直接将within改成target就可以了。

@Pointcut("@target(com.shy.aopdemo.anno.NeedAuth) && within(com.shy.aopdemo.service.*)")

@args()

匹配传入的参数标注有指定注解的方法,如下

@Pointcut("@args(com.shy.aopdemo.anno.NeedAuth) && within(com.shy.aopdemo.service.*)")

在Product上加上注解

@NeedAuth
public class Product {
    private Long id;
    private String name;
    private Double price;
    // getter和setter省略
}

会匹配到service包下所有入参是Product的方法,如下:

public void insert(Product product) {
    // Before
    System.out.println("insert a product");
}

advice注解

  • 前置通知(Before):在目标方法被调用之间前调用通知功能;

  • 后置通知(After):在目标方法被调用或者抛出异常之后都会调用通知功能;

  • 返回通知(After-returning):在目标方法成功执行之后调用通知;

  • 异常通知(After-throwing):在目标方法抛出异常之后调用通知;

  • 环绕通知(Around):通知包裹了被通知的方法,在目标方法被调用之前和调用之后执行自定义的行为。

AOP中 @Before @After @AfterThrowing @AfterReturning的执行顺序如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   Object result;
   try {
       // @Before
       result = method.invoke(target, args);
       // @After
       return result;
   } catch (InvocationTargetException e) {
       Throwable targetException = e.getTargetException();
       // @AfterThrowing
       throw targetException;
   } finally {
       // @AfterReturning
   }
}

可知@AfterReturning的执行在@After之后。举一个更详细的例子,来看各个advice的执行顺序。

package com.shy.aopdemo.aspect;

import com.shy.aopdemo.security.AuthService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AuthAspect {
    @Autowired
    @Pointcut("execution(* com.shy.aopdemo.service.ProductService.*(..))")
    public void adminOnly() {}

    @Before("adminOnly()")
    public void checkAccess() {
        System.out.println("Before");
    }
    @After("adminOnly()")
    public void checkAfter() {
        System.out.println("After");
    }
    @AfterReturning(value = "adminOnly()")
    public void checkAfterRe() {
        System.out.println("AfterReturning ");
    }

    @Around("adminOnly()")
    public void checkAround(ProceedingJoinPoint joinPoint) {
        System.out.println("Around before");
        try {
            joinPoint.proceed(joinPoint.getArgs());
            System.out.println("Around After");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            System.out.println("Around finally");
        }
    }
}

输出如下,其中”insert a product“是被通知方法的输出。

Around before
Before
insert a product
Around After
Around finally
After
AfterReturning 

在@Around中,通过获取joinPoint.proceed(..);来执行被通知的方法,joinPoint.getArgs()用来获取被通知的方法参数列表。如果确定被通知方法无参,那么proceed中不传入入getArgs()。

使用args还可以获取被通知方法的参数,如下,它表明ProductService下名为productId的参数也会传递到通知中,在这里是adminOnly(Long productId),参数的名称与切点方法的参数名一样。这个参数会传递到通知方法中,@Before注解中的参数名也和切点定义的参数名一样,这样就完成了从被通知方法参数到通知方法参数的转移。

@Pointcut("execution(* com.shy.aopdemo.service.ProductService.*(Long)) && args(productId))")
public void adminOnly(Long productId) {}

@Before(value = "adminOnly(productId)")
public void checkAccess(Long productId) {
    System.out.println("Before");
    System.out.println(productId);
}

代理模式

静态代理

下面是代理模式的类图

Snipaste_2018-10-11_09-34-12

实现如下

接口Subject,代理类和目标类都需要实现它

package com.shy.aopdemo.proxy;


public interface Subject {
    void request();
}

目标类

package com.shy.aopdemo.proxy;


public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("real subject");
    }
}

代理类实现Subject接口并且包裹了目标类对象,可以控制目标对象方法的调用。

package com.shy.aopdemo.proxy;

public class Proxy implements Subject{
    private RealSubject realSubject;
    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public void request() {
        System.out.println("before proxy");
        realSubject.request();
        System.out.println("after proxy");
    }
}

测试一下

package com.shy.aopdemo.proxy;


public class Client {
    public static void main(String[] args) {
        Subject subject = new Proxy(new RealSubject());
        subject.request();
    }
}

输出如下

before proxy
real subject
after proxy

静态代理的缺点是,代理类和目标类实现了相同的接口,代理类通过目标类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

动态代理

动态代理有两种实现:

  • 基于接口的JDK动态代理
  • 基于继承的Cglib代理
JDK动态代理

需要用到Proxy类和InvocationHandler接口,代理类实现InvocationHandler,如下。

package com.shy.aopdemo.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @author Haiyu
 * @date 2018/10/11 10:23
 */
public class JdkProxy implements InvocationHandler {
    private RealSubject realSubject;

    public JdkProxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object object = null;
        System.out.println("before");
        object = method.invoke(realSubject, args);
        // System.out.println(object.toString());
        System.out.println("after");
        return object;
    }
}

object = method.invoke(realSubject, args);这句使用反射调用了realSubject的method,并且方法的入参是args,其返回值object正是method的返回值。

修改Clinet类,关键是Proxy.newProxyInstance,第一个参数是指定代理类的classLoader,第二个参数是代理类要实现的接口,第三个参数传入invocation handler,可认为是代理类实例。

package com.shy.aopdemo.proxy;

import java.lang.reflect.Proxy;

public class Client {
    public static void main(String[] args) {
        Subject subject = (Subject) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[]{Subject.class},new JdkProxy(new RealSubject()));
        subject.request();
    }
}

现在在Subject类中新增方法String hello,同时RealSubject实现该方法。

@Override
public String hello() {
    String s = "hello";
    System.out.println(s);
    return s + " return";
}

而代理类无需任何修改,在Client类中,直接调用subject.hello();即可。因为被代理的hello方法有返回值,我们在InvocationHandler的invoke方法中打印下

object = method.invoke(realSubject, args);
System.out.println(object.toString()); // hello return

可知object是被代理类realSubject中hello方法的返回值,关于这点不清楚的可以先补一补Java反射。

CGlib动态代理

需要实现MethodInterceptor并重写intercept方法,

package com.shy.aopdemo.cglib;

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;


public class CGlibProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object result = null;
        System.out.println("cglib before");
        result = methodProxy.invokeSuper(o, objects);
//        System.out.println(Arrays.toString(objects));
//        System.out.println(method);
//        System.out.println(result);
        System.out.println("cglib after");
        return result;
    }
}

关键是这句result = methodProxy.invokeSuper(o, objects);,Object o表示被代理的目标对象,objects也就是args,是被代理方法的入参。method是被代理的方法。

对比
  • JDK只能针对有接口的类的接口方法进行动态代理
  • Cglib基于继承来实现代理,无法对static、final 的类进行代理;同样无法对于privage、static方法进行代理
  • 由于接口中也不能存在private方法,所以JDK也不能对private方法进行代理

Spring AOP如何选择使用哪种动态代理

  • 如果目标对象实现了接口,默认使用JDK动态代理
  • 如果目标对象没有实现接口,使用Cglib动态代理
  • 如果目标对象实现了接口,但是强制使用Cglib代理,则使用Cglib代理

如何设置强制使用Cglib呢?在Applicateion上添加@EnableAspectJAutoProxy注解,并摄者proxyTargetClass = true即可。

package com.shy.aopdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
// 添加这个注解,并proxyTargetClass = true
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopdemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(AopdemoApplication.class, args);
    }
}

相关文章

网友评论

      本文标题:Spring AOP

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