Spring AOP那些事儿

1 AOP前言

散布于应用中多处的功能(日志、安全、事务管理等)被称为横切关注点。

把横切关注点与业务逻辑分离是AOP要解决的问题。

1.1 编程范式

面向过程编程(Normal普遍)

面向对象编程(Java OOP)

函数式编程(Java JDK8)

事件驱动编程(Java GUI、或者Anroid手机编程)

面向切面编程(Spring AOP)

1.2 AOP的初衷

DRY:Don't Repeat Yourself

Soc:Separation of Concerns

    -水平分离:展示层->服务层->持久层

    -垂直分离:模块划分(订单、库存)

    -切面分离:分离功能性需求与非功能性需求

1.3 AOP的应用场景

权限控制(@PreAuthorize)

缓存控制(@Cacheable)

事务控制(@Transactional)

审计日志

性能监控

分布式跟踪

异常处理

1.4 AOP简介

  • AOP(Aspect-Oriented Programming, 面向切面编程): 是一种新的方法论, 是对传统 OOP(Object-Oriented Programming, 面向对象编程) 的补充.
  • AOP 的主要编程对象是切面(aspect), 而切面模块化横切关注点(即对象里放入的是一个个横切关注点的方法).
  • 在应用 AOP 编程时, 仍然需要定义公共功能, 但可以明确的定义这个功能在哪里, 以什么方式应用, 并且不必修改受影响的类. 这样一来横切关注点就被模块化到特殊的对象(切面)里.
  • AOP 的好处: 每个事物逻辑位于一个位置, 代码不分散, 便于维护和升级 业务模块更简洁, 只包含核心业务代码.

  • 这里写图片描述

  • 这里的一个个需求,例如验证参数、日志功能等都是横切关注点,我们将横切关注点抽取出来

  • 通过切面和业务逻辑的结合实现目标功能,即为面向切面编程。

1.5 AOP术语

  • 切面(Aspect): 横切关注点(一个个具体需求)(跨越应用程序多个模块的功能)被模块化的特殊对象
  • 通知(Advice): 切面必须要完成的工作(比如说切面需要完成验证,即切面中的每一个方法)
  • 目标(Target): 被通知的对象(即业务逻辑)
  • 代理(Proxy): 向目标对象应用通知之后创建的对象(将切面和目标混合)
  • 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如 ArithmethicCalculator#add() 方法执行前的连接点,执行点为 ArithmethicCalculator#add(); 方位为该方法执行前的位置
  • 切点(pointcut):每个类都拥有多个连接点:例如 ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

 

2 AOP注解应用

2.1 @Aspect

2.2 @PointCut

2.2.1 切面表达式Pointcut expression

1)designators指示器:

  • 匹配方法:execution(

                        modifier-pattern?​​​​​​​

                        ret-type-pattern

                        declaring-type-pattern?

                        name-pattern(param-pattern)

                        throws-pattern?

                        )   必会,问号代表可无

  • 匹配注解:@target()/@args()/@within()/@annotation() 了解
  • 匹配包、类型:within() 了解
  • 匹配对象:this()/bean()/target() 了解
  • 匹配参数:args() 必会

2)Wildcards通配符:

  • * 匹配任意数量的字符
  • .. 匹配任意数的子包或参数
  • + 匹配指定类及其子类

3)operators操作符:

  • && 与
  • || 或
  • !非

例子:

匹配包、类型(了解)

//匹配ProductService类里头的所有方法
@Pointcut("within(com.imooc.service.ProductService)")

//匹配com.imooc包及子包下所有类的方法
@Pointcut("within(com.imooc..*)")

匹配对象(了解)

//匹配AOP对象的目标对象为指定类型的方法,即LogService的aop代理对象的方法
@Pointcut("this(com.imooc.log.Loggable)")

//匹配实现Loggable接口的目标对象(而不是aop代理后的对象)的方法
@Pointcut("target(com.imooc.log.Loggable)")

//this 可以拦截 DeclareParents(Introduction)
//target 不拦截 DeclareParents(Introduction)
//匹配所有以Service结尾的bean里头的方法
@Pointcut("bean(*Service)")

匹配注解(了解),终于知道@Transactional是怎样通过AOP管理事务的

//匹配方法标注有AdminOnly的注解的方法
@Pointcut("@annotation(com.imooc.anno.AdminOnly) && within(com.imooc..*)")

//匹配标注有NeedSecured的类底下的方法 //class级别
@Pointcut("@within(com.imooc.anno.NeedSecured) && within(com.imooc..*)")

