AspectJ学习笔记

726 阅读6分钟

概念

  AOP(Aspect-oriented programming,面向切面的程序设计,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。

原理

  AspectJ是对Java编程语言的扩展,通过增加了一些新的构造块支持对横切关注点的模块化封装,通过对源代码级别的代码混合实现织入,是一种典型的使用静态织入的AOP实现机制。AspectJ提供了两种横切实现机制,一种称为动态横切(DynamicCrosscutting),另一种称为静态横切(StaticCrosscutting)。
  动态横切是指在程序执行的某一个明确的点上运行额外的,预先定义好的实现,是一种静态实现机制,并非是动态的。为了实现动态横切,AspectJ中引入了四个新的概念:连接点(JoinPoint),切入点(Pointcut),通知(Advice)和切面(Aspect)。连接点是明确定义的程序执行过程中的一个点,切入点则是指一组相关的连接点,通知定义了在连接点执行的额外实现,切面则是指对横切关注点的模块化封装实现的单元,类似于AOP中的类,由切入点,通知与普通的Java成员声明组成。如前所述,连接点是程序执行中明确定义的点,比如,类接受到方法调用时,方法调用时,属性访问时都可以是连接点,在连接点处可以执行预定义的额外实现。而要指明在哪些连接点上执行,则需要定义切入点,切入点可以在程序运行时匹配特定的连接点。在编译时,切面中的通知将被转化为标准的方法,类代码中匹配切入点的连接点将被转化为一个静态的标记点,然后,这些静态的点将被对通知所转化成的方法的调用所取代,由此完成两种代码的织入,最后对织入完成的代码编译为字节码,即完成了整个编译过程。目前,AspectJ即支持编译前的预处理方式实现代码的织入,也支持编译后的字节码操作。

AspectJ根据开发者编写的Aspect程序编织到目标程序中
  静态横切是指对已存在的类型定义引入新的方法,属性等,与动态横切不同,静态横切不改变类型的动态行为,而是改变其静态结构,也即导入(Introduction)。通过在切面代码中声明方法,属性,需要继承的超类,接口等,在代码织入时,可以改变应用此切面的类的定义。

实现

动态横切

  1. 创建一个注释Log
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
	String value() default "";
}
  1. 创建一个切面类LogAspect
@Aspect
@Component
public class LogAspect {

	@Pointcut("@annotation(com.springboot.annotation.Log)")
	public void pointcut() {
	}

	@Around("pointcut()")
	public void around(ProceedingJoinPoint point) {
		long beginTime = System.currentTimeMillis();
		try {
			// 执行方法
			point.proceed();
		} catch (Throwable e) {
			e.printStackTrace();
		}
		// 执行时长(毫秒)
		long time = System.currentTimeMillis() - beginTime;
		// 保存日志
		pringLog(point, time);
	}

private void pringLog(ProceedingJoinPoint joinPoint, long time){
	    StringBuilder stringBuilder = new StringBuilder();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Log logAnnotation = method.getAnnotation(Log.class);
        if (logAnnotation != null) {
            // 注释上的描述
            stringBuilder.append("注解上的value参数值为:").append(logAnnotati> on.value()).append("\n");
        }
        // 请求的方法名
        String className = joinPoint.getTarget().getClass().getName();
        stringBuilder.append("类名:").append(className).append("\n");
        String methodName = signature.getName();
        stringBuilder.append("方法名:").append(methodName).append("\n");
        // 请求的方法参数值
        Object[] args = joinPoint.getArgs();
        // 请求的方法参数名称
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames = u.getParameterNames(method);
        if (args != null && paramNames != null) {
            StringBuilder params = new StringBuilder();
            for (int i = 0; i < args.length; i++) {
                params.append("  ").append(paramNames[i]).append(": ").append(args[i]);
            }
            stringBuilder.append("参数:").append(params).append("\n");
        }
        // 获取request
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        // 设置IP地址
        String ipAddr = IPUtils.getIpAddr(request);
        stringBuilder.append("请求的ip地址:").append(ipAddr).append("\n");
        Date date = new Date();
        stringBuilder.append("时间:").append(date).append("\n");
        System.out.println(stringBuilder.toString());
    }
}

  这个类首先使用了@Aspect注释开启了 3. 使用Log注释在需要切面的方法上进行注释

@RestController
public class TestController {

	@Log("执行方法一")
	@GetMapping("/one")
	public void methodOne(String name) {
		
	}
	
