关于SpringAOP的详细介绍

Aspect Oriented Programming with Spring :: Spring Framework

什么是AOP

  • AOP是指面向切面编程,能够解决OOP纵向编程中会出现像权限认证、日志、事务处理外围事务导致核心业务代码混乱冗余的问题。
  • 将外围事务封装为一个可重用的模块,命名为切面,降低耦合度提高可维护性
    在这里插入图片描述

AOP的原理

AOP的实现原理在于代理模式,分为静态代理和动态代理。像AspectJ就是静态代理,SpringAOP就是动态代理

  • AspectJ是静态代理,也称为编译时增强,会在编译期间生成AOP代理类,并将切面织入Java字节码中,运行的时候就是增强之后的AOP对象
  • SpringAOP使用的是动态代理,所谓动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP代理对象,这个代理对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法(反射invoke)

SpringAOP实现动态代理的方式(原理)

JDK动态代理

  • jdk动态代理只提供接口代理,不支持类的代理,被代理的类必须要实现接口,好让代理类重写接口方法生成代理对象
  • jdk动态代理的核心是invocationHandler接口和proxy类,在获取代理对象时使用Proxy类来动态创建目标代理类
  • 当代理对象调用真实对象的方法时,会自动跳转到代理对象关联的invocationHandler对象,其会通过invoke()方法反射来调用目标类中的方法,动态地把业务横切进去
invocationHandler中invoke(Object proxy,Method method,Object[] args)
  • proxy是指生成代理的对象
  • method是指目标对象的方法,通过反射调用
  • args是指目标对象方法的参数

例子

  1. 通过实现invocationHandler接口创建自己的调用处理器
  2. 为Proxy类提供要代理对象的类加载器、接口、处理器来创建动态代理对象
  3. 当代理对象调用真实对象的方法时,会自动跳转到代理对象关联的invocationHandler对象,其会通过invoke()方法反射来调用目标类中的方法,动态地把业务横切进去

创建接口

public interface Subject {
    public void SayHello(String name);
}

创建接口实现类

public class SubjetcImpl implements Subject{
    @Override
    public void SayHello(String name) {
        System.out.println(name+"嘿嘿");
    }
}

继承invocationHandler实现自己的调用器处理器

public class InvocationHandlerImpl implements InvocationHandler {
    /**
     * 要代理的真实对象
     */
    private Object subject;

    public InvocationHandlerImpl(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //代理之前
        System.out.println("调用之前");
        System.out.println("Method: "+method);
        //代理中
        Object returnValue = method.invoke(subject, args);
        //代理之后
        System.out.println("调用结束");
        return returnValue;
    }
}

测试,通过proxy类生成动态代理对象

public class Main {
    public static void main(String[] args) {
        //要被代理的对象
        Subject realSubject=new SubjetcImpl();
        //要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法
        InvocationHandler handler = new InvocationHandlerImpl(realSubject);
        ClassLoader classLoader = realSubject.getClass().getClassLoader();
        Class[] interfaces = realSubject.getClass().getInterfaces()//该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
        Subject subject  =(Subject)Proxy.newProxyInstance(classLoader, interfaces, handler);
        System.out.println("动态代理类的类型"+subject.getClass().getName());
        subject.SayHello("aciu");
    }
}

CGLIB

  • 如果代理类没有实现接口,那么AOP就会选择使用CGLIB,用继承方式做动态代理。若该类为final类,则没法用CGLIB
  • CGLIB(Code Generation Library)是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象。并覆盖其中特定方法并添加增强代码,从而实现AOP。

AOP名词概念

  • 连接点(Join Point):指程序运行过程中执行的方法。SpringAOP中一个连接点总代表一个方法的执行
  • 切入点(Pointcut): 匹配连接点的断言。通知与切入点表达式相关联,并在切入点匹配的任何连接点上运行(例如,具有特定名称的方法的执行),与切入点表达式匹配的连接点的概念是 AOP 的核心。用来描述我们要在哪些地方执行,也可以说成是用表达式匹配(正则断言)的切入点
  • 切面(Aspect):被抽取出来的公共模块,可以用来横切多个对象。Aspect切面可以看成Poincut切点和Advice通知的结合,一个切面可以由多个切点和通知组成(SpringAOP中用@Aspect)
  • 通知(Advice):指要在连接点(Join Point)上执行的操作,即增强的逻辑,比如权限的校验和日志的记录等。通知有各种类型,包括AroundBeforeAfterAfter returningAfter throwing
  • 目标对象(Target):包含连接点的对象,也称作被通知的对象
  • 织入(Weaving):通过动态代理在目标对象的方法中执行增强逻辑的过程,即在Target的Join point中执行Advice
  • 引入(Introduction):添加额外的方法或字段到被通知的类中
    在这里插入图片描述