//匹配标注有NeedSecured的类及其子类的方法 //runtime级别
//在spring context的环境下,二者没有区别
@Pointcut("@target(com.imooc.anno.NeedSecured) && within(com.imooc..*)")

//匹配传入的参数类标注有Repository注解的方法
@Pointcut("@args(com.imooc.anno.NeedSecured) && within(com.imooc..*)")

匹配参数(必会)

//匹配任何以find开头而且只有一个Long参数的方法
@Pointcut("execution(* *..find*(Long))")

//匹配任何以find开头的而且第一个参数为Long型的方法
@Pointcut("execution(* *..find*(Long,..))")

//匹配任何只有一个Long参数的方法
@Pointcut("within(com.imooc..*) && args(Long)")

//匹配第一个参数为Long型的方法
@Pointcut("within(com.imooc..*) && args(Long,..)")

匹配方法(必会)

//匹配任何公共方法
@Pointcut("execution(public * com.imooc.service.*.*(..))")

//匹配com.imooc包及子包下Service类中无参方法
@Pointcut("execution(* com.imooc..*Service.*())")

//匹配com.imooc包及子包下Service类中的任何只有一个参数的方法
@Pointcut("execution(* com.imooc..*Service.*(*))")

//匹配com.imooc包及子包下任何类的任何方法
@Pointcut("execution(* com.imooc..*.*(..))")

//匹配com.imooc包及子包下返回值为String的任何方法
@Pointcut("execution(String com.imooc..*.*(..))")

//匹配异常
execution(public * com.imooc.service.*.*(..) throws java.lang.IllegalAccessException)

2.2.2 通知Advice 参数与结果绑定

@Before前置通知

@After后置通知,方法执行完之后

@AfterThrowing异常通知,抛出异常之后

@AfterReturning返回通知,成功执行之后

@Around 环绕通知,一定要有返回值Object,否则程序返回值为null