	@Log("执行方法二")
	@GetMapping("/two")
	public void methodTwo() throws InterruptedException {
		Thread.sleep(2000);
	}

	@Log("执行方法三")
	@GetMapping("/three")
	public void methodThree(String name, String age) {
		
	}
}
  1. 测试
  • 访问http://localhost:8080/web/one?name=yellowdoge

  控制台打印输出如下

测试结果

  • 访问http://localhost:8080/web/two   控制台打印输出如下
    测试结果
  • 访问http://localhost:8080/web/three?name=yellow&age=100   控制台打印输出如下

静态横切

  1. 环境配置   我用的是IDEA,使用静态横切要先安装aspectj,然后把编译器切换成ajc并且在项目中导入aspectjrt.jar。
  2. 新建一个Person类。
public class Person {
    private String name;
    private String mobile;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }
}
  1. 新建一个MobileValidatable接口。
public interface MobileValidatable {
    boolean isMobile();
}
  1. 新建一个PersonMobileValidateAspect.aj文件
public aspect PersonMobileValidateAspect {
    //让Person类实现MobileValidatable接口
    declare parents: Person implements MobileValidatable;
//    pointcut isMobile() : execution(* Person.setMobile(mobile));
    //实现MobileValidatable接口中的isMobile
    public boolean Person.isMobile(){
        final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
        if(this.getMobile()==null||"".equals(this.getMobile())){
            return false;
        }
        Matcher m = mobile_pattern.matcher(this.getMobile());
        return m.matches();
    }
//    Object around() : isMobile() {
//        Object ret = proceed();
//        System.out.println("around");
//        return ret; //Sguiggly line under the return ret;
//    }
//    public Person.new(String name, String mobile){
//        super();
//        this.setName(name);
//        this.setMobile(mobile);
//    }
}
  1. 测试
public class TestCase {
    public static void main(String[] args) {
        Person person = new Person();
//        Person person1 = new Person("123","123");
        person.setName("yellowdoge");
        person.setMobile("");
        System.out.println("Person类是否已经继承MobileValidatable接口:"+ (person instanceof MobileValidatable));
        System.out.println("\"\"是否是合格的手机号码:" + person.isMobile());
        person.setMobile("12345678901");
        System.out.println("\"12345678901\"是否是合格的手机号码:" + person.isMobile());
    }
}

测试结果

  1. 编译后的结果
    主要看Person类和PersonMobileValidateAspect的编译结果
Person.class
public class Person implements MobileValidatable {
    private String name;
    private String mobile;

    public Person() {
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getMobile() {
        return this.mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public boolean isMobile() {
        return PersonMobileValidateAspect.ajc$interMethod$com_example_test_aspectJ_PersonMobileValidateAspect$com_example_test_aspectJ_Person$isMobile(this);
    }
}
PersonMobileValidateAspect.class
@Aspect
public class PersonMobileValidateAspect {
    static {
        try {
            ajc$postClinit();
        } catch (Throwable var1) {
            ajc$initFailureCause = var1;
        }

    }

    public PersonMobileValidateAspect() {
    }
//MobileValidatable中isMobile的实现
    public static boolean ajc$interMethod$com_example_test_aspectJ_PersonMobileValidateAspect$com_example_test_aspectJ_Person$isMobile(Person ajc$this_) {
        Pattern mobile_pattern = Pattern.compile("1\\d{10}");
        if (ajc$this_.getMobile() != null && !"".equals(ajc$this_.getMobile())) {
            Matcher m = mobile_pattern.matcher(ajc$this_.getMobile());
            return m.matches();
        } else {
            return false;
        }
    }
//异常相关
    public static PersonMobileValidateAspect aspectOf() {
        if (ajc$perSingletonInstance == null) {
            throw new NoAspectBoundException("com_example_test_aspectJ_PersonMobileValidateAspect", ajc$initFailureCause);
        } else {
            return ajc$perSingletonInstance;
        }
    }

    public static boolean hasAspect() {
        return ajc$perSingletonInstance != null;
    }
}

  可以看出aspectJ在编译时帮我们实现了Person对MobileValidatable的继承,而且aj文件也是被编译成了class文件。而接口的实现最后是通过调用的PersonMobileValidateAspect.class中的ajc$interMethod$com_example_test_aspectJ_PersonMobileValidateAspect$com_example_test_aspectJ_Person$isMobile代理方法来实现。

参考文献: