AOP(Aspect Orient Programming) 是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善.它以通过预编译方式和运行期动态代理方式,实现在不修改代码的情况下给程序动态统一添加额外功能的一种技术,如下图
实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解**为一个动态过程,**可以在对象运行动态植入一些扩展功能或控制对象执行.
实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核心业务.在编程实现时我们首先要完成的是核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定方式一般就是借助AOP进行实现.
AOP就是要基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能并可以"控制"对象的执行.例如AOP应用于项目中的日志处理,事务处理,权限处理,缓存处理等.
Spring AOP底层基于代理机制实现功能扩展:
Spring.aop.proxy-target-class=false
公式: AOP = 切入点表达式+通知方法
1.bean(bean的ID) 按照指定的bean名称拦截用户的请求,之后执行通知方法,只能匹配单个bean对象.
bean表达式一般应用于类级别,实现粗粒度的切入点定义.
- bean("userServiceImpl")指定一个userServiceImpl类中所有方法.
- bean("*ServiceImpl") 指定所有后缀为ServiceImpl的类中所有方法.
说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内容的名字应该是spring容器中某个bean的name(类名开头字母小写).
2.within(包名.类名) 可以按照类通配的方式去拦截用户的请求,控制粒度较粗.
within表达式应用于类级别,实现粗粒度的切入点表达式定义.
- within("com.aop.service.UserServiceImpl")指定当前包中这个类内部的所有方法.
- within("com.aop.service.*") 指定当前目录下的所有类的所有方法.
- within("com.aop.service..*") 指定当前目标已经子目录中类的所有方法.
within表达式应用场景分析:
1)对所有业务bean都要进行功能增强,但是bean名字又没有规则.
2)按业务模块(不同包下的业务)对bean对象进行业务功能增强.
3.execution (返回值类型 包名.类名.方法名(参数列表)) 方法参数级别,控制粒度较细.
- execution(void com.aop.service.UserServiceImpl.addUser()) 匹配addUser方法.
- execution(void com.aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为String的addUser方法.
- execution (* com.aop.service..*.*(..)) 万能配置
4.@annotation (包名.注解名称) 按照注解的方式去拦截用户请求,应用于方法级别,实现细粒度的切入点表达式定义.
- @annotation(com.anno.RequiredLog) 匹配有此注解描述的方法.
- @annotation(com.anno.RequiredCache) 匹配有此注解描述的方法.
其中:RequiredLog为我们自己定义的注解,当我们使用@RequiredLog注解修饰业务层方法时,系统底层会在执行此方法时进行扩展操作.
在基于Spring AOP编程的过程只中,基于AspectJ 框架标准,spring中定义了五种类型的通知(通知描述的是一种扩展业务)
1.前置通知(@Before): 主要在目标方法执行之前执行
2.后置通知(@After) : 在目标方法执行之后执行.
3. 异常通知(@AfterThrowing) : 在目标方法执行过程中出现异常执行,所以当做一些异常监控时可在此方法中进行代码实现.
4. 最终正常通知(@AfterReturning) : 在目标方法正常执行时执行
上述的通知方法,无法控制目标方法是否执行,所以一般"只做记录不做改变".
5.环绕通知(@Round) : 一般采用环绕通知实现对业务的控制.
切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低.
@Order(1)
@Aspect
@Component
public class SysLogAspect {
…
}
@Order(2)
@Aspect
@Component
public class SysCacheAspect {
…
}
说明:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤链,拦截器链,其执行分析如图:
Spring 基于AspectJ框架实现AOP设计的关键对象概览,如图:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
#redis.host=192.168.126.129
#redis.port=6379
#redis.nodes=192.168.126.129:6379,192.168.126.129:6380,192.168.126.129:6381
redis.nodes=192.168.126.129:7000,192.168.126.129:7001,192.168.126.129:7002,192.168.126.129:7003,192.168.126.129:7004,192.168.126.129:7005
@Configuration //我是一个配置类 一般都会与@Bean联用
@PropertySource("classpath:/properties/redis.properties")
public class RedisConfig {
/**
* spring整合Redis集群
*/
@Value("${redis.nodes}")
private String redisNodes;
@Bean
public JedisCluster jedisCluster() {
Set<HostAndPort> nodeSet = new HashSet<HostAndPort>();
String[] clusters = redisNodes.split(",");
for (String cluster : clusters) { //host:port
String host = cluster.split(":")[0];
int port = Integer.parseInt(cluster.split(":")[1]);
nodeSet.add(new HostAndPort(host, port));
}
return new JedisCluster(nodeSet);
}
/**
*
@Value("${redis.nodes}")
private String redisNodes; //node,node,node
*/
/*整合分片实现Redis内存扩容*/
/**
@Bean
public ShardedJedis shardedJedis() {
String[] nodes = redisNodes.split(","); //节点数组
//动态获取Redis节点信息.
List list = new ArrayList();
for (String node : nodes) { //node= host:port ---->[host,port]
String host = node.split(":")[0];
int port = Integer.parseInt(node.split(":")[1]);
list.add(new JedisShardInfo(host, port));
}
//返回分片对象
return new ShardedJedis(list);
}
**/
/**
* 单台测试
@Value("${redis.host}")
private String host;
@Value("${redis.port}")
private Integer port;
//将返回值的结果交给spring容器进行管理,如果以后想要使用该对象则可以直接注入.
@Bean
public Jedis jedis() {
return new Jedis(host, port);
}
*/
}
@Target(ElementType.METHOD) //标识注解 对谁生效
@Retention(RetentionPolicy.RUNTIME) //注解使用的有效期
public @interface CacheFind {
public String key(); //标识存入redis的key的前缀
public int seconds() default 0; //标识保存的时间 单位是秒
}
//1.将对象交给容器管理
@Component
//2.定义aop切面
@Aspect
public class CacheAOP {
@Autowired(required = false)
//private Jedis jedis; //单台redis注入
//private ShardedJedis jedis; //分片redis注入 性能更高 内存更大
private JedisCluster jedis; //集群注入,可以实现高可用.
/**
* 实现思路: 拦截被@CacheFind标识的方法 之后利用aop进行缓存的控制
* 通知方法: 环绕通知
* 实现步骤:
* 1.准备查询redis的key ITEM_CAT_LIST::第一个参数
* 2.@annotation(cacheFind) 动态获取注解的语法.
* 拦截指定注解类型的注解并且将注解对象当做参数进行传递.
*/
@SuppressWarnings("unchecked") //压制警告
@Around("@annotation(cacheFind)")
//@Around("@annotation(com.jt.anno.CacheFind)") //不需要获取注解中的内容
public Object around(ProceedingJoinPoint joinPoint,CacheFind cacheFind) {
//1.获取用户注解中的key ITEM_CAT_LIST::0
String key = cacheFind.key();
//2.动态获取第一个参数当做key
//joinPoint.getArgs() 获取的是目标方法中的参数信息
String firstArg = joinPoint.getArgs()[0].toString();
key += "::"+firstArg;
Object result = null;
//3.根据key查询redis.
if(jedis.exists(key)) {
//根据redis获取数据信息
String json = jedis.get(key);
//如何获取返回值类型
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
result = ObjectMapperUtil.toObject(json, methodSignature.getReturnType());
System.out.println("aop查询redis缓存");
}else {
//如果key不存在,则证明是第一次查询. 应该查询数据库
try {
result = joinPoint.proceed(); //目标方法返回值
System.out.println("AOP查询数据库获取返回值结果");
//将数据保存到redis中
String json = ObjectMapperUtil.toJSON(result);
int seconds = cacheFind.seconds();
if(seconds>0)
jedis.setex(key, seconds, json);
else
jedis.set(key, json);
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
return result;
}
//公式: 切面 = 切入点表达式 + 通知方法.
/**
* 业务需求: 要求拦截ItemCatServiceImpl类中的业务
* @Pointcut 切入点表达式 可以理解为就是一个if判断,只有满足条件,才能执行通知方法.
*/
/**
//@Pointcut("bean(itemCatServiceImpl)") //按类匹配,控制的粒度较粗 单个bean
//@Pointcut("within(com.jt.service..*)") //按类匹配,控制的粒度较粗 多个bean
@Pointcut("execution(* com.jt.service..*.*(..))") //细粒度的匹配方式
@Pointcut("@annotation("xxxxxx")") //只拦截特定注解标识的方法
public void pointCut() {
}
//joinPoint 方法执行切恰好被切入点表达式匹配,该方法的执行就称之为连接点.
@Before("切入点表达式") 效果一致
@Before("pointCut()") 可以共用同一个切入点.
public void before(JoinPoint joinPoint) {
System.out.println("我是前置通知!!!!");
String typeName =
joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
Object[] objs = joinPoint.getArgs();
Object target = joinPoint.getTarget();
System.out.println("方法执行的全路径为:"+typeName+"."+methodName);
System.out.println("获取方法参数:"+objs);
System.out.println("获取目标对象:"+target);
}
//添加环绕通知 可以控制目标方法执行 要求添加参数
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) {
System.out.println("我是环绕通知开始");
try {
//Object result = joinPoint.proceed();
System.out.println("我是环绕通知结束");
return null;
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException(e);
} //指定目标方法
}
**/
}
假如目标对象有实现接口,则可以基于JDK为目标对象创建代理对象,然后为目标对象进行功能扩展.
假如目标对象没有实现接口,可以基于CGLIB代理方式为目标织入功能扩展,如图:
说明: 目标对象实现了接口也可以基于CGLIB为目标对象创建代理对象.