探秘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);
}
代理模式
静态代理
下面是代理模式的类图

实现如下
接口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);
}
}
网友评论