SpringAOP通知类型

  • 前置通知(Before Advice):在连接点之前执行通知
  • 后置通知(After Advice):在连接点退出的时候执行的通知
  • 环绕通知(Around Advice):包围一个连接点的通知,可以在方法调用前后完成自定义行为
  • 返回后通知(Afterreturning Advice):在连接点正常完成后执行的通知
  • 抛出异常(AfterThrowing Advice):在方法抛出异常退出时执行的通知

执行顺序为,@Around, @Before, @After, @AfterReturning, @AfterThrowing,若对同一切面切点要使用同一注解的话可使用@Order进行排序

SpirngAOP注解定义

这里只记录@Aspect@Pointcut@Around,像其他通知类型的注解、@DeclareParents声明注解查阅官方文档Declaring Advice :: Spring Framework

@Aspect

定义切面,作用于类上。需要注意的是@Aspect注释不足以在类路径中进行自动检测,需要添加一个单独的@Component 注释(或者根据 Spring 组件扫描器的规则,添加一个符合条件的自定义原型注释)。

@Pointcut

定义切点,有多种表达式,这里只记录一下最常用的execution,其他的可翻阅文档Declaring a Pointcut :: Spring Framework

执行表达式为

execution(modifiers-pattern?
			ret-type-pattern
			declaring-type-pattern?name-pattern(param-pattern)
			throws-pattern?)

modifiers-pattern:修饰符,publicprivate等,省略时匹配任意修饰符

ret-type-pattern:返回类型,不可省略的参数,使用 * 表示匹配任何返回类型

declaring-type:声明类型,类名称模式,省略时匹配任意类型。

  • 如果有指定,要包含. 将其连接到方法名称name-pattern
  • .. 表示匹配包及其子包的所有类

name-pattern:方法名称,使用* 表示全部,匹配任意方法,set* 匹配名称以 set 开头的方法

param-pattern:匹配参数类型和数量

  • () 表示匹配没有参数的方法
  • (..) 表示匹配有任意数量参数的方法
  • (*) 表示匹配有一个任意类型参数的方法
  • (*,String) 表示匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
//匹配任何公共方法,修饰符+返回类型+方法名称+参数
execution(public * *(..))
    
//匹配任何以set开头的方法,返回类型+方法名称+参数
execution(* set*(..))

//在AccountService接口定义的任何方法的执行,返回类型+声明类型+方法名+参数    
execution(* com.xyz.service.AccountService.*(..))    

//服务包中定义的任何方法的执行,返回类型+声明类型+方法名+参数
execution(* com.xyz.service.*.*(..))
    
//服务包或其子包中定义的任何方法的执行,返回类型+声明类型+方法名称+参数 
execution(* com.xyz.service..*.*(..))

@Around

  • Around通知能在方法运行之前和之后执行工作,如果需要在方法执行之前和之后以线程安全的方式共享状态(例如,启动和停止计时器) ,则经常使用 Around 通知。如果不需要前后都进行操作,不建议用Around,要节约性能。

  • @Around注释的方法应该将Object声明为其返回类型,并且第一个参数必须ProceedingJoinPoint类型。

  • 在通知方法的主体中,必须在ProceedingJoinPoint上调用process (),一般无参调用原始方法,可以接受Object[]数组作为参数。

  • ProceedingJoinPoint为JoinPoint的子类,提供了以下常用方法

    • getArgs():返回方法的参数

    • getThis():返回代理对象

    • getTarget():返回目标对象

    • getSignature():返回正在通知的方法的说明,可以将Signature对象转换为MethodSignature对象,通过MethodSignature对象可以获取到更加详细的方法签名信息,比如方法返回类型、参数类型、判断是否有特定注解等。

      • MethodSignature methodSignature = (MethodSignature) signature;
        String methodName = methodSignature.getName();
        Class<?> returnType = methodSignature.getReturnType();
        Class<?>[] parameterTypes = methodSignature.getParameterTypes();
        boolean annotationPresent = method.isAnnotationPresent(DisableLog.class);
        
    • toString():打印所建议的方法的有用说明

@Aspect
public class AroundExample {
	@Around("execution(* com.xyz..service.*.*(..))")
	public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
		// start stopwatch
		Object retVal = pjp.proceed();
		// stop stopwatch
		return retVal;
	}
}

SpringAOP的例子

重试操作

业务服务的执行有时会由于并发问题(例如,死锁失败)而失败。如果重试操作,则下一次尝试可能会成功。对于适合在这种情况下重试的业务服务(不需要返回给用户进行冲突解决的幂等操作) ,我们希望透明地重试该操作,以避免客户机看到 PessimisticLockingfalureException。这是一个明显跨越服务层中多个服务的需求,因此非常适合通过aop横切织入来实现。

@Aspect
@Component
public class ConcurrentOperationExecutor implements Ordered {

	private static final int DEFAULT_MAX_RETRIES = 2;

	private int maxRetries = DEFAULT_MAX_RETRIES;
	private int order = 1;

	public void setMaxRetries(int maxRetries) {
		this.maxRetries = maxRetries;
	}

	public int getOrder() {
		return this.order;
	}

	public void setOrder(int order) {
		this.order = order;
	}

	@Around("com.xyz.CommonPointcuts.businessService()") 
	public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
		int numAttempts = 0;
		PessimisticLockingFailureException lockFailureException;
		do {
			numAttempts++;
			try {
				return pjp.proceed();
			}
			catch(PessimisticLockingFailureException ex) {
				lockFailureException = ex;
			}
		} while(numAttempts <= this.maxRetries);
		throw lockFailureException;
	}
}

日志记录

@Slf4j
@Aspect
@Order(1)
@Component
public class CoreAspect {
    @Autowired
    private HttpServletRequest servletRequest;

    @Around("execution(* com.aciu.admin.apis.controller..*.*(..))")
    public Object execute(ProceedingJoinPoint pjp) throws Throwable {
        Long startTime = System.currentTimeMillis();
        //用于logback记录整个调用链路 web->service->dao,日志跟踪id
        String traceLogId = SnowFlakeUtil.getId();
        MDC.put("traceLogId", traceLogId);
        //拿到接口方法信息
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        //如果有该注解,则不记录日志
        if(method.isAnnotationPresent(DisableLog.class)) {
            return pjp.proceed();
        }

        Object[] args = pjp.getArgs();
        List<Object> logArgs = Lists.newArrayList(args).stream()
                .filter(arg -> (!(arg instanceof HttpServletRequest) 
                                && !(arg instanceof HttpServletResponse)))
                .collect(Collectors.toList());
        String logArgsJson =  JsonUtils.obj2String(logArgs);
        StringBuilder logArgsSb = null;
        if(logArgsJson!=null) {
            if(logArgsJson.length()>4096) {
                logArgsSb = new StringBuilder(logArgsJson.substring(0,4096));
            }
        }
		//执行日志
        log.debug("Http请求开始: Url:{},HTTP Method:{},IP:{},方法:{},被调用开始,是需要否授权:{},入参:{}",servletRequest.getRequestURI(), servletRequest.getMethod(), 		 IpUtils.getIpAddress(servletRequest),
                methodSignature.getDeclaringTypeName() + "." + methodSignature.getName(),
                !method.isAnnotationPresent(DisableLoginSecurity.class), logArgsSb==null?logArgsJson:logArgsSb);
       //TODO:可以执行检验入参是否符合规范的操作,抛出异常
       // 执行方法
        Object result = pjp.proceed();
       //TODO:可以执行检验响应值是否符合规范的操作,抛出异常
        String resultStr = JsonUtils.obj2String(result);
        if(resultStr!=null) {
            if(resultStr.length()>4096) {
                resultStr = resultStr.substring(0,4096);
            }
        }
        //执行日志
        log.debug("Http请求【结束】: 请求耗时秒:{},【Url】:{},【HTTP Method】:{},【IP】:{},【方法】:{},【响应报文】:{}",
                (System.currentTimeMillis() - startTime) / 1000, servletRequest.getRequestURI(), servletRequest.getMethod(),
                IpUtils.getIpAddress(servletRequest), methodSignature.getDeclaringTypeName() + "." + methodSignature.getName(),
                resultStr);
        return result;
    }
}
  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值