package com.example.demo.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ControllerAspect {

    @Pointcut("execution(String com.example.demo.service.*.*(String))")
    public void pointcut(){}

    @Before("pointcut() && args(para)")
    public void before(String para){
        System.out.println("before:"+para);
    }

    @After("pointcut() && args(para)")
    public void after(String para){
        System.out.println("after:"+para);
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint){
        Object obj = null;
        try {
            obj = proceedingJoinPoint.proceed();

        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        String methodName = proceedingJoinPoint.getSignature().getName();
        System.out.println("methodName:"+methodName);
        Object[] args = proceedingJoinPoint.getArgs();
        System.out.println("around:"+args);
        return obj;
    }

    @AfterReturning(value="pointcut()",returning="ret")
    public void afterReturning(String ret){
        System.out.println("afterReturning:"+ret);
    }

    @AfterThrowing(value="pointcut()",throwing="e")
    public void afterThrowing(Exception e){
        System.out.println("afterThrowing:"+e.getMessage());
    }

}

2.2.3 Restful记录日志与异常处理

package com.hrhx.springboot.aop;

import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.hrhx.springboot.exception.CheckException;
import com.hrhx.springboot.jopo.ResultBean;
import com.hrhx.springboot.util.JsonUtil;
@Aspect
@Component
public class ControllerAOP {
	//private final Logger logger = Logger.getLogger(getClass());
	private static final Logger logger = LoggerFactory.getLogger(ControllerAOP.class);

	@Pointcut("execution(public com.hrhx.springboot.jopo.ResultBean *(..))")
    public void webLog(){}
	
	@Around("webLog()")
	public Object handlerControllerMethod(ProceedingJoinPoint pjp) {
		
		long startTime = System.currentTimeMillis();
		ResultBean<?> result;
		try {
			result = (ResultBean<?>) pjp.proceed();
			logger.info(pjp.getSignature() + "use time:" + (System.currentTimeMillis() - startTime));
			// 接收到请求,记录请求内容
	        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
	        HttpServletRequest request = attributes.getRequest();
	        // 记录下请求内容
	        logger.info("URL : " + request.getRequestURL().toString());
	        logger.info("HTTP_METHOD : " + request.getMethod());
	        logger.info("IP : " + request.getRemoteAddr());
	        logger.info("CLASS_METHOD : " + pjp.getSignature().getDeclaringTypeName() + "." + pjp.getSignature().getName());
	        logger.info("ARGS : " + Arrays.toString(pjp.getArgs()));
		} catch (Throwable e) {
			result = handlerException(pjp, e);
		}
		return result;
		
	}

	@AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(Object ret) throws Throwable {
        // 处理完请求,返回内容
        logger.info("RESPONSE : " + new JsonUtil().toStrings(ret));
    }
	
	@SuppressWarnings("rawtypes")
	private ResultBean<?> handlerException(ProceedingJoinPoint pjp, Throwable e) {

		ResultBean<?> result = new ResultBean();
		// 已知异常
		if (e instanceof CheckException) {
			result.setMsg(e.getLocalizedMessage());
			result.setCode(ResultBean.FAIL);
		} else {
			logger.error(pjp.getSignature() + " error ", e);
			result.setMsg(pjp.getSignature() + e.toString());
			result.setCode(ResultBean.FAIL);
			// 未知异常是应该重点关注的,这里可以做其他操作,如通知邮件,单独写到某个文件等等。
		}
		return result;
		
	}

}

2.2.4 日志切面优先级

/**
 * 日志切面
 * @author Megustas
 *
 */
//把这个类声明为一个切面:首先需要把该类放入到IOC容器中,通过注解@Component、再声明为一个切面,通过注解@Aspect,并且在配置文件中加入配置
//通过Order注解来指定切面的优先级,优先级数字越小代表优先级越高,越先执行
@Order(2)
@Aspect
@Component
public class LoggingAspect {

    //这个方法在哪些类的哪些方法前执行,通过注解来规定
    //声明该方法是一个前置通知,在目标方法开始之前执行,".add"方法说明在add方法之前执行,".*"则表示在包下所有方法之前执行
    @Before("execution(public int com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println("The method " + methodName + " begins with " + args);
    }

    //后置通知:在目标方法执行后(无论是否发生异常),执行的通知
    //在后置通知中还不能访问目标目标方法执行的结果,执行结果在返回通知中进行访问
    @After("execution(* com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.*(int, int))")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " end ");
    }

    /*
     * 返回通知:在方法正常结束后执行的代码,返回通知是可以访问到方法的返回值的
     */
    @AfterReturning(value="execution(public int com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))",
            returning="result")
    public void afterReturning(JoinPoint joinPoint,Object result){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends with " + result);
    }

    /**
     * 在目标方法出现异常时会执行的代码
     * 可以访问到异常对象,且可以指定在出现特定异常时再执行通知代码
     * 例如Exception ex,NullPointerException ex,可以指定不同种类的异常,当是指定的异常种类时执行
     * @param joinPoint
     * @param ex
     */
    @AfterThrowing(value="execution(public int com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.div(int, int))",
            throwing="ex")
    public void afterThrowing(JoinPoint joinPoint,Exception ex){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " occurs excetion " + ex);
    }

    /**
     * 环绕通知需要携带ProceedingJoinPoint类型的参数
     * 环绕通知类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法
     * 并且环绕通知必须有返回值,返回值即为目标方法的返回值(类似于动态代理)
     * 最强的,前置、后置、返回与异常通知都可以,但是并不代表是最常用的
     * @param pjd
     */
    @Around("execution(* com.atguigu.spring.aop.impl.ArithmeticCalculatorImpl.add(int, int))")
    public Object aroundMethod(ProceedingJoinPoint pjd){

        Object result = null;
        String methodName = pjd.getSignature().getName();
        //执行目标方法
        try {
            //前置通知
            System.out.println("The method " + methodName + "begins with" + Arrays.asList(pjd.getArgs()));
            result = pjd.proceed();
            //后置通知
            System.out.println("The method " + methodName + " ends with " + result);
        } catch (Throwable e) {
            // 异常通知
            System.out.println("The method " + methodName + " occurs excetion " + e);
        }
        //后置通知
        System.out.println("The method " + methodName + " ends" );
        return result;
    }

}

3 原理概述:织入的时机

    编译期(AspectJ)

    类加载时(AspectJ 5+)

    运行时(Spring AOP):静态代理:代理模式;动态代理:JDK接口代理、Cglib继承代理

    多个AOP链式调用,Order(0):责任链模式

@EnableDiscoveryClient
@SpringBootApplication
//强制使用cglib代理
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Application{
	public static void main(String[] args) {  
        SpringApplication.run(Application.class,args);   
    }  
}

4 使用SpringAOP的注意事项/坑

不宜把重要的业务逻辑放到aop中处理

无法拦截static、final、private方法

无法拦截内部方法调用:SpringAOP用的是代理模式,所以spring容器中的对象才能被代理,被切面。

 

 

 

转载于:https://my.oschina.net/duhongming52java/blog/1637789

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值