Spring AOP简介
1.1 AOP概述
1.1.1 AOP是什么
AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面相切面编程,它是面向对象编程(OOP)的一种补充和完善,它以通过预编译方式和运行期动态代理方式,实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。
作用:降低系统中代码的耦合性,并且在不改变原有代码的条件下对原有的方法进行功能的扩展
公式:AOP = 切入点表达式+通知方法
1.1.2 通知类型
- 前置通知 目标方法执行之前执行
- 后置通知 目标方法执行之后执行
- 异常通知 目标方法执行过程中抛出异常时执行
- 最终通知 无论什么时候都要执行的通知
特点:四大通知类型 不能干预目标方法是否执行。一般用来做程序运行状态的记录监控
环绕通知:在目标方法执行前后都要执行的通知方法,该方法可以控制目标方法是否运行.joinPoint.proceed();功能作为强大的。
1.1.2AOP和OOP的区别
AOP和OOP字面意思相近,但其实两者完成是面向不同领域的设计思想。OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获取逻辑过程的中各部分之间低耦合的隔离效果,这两种设计思想在目标上有着本质的差异。OOP面向名次领域,AOP面向动词领域。
1.2AOP应用场景分析
实际项目中通常会将系统分为两大部分,一部分是核心业务,一部分是非核心业务,在编程实现时,我们首先要实现核心业务的实现,非核心业务一般是通过特定方式切入到系统中,这种特定的方式借助AOP进行实现。
AOP就是要基于OCP(开闭原则),在不改变原有系统核心业务的基础上动态添加一些扩展功能并可以“控制”对象的执行。
1.3 String AOP应用原理分析
1)假如目标对象(被代理对象)实现接口,则底层可以采用JDK动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
2)假如目标对象(被代理对象)没有实现接口,则底层可以采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。
1.4 Spring中AOP相关术语分析
- 切面(aspect):横切面对象,一般为一个具体类对象(可以借助@Aspect声明)。
- 通知(Advice):在切面的某个特定连接点上执行的动作(扩展功能),例如around,before,after等。
- 连接点(joinpoint):程序执行过程中某个特定的点,一般指定被拦截的目标方法。
- 切入点(pointcut):对多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集合。
例如:简单的讲一个机场的安检口理解为连接点,多个安检口为切入点,安全检查过程看是通知。
2. Spring AOP快速实践
2.1创建maven项目或者在原有项目基础上添加AOP启动依赖
org.springframework.boot
spring-boot-starter-aop
注:基于此依赖spring可以整合AspectJ框架快速完成AOP的基本实现,AspectJ是一个面向切面的框架,定义了AOP的一些语法,有一个专门的字节码生成器来生成遵守java规范的class文件。
2.2 扩展业务分析及实现
将此日志切面类作为核心业务增强(一个横切面对象)类,用于输出业务执行时长。
package com.cy.pj.common.aspect;
@Aspect
@Slf4j
@Component
public class SysLogAspect {
@Pointcut("bean(sysUserServiceImpl)")
public void doLogPointCut() {}
@Around("doLogPointCut()")
public Object around(ProceedingJoinPoint jp)
throws Throwable{
try {
log.info("start:{}"+System.currentTimeMillis());
Object result=jp.proceed();//最终会调用目标方法
log.info("after:{}"+System.currentTimeMillis());
return result;
}catch(Throwable e) {
log.error("after:{}",e.getMessage());
throw e;
}
}
}
- @Aspect 注解用于标识或者描述AOP中的切面类型,基于切面类型构建的对象用于为目标对象进行功能扩展或控制目标对象的执行
- @Pointcut注解用于描述切面中的方法,并定义切面的切入点,(基于热定表达式的方式进行描述),在上述案例中切入点表达式用的是bean表达式,这个表达式以bean开头,bean括号中的内容为一个spring管理的某个bean对象的名字。
- @Around注解用于描述切面中的方法,这样的方法被认为是一个环绕通知(核心业务方法执行之前和之后要执行的一个动作),@Aournd注解内部value属性的值为一个切入点表达式或者是切入点表达式的一个引用(这个引用为一个@PointCut注解描述的方法的方法名)。
- ProceedingJoinPoint类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息,只能用于@Around注解描述的方法参数。
2.3AOP相关对象分析
2.4扩展业务织入增强分析
2.4.1 基于JDK代理方式实现
目标对象有实现接口,可以基于JDK为目标对象创建代理对象,然后为目标哦对象进行功能扩展
2.4.2基于CGLIB代理方式实现
目标对象没有实现接口,(当然是实现了接口也是可以的),可以基于CGLIB代理方式为目标对象织入扩展功能。
目标对象实现了接口也可以结余CGLIB为目标对象创建代理对象。
3. Spring AOP编程增强
3.1 切面通知应用增强
基于SpringAOP编程中,基于AspectJ框架标准,spring中定义了五种类型通知(通知描述的是一种扩展业务):
- @Before
- @AfterReturning
- @AfterThrowing
- @After
- @Around(优先级最高!重点掌握)
3.1.2通知执行顺序
这些通知全部写在一个切面对象的倩况下,其执行顺序以及过程:
注:在实际项目可能不会在切面中定义所有的通知,具体要结合业务进行实现.
3.2切入点表达式增强
理解:切入点表达式就是一个程序是否进入通知的一个判断(IF)
作用:当程序运行过程中,满足了切入点表达式时才会去执行通知方法,实现业务的扩展。
扩展(写法):
- bean(bean的名称bean的ID)只能拦截具体的某个bean对象只能匹配一个对象
lg:bean("itemServiceImpl") - withhin(包名.类名)whithin("com.jt.service.*") 可以匹配对个对象
粗粒度的匹配原则 按类匹配
3. execution(返回值类型 包名.类名.方法名(参数列表)) 最为强大的用法
lg : execution(* com.jt.service..*.*(..))
返回值类型任意 com.jt.service包下的所有的类的所有的方法都会被拦截.
4.@annotation(包名.注解名称) 按照注解匹配.
3.2.1bean表达式(重点)
bean表达式一般应用于类级别,实现粗粒度的切入点定义.
案例分析:
- bean("userServiceImpl")指定一个userServiceImpl类中所有方法
- bean("*ServiceImpl")指定所有后缀为ServiceImpl的类中所有方法。
注:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的名字应该是spring容器中某个bean的name。
3.2.2 within表达式(了解)
within表达式应用于类级别,实现粗粒度的切入点表达式定义;
案例分析:
- within("aop.service.UserServiceImpl")指定当前包中这个类内部的所有方法。
- within("aop.service.*")制定当前目录下的所有类的所有方法。
- within("aop.service..*")指定当前目录下以及子目录中类的所有方法。
3.2.3 execution表达式(了解)
execution表达式应用于方法级别,实现细粒度的切入点表达式定义。
案例分析:
语法: execution(返回值类型 包名。类名。方法名(参数列表))。
- execution(void aop.service.UserServiceImpl.addUser())--匹配addUser方法;
- execution(void aop.service.PersonServiceImpl.addUser(String))--方法参数必须为String的addUser方法;
- execution(aop。service...*(..))--万能配置。
3.2.4 @annotation表达式(重点)
@annotation表达式应用于方法级别,实现细粒度的切入点表达是定义,
案例分析:
- @annotation(anno.RequiredLog)--匹配有此注解描述的方法;
- @annotation(anno.RequiredCache)--匹配有此注解描述的方法。
注:RequiredLog为我们自己定义的注解,当我们使用@RequiredLog注解修饰业务层方法时,系统底层会执行此方法时进行日志扩展操作。
3.3 切面优先级设置实现
切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低。例如:
定义日志切面并制定优先级。
@Order(1)
@Aspect
@Component
public class SysLogAspect {
...
}
定义缓存切面并制定优先级:
@Order(2)
@Aspect
@Component
public class SysCacheAspect {
…
}
注:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤链、拦截器链。
3.4 关键对象与术语总结
Spring 基于AspectJ框架实现AOP设计的关键对象概览:
4 Spring AOP事务处理
4.1 Spring中事务简介
4.1.1 事务定义
事务(Transaction)是一个业务,是一个不可分割的逻辑工作单元,基于事务可以更好的保证业务的正确性。
4.1.2 事务特性
事务具备ACID特性,分别是:
- 原子性(Atomicity):一个事务中的多个操作要么都成功要么都失败。
- 一致性(Consistency):例如存钱操作,存之前和存之后的总钱数应该是一致的。
- 隔离性(lsolation):事务与事务应该是相互隔离的。
- 持久性(Durability):事务一旦提交,数据要持久保存。
注:目前在事务一致性方面,通常会做一定的优化,比如说只要最终一致就可以了,这样的事务我们通常称之为柔性事务(只要最终一致就可以了)。
4.2 Spring中事务管理
4.2.1 spring中事务方式概述
- spring框架中提供了一种声明式事务的处理方式,此方式基于AOP处理,可以将具体业务逻辑与事务处理进行解耦。也就是让我们的业务代码逻辑不受污染或少量污染,就可以实现事务控制。
- 在Soring-boot项目中,其内部提供了事务的自动配置,当我们在项目中添加了指定依赖spring-boot-starer-jdbc时,框架会自动为我们的项目注入事务管理器对象,最常用的为DateSourceTransactionManager对象。
4.2.2 Spring中事务管理实现
最常用的注解方式的事务管理,以注解@Teansactional配置方式为例。
基于@Transactional注解进行声明式管理的实现步骤分为两步:
- 启用声明式事务管理,在项目启动类上添加@EnableTransactionManagement,新版本中也可不添加。
- 将@Transactional注解添加到合适的业务类或方法上,并设置合适的属性信息。
@Transactional(timeout = 30,
readOnly = false,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Throwable.class,
propagation = Propagation.REQUIRED)
@Service
public class implements SysUserService {
@Transactional(readOnly = true)
@Override
public PageObject findPageObjects(
String username, Integer pageCurrent) {
…
}
}
注:@Transactional注解用于描述类或方法,告诉spring框架我们我在此类的方法执行时进行事务控制。
- 当@Transactional注解应用在类上时表示类中所有方法启动事务管理,并且一般用于事务共性的定义。
- 当@Transacctional描述方法时表示此方法要进行事务管理,假如类和方法上都有@Transactional注解,则方法上的事务特性优先级比较高。
@Transactional常用属性应用说明:
- timeout:事务的超时时间,默认值为-1,表示没有超时显示。如果配置了具体时间,则超过该时间限制但事务还没有完成,则自动回滚事务。这个时间的记录方式是在事务开启以后到sql语句执行之前。
- read-only:指定事务是否为只读事务,默认值为false;为了忽略那些不需要事物的方法,比如读取数据,可以设置read-only为true。对添加,修改,删除业务read-only的值应该为false。
- rollback-for:用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。
- no-rollback-for:抛出no-rollback-for指定的异常类型,不回滚事务。
- isolation事务的隔离级别,默认值采用 DEFAULT。当多个事务并发执行时,可能会出现脏读,不可重复读,幻读等现象时,但假如不希望出现这些现象可考虑修改事务的隔离级别(但隔离级别越高并发就会越小,性能就会越差)
Spring 中事务控制过程分析:
spring事务管理是基于接口代理(JDK)或动态字节码(CGLIB)技术,然后通过AOP实施事务增强的,当我们执行添加了事物特性的目标方式时,系统会通过目标对象的代理对象调用DataSourceTransactionManager对象,在事务开始时,执行doBegin方法,事务结束时执行doCommit或doRollback
方法。
4.2.3 Spring中事务传播特性
事务传播(propagation)特性指"不同业务(service)对象"中的事务方法之间相互调用时,事务的传播方式。
常用事务传播方式:
- @Transactional(propagation=Propagation.REQUIRED)
如果没有事务创建新事物,如果当前有事务参与当前事务,spring默认的事务传播行为是PROPAGATION_REQUIRED,适合于绝大多数的情况。
代码示例:
@Transactional(propagation = Propagation.REQUIRED)
@Override
public List findZtreeMenuNodes() {
return sysMenuDao.findZtreeMenuNodes();
}
当有一个业务对象调用方法时,此方法始终工作在一个已经存在的事务方法,或者是由调用者创建的一个事务方法中。
- @Transactional(propagation=Proagation.REQUIRES_NEW)
必须是新事务,如果有当前事务,挂起当前事务并且开启新事物。
代码示例如下:
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void saveObject(SysLog entity) {
sysLogDao.insertObject(entity);
}
当有一个业务对象调用如上业务方法时,此方法会始终运行在一个新的事务中。
4.3 Spring中事务管理小结
Spring声明式事务是 Spring 最核心,最常用的功能。由于 Spring 通过 IOC和AOP的功能非常透明地实现了声明式事务的功能,对于一般的开发者基本上无序了解Spring声明式事务的内部细节,仅需要懂得如何配置就可以了,但中高端开发者还需要了解其内部机制。
5 Spring AOP异步操作实现
5.1 异步场景分析
在开发系统的过程中,通常会考虑到系统的性能问题,提升系统性能的一个重要思想就是“串行”改“并行”。说起“并行”自然离不开“异步”,如何使用Spring的@Async的异步注解。
5.2 Spring业务的异步实现
5.2.1 启动异步配置
在基于注解方式的配置中,借助@EnableAsync注解进行异步启动声明,SpringBoot版的项目中,将@EnableAsync注解应用到启动类上,代码示例:
@EnableAsync //spring容器启动时会创建线程池
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
5.2.2 Spring中@Async注解应用
在需要异步执行的业务方法上,使用@Async方法进行异步声明。
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void saveObject(SysLog entity) {
System.out.println("SysLogServiceImpl.save:"+
Thread.currentThread().getName());
sysLogDao.insertObject(entity);
//try{Thread.sleep(5000);}catch(Exception e) {}
}
假如需要获取业务层异步方法的执行结果,可参考以下代码:
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Async
@Override
public Future saveObject(SysLog entity) {
System.out.println("SysLogServiceImpl.save:"+
Thread.currentThread().getName());
int rows=sysLogDao.insertObject(entity);
//try{Thread.sleep(5000);}catch(Exception e) {}
return new AsyncResult(rows);
}
注:AsyncResult对象可以对异步方法的执行结果进行封装,假如外界需要异步方法结果时,可以通过Future对象的get方法获取结果。
当需要对Spring框架提供的线程池进行一些建议配置,可以参考一下代码:
spring:
task:
execution:
pool:
queue-capacity: 128
core-size: 5
max-size: 128
keep-alive: 60000
thread-name-prefix: db-service-task-
对于spring框架中线程池配置参数的涵义,可以参考ThreadPoolExcutor对象中的解释。
注:对于@Async注解默认会基于TreadPoolTaskExecutor对象获取工作线程,然后调用由@Async描述的方法,让方法运行于一个工作线程,以实现异步操作。但是假如系统中的默认拒绝处理策略,任务执行过程的异常处理不能满足我们自身业务需求的话,可以对异步线程池进行自定义(SpringBoot中默认的异步配置可以参考自动配置对象TaskExecutionAutoConfiguration)。
6 AOP实现Redis缓存
6.1自定义缓存注解
问题:如何控制 哪些方法需要使用缓存?CacheFind()
解决方案:采用自定义注解的形式进行定义,如果方法执行需要使用缓存,则标识注解即可。
关于注解的说明:
- 注解名称:cacheFind
属性参数:
- key:应该有用户自己手动添加一般添加业务名称之后动态拼接形成唯一的key
- seconds:用户可以指定数据的超时的时间
@Target(ElementType.METHOD) //注解对方法有效
@Retention(RetentionPolicy.RUNTIME) //运行期有效
public @interface CacheFind {
public String preKey(); //用户标识key的前缀.
public int seconds() default 0; //如果用户不写表示不需要超时. 如果写了以用户为准.
}
6.2编辑CacheAOP
package com.jt.aop;
import com.jt.anno.CacheFind;
import com.jt.config.JedisConfig;
import com.jt.util.ObjectMapperUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import java.lang.reflect.Method;
import java.util.Arrays;
@Aspect //我是一个AOP切面类
@Component //将类交给spring容器管理
public class CacheAOP {
@Autowired
private Jedis jedis;
/**
* 切面 = 切入点 + 通知方法
* 注解相关 + 环绕通知 控制目标方法是否执行
*
* 难点:
* 1.如何获取注解对象
* 2.动态生成key prekey + 用户参数数组
* 3.如何获取方法的返回值类型
*/
@Around("@annotation(cacheFind)")
public Object around(ProceedingJoinPoint joinPoint,CacheFind cacheFind){
Object result = null;
try {
//1.拼接redis存储数据的key
Object[] args = joinPoint.getArgs();
String key = cacheFind.preKey() +"::" + Arrays.toString(args);
//2. 查询redis 之后判断是否有数据
if(jedis.exists(key)){
//redis中有记录,无需执行目标方法
String json = jedis.get(key);
//动态获取方法的返回值类型 向上造型 向下造型
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Class returnType = methodSignature.getReturnType();
result = ObjectMapperUtil.toObj(json,returnType);
System.out.println("AOP查询redis缓存");
}else{
//表示数据不存在,需要查询数据库
result = joinPoint.proceed(); //执行目标方法及通知
//将查询的结果保存到redis中去
String json = ObjectMapperUtil.toJSON(result);
//判断数据是否需要超时时间
if(cacheFind.seconds()>0){
jedis.setex(key,cacheFind.seconds(),json);
}else {
jedis.set(key, json);
}
System.out.println("aop执行目标方法查询数据库");
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}