Spring 之 AOP
概述
Spring的最终目的是简化应用开发。通俗的讲减少重复代码,少写代码达到相同的目的。面向切面编程(AOP, Aspect Oriented Programming)就是一种减重复代码方式。我们都知道JAVA是一门面向对象编程(OOP, Object Oriented Programming)语言,在java中将一个个功能模块抽象成一个个对象。这些对象通过一定的联系完成我们所看到的一个个应用,一个个服务。它的核心就是对象(Object)。
要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。
类应该是纯净的,不应含有与本身无关的逻辑。单一职责原则。
切面编程,可以带来代码的解耦;同时,切面编程也是需要执行代码的,增加了一些额外的代码执行量。因地制宜,使用AOP。
他山之石
- 轻松理解AOP思想(面向切面编程):www.cnblogs.com/Wolfmanlq/p…
- AOP那点事儿:面向切面编程:my.oschina.net/huangyong/b…
- OOP的完美点缀—AOP之SpringAOP实现原理:www.cnblogs.com/chenjunping…
- Spring系列之AOP: www.cnblogs.com/xiaoxi/p/59…
具象化理解
我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。
按照正常的逻辑,我们可以这么做。
这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。
同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。
红框处,就是面向切面编程。
理论
使用AOP之前,我们需要理解几个概念。
连接点(Join Point)
所有可能的需要注入切面的地方。如方法前后、类初始化、属性初始化前后等等。
切点(Poincut)
需要做某些处理(如打印日志、处理缓存等等)的连接点。如何来指明一个切点?spring使用了AspectJ的切点表达式语言来定义Spring切面。
多个表达式之间,可用“and” 、“or”、“not”做逻辑连接。
其中较为复杂的是execution。
目前,Spring只支持方法的切点定义
通知(Advice)
定义在什么时候做什么事情。spring支持5种方法上的通知类型
切面(Aspect)
通知+切点的集合,定义在什么地方什么时间做什么事情。
引入(Introduction)
允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗
目标(Target)
引入中提到的目标类,也就是要被通知的对象。也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。
织入(Weaving)
把切面应用到目标对象来创建新的代理对象的过程。
spring中AOP实现原理
AOP的实现实际上是用的是代理模式。
代理
代理概念:简单的理解就是通过为某一个对象创建一个代理对象,我们不直接引用原本的对象,而是由创建的代理对象来控制对原对象的引用。
按照代理的创建时期,代理类可以分为两种。
- 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
- 动态代理:在程序运行时,运用反射机制动态创建而成,无需手动编写代码。动态代理不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java反射机制可以生成任意类型的动态代理类。
代理原理:代理对象内部含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。
spring使用的是动态代理。
动态代理之JDK
java jdk 本身支持动态创建代理对象。
java的动态代理,有个缺点,它只支持实现了接口的类。因此可以引入cglib(Code Generation Library)三方库支持类的代理。 对JDK动态代理有兴趣的可以参考一下:blog.csdn.net/u012410733/…
动态代理之cglib
对cglib库有兴趣可以参考一下:blog.csdn.net/danchu/arti…
样例
有这么一个需求,对某些接口做缓存或打印日志。有不想每个接口中都调用缓存方法或打印日志方法。可以这么来做。 代码:
- 定义动物抽象接口:Animal
- 定义具体动物猫:Cat
- 定义bean扫描配置类:AnimalConfig
- 定义缓存注解:Cache
- 定义缓存切面:CacheAspect
- 定义日志切面:LogAspect
- 定义测试入口类:App
1. Animal接口
package bean;
public interface Animal {
public String sayName(String name, Integer num);
}
复制代码
2. Cat类
package bean;
import org.springframework.stereotype.Component;
@Component
public class Cat implements Animal {
@Cache(60)
public String sayName(String name, Integer num) {
return "this is cat " + name + "," + num;
}
}
复制代码
3. AnimalConfig spirng配置扫描类
package bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AnimalConfig {
}
复制代码
4.Cache注解(与CacheAspect配合使用)
package bean;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cache {
int value() default 0;
}
复制代码
5.CacheAspect 缓存切面
package bean;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
@Aspect
@Order(1000)
public class CacheAspect {
// 定义切入点:带有Cache注解的方法
@Pointcut("@annotation(Cache)")
private void cache(){}
// 临时存储区
private static Map<String, Object> cacheList = new LinkedHashMap<String, Object>();
// 定义环绕通知,处理接口/方法添加缓存
@Around("cache()")
private Object cacheAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
Object object = proceedingJoinPoint.getTarget();
Object[] args = proceedingJoinPoint.getArgs();
String className = object.getClass().getName();
MethodSignature signature = (MethodSignature)proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
// 组装cache key
String cacheKey = className + "_" + method.getName() + "_" + JSONObject.toJSONString(args);
if (cacheList.containsKey(cacheKey)){
System.out.println("data get cache");
return cacheList.get(cacheKey);
}else {
System.out.println("data get db");
Object result = proceedingJoinPoint.proceed();
cacheList.put(cacheKey, result);
return result;
}
}
}
复制代码
6.LogAspect 日志切面
package bean;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Order(100)
public class LogAspect {
//定义切点:包含任意参数的、任意返回值、的公共方法sayName
@Pointcut("execution(public * *sayName(..))")
private void log(){}
//定义环绕通知:处理日志注入
@Around("log()")
private Object logAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("before, params:" + JSONObject.toJSONString(args));
Object result = proceedingJoinPoint.proceed();
System.out.println("after, result:" + JSONObject.toJSONString(result));
return result;
}
}
复制代码
7. APP 测试入口类
import bean.Animal;
import bean.AnimalConfig;
import bean.Cat;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class App {
public static void main(String[] args){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AnimalConfig.class);
Animal animal = applicationContext.getBean("cat", Cat.class);
String result = animal.sayName("rudytan", 12);
String result1 = animal.sayName("rudytan", 12);
String result2 = animal.sayName("rudytan", 12);
String result3 = animal.sayName("rudytan", 13);
String result4 = animal.sayName("rudytan", 13);
System.out.println(result);
}
}
复制代码
说明:
- @Cache为自定义注解(标记该方法需要缓存)
- @EnableAspectJAutoProxy 为是否开启cglib动态代理
- @Aspect为定义该类为切面类
- @Order为当有多个切面类的时候,定义执行顺序,数值越大,约先执行。
- @Pointcut定义当前函数为切点。
- @Around定义环绕通知