本文包括以下几个部分:
1 什么是aop;
2 aop 的好处及使用场景;
3 aop 的实现;
3.1 基于aop 实现日志记录;
3.2 基于aop 配置数据库事务;
4 aop总结;
1 什么是sping - aop:
全称:Aspect Oriented Programming 面向切面编程,也叫做面向方法的编程;理解aop 先来理解切面:
切面指的是与业务无关,但是却被业务模块所共同调用的,我们将其进行一个封装,并将其命名为一个切面。
比如我们到商城去购物,商城有一个入口和一个出口,因为我们的目得是到商城中获取自己所需要的东西,我们本质上并不关心商城的入口和出口,因为我只要能进去并且我在买完东西可以离开就行了,但是每当我们进入商城的时候,保安就会要求我们将一些包包进行存放,出来在取,或者当我们感觉要 买的东西比较多可以拿个篮子或者手推车,以便我么购物,当我们出去商城的时候会被要求从一个有感应器的出口出来,以便检查本次所购买的物品是否都已结账;面向切面编程就相当于商城入口的一个保安,和出口的感应器,我们本质上根本不关心出口和入口,但是却与我们本次购物息息相关;
面向切面编程,程序会在进入一个/一些 类中的方法之前/之后,先进入/在进入另外一个类的一个/一些方法,以便对进入的方法进行增强;
2 aop 的好处使用场景/好处:
横向的将一些类中通用的业务处理 进行单独抽离,实现与主业务上逻辑的解耦,解决一些系统层面上的问题,如日志、事务、权限等,从而提高应用的可重用性和可维护性,和开发效率;
使用场景1:
客户每次访问程序都记录一个日志;客户在访问程序方法前 ,进行一些必要的参数及其它验证;
使用场景2:
配合数据库的事务配置我们程序的spring 事务,以方便程序在报错时对数据进行的一个回滚;
3 aop 的实现:
以下基于spring-boot 及使用maven 进行仓库管理,需要引入aop 的依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.1基于aop 实现日志记录:
(1) 要实现一个切面,首先我们需要声明一个切面类并将其交由spirng 进行管理:
@Aspect // 声明切面
@Component // spring扫描后交由spring 管理
public class DoHomeWorkAspect1 implements Ordered{
}
(2)接下来在切面类中规定,需要在进入程序中的哪些方法之前,进行一个拦截,这里的设置被称之为切点 Pointcut 如:
@Pointcut("execution(public * com.example.demo.collections.*.*(..))")
public void homeWorkPointCut() {
System.out.println("切面2 进入 homeWorkPointCut");
}
下图中为切点配置的参考:
对于我们通过设置的execution 路径下对应的一个个方法我们称之为一个个连接点 JoinPoint;
(3)然后我们需要设置在进入连接点的方法之前或者之后进行一些动作,称之为通知Advice:
这里通知有以下 几种:
进入方法前的通知:@Before
/**
* 进入方法前执行,有around 环绕 则先进入环绕
*/
@Before("homeWorkPointCut()")
public void beforeHomeWork(JoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
System.out.println("切面2进入before");
System.out.println("切面2"+request.getParameter("name")+"想先吃个苹果before");
}
方法完成后的通知,我们知道方法在执行过程中可能会报错,所以spring 定义的通知有两个:@AfterReturning 方法程序执行过程中不抛出异常
/**
* 方法执行完成后(方法程序执行过程中不抛出异常)调用 在around --> before --> 之后
*/
@AfterReturning(pointcut = "homeWorkPointCut()" , returning ="object")
public void afterRetyrnHomeWork(JoinPoint joinPoint,Object object) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
System.out.println("切面2进入@AfterReturning");
System.out.println("切面2"+request.getParameter("name")+"已做完功课afterRetyrnHomeWork");
}
和@AfterThrowing 方法程序执行过程中抛出异常
/**
* 方法执行完成后(方法程序执行过程中抛出异常)调用 在around --> before --> 之后
*/
@AfterThrowing ("homeWorkPointCut()")
public void afterThrowHomeWork() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
System.out.println("切面2进入@AfterThrowing");
System.out.println("切面2"+request.getParameter("name")+"已做完功课AfterThrowing");
}
然后还有一个方法是在执行完成不管抛不抛出(相当于程序里面的finally) 都进行的一个通知:@After
/**
* 方法执行完成后(方法程序执行过程中抛不抛出异常都执行)调用 在around --> before --> 之后
*/
@After( "homeWorkPointCut()")
public void afterHomeWork() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
System.out.println("切面2进入@after");
System.out.println("切面2"+request.getParameter("name")+"已做完功课@After");
//log.info("结束执行:{}",LocalDateTime.now());
}
还有一个通知比较特殊环绕通知:@Around 顾名思义该通知定义的方法会在方法执行前和执行后都会进入定义的方法
/**
* 方法执执行前调用 在around --> before -->方法执行--> around --> after -->AfterThrowing/AfterReturning
*/
@Around("homeWorkPointCut()")
public Object aroundHomeWork(ProceedingJoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
System.out.println("切面2进入@Around");
Object result = null;
// log.info("开始执行:{}",LocalDateTime.now());
OperationRecordEntity record = new OperationRecordEntity();
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String time1 = df.format(LocalDateTime.now());
record.setCzStartTime(time1);// 操作开始时间
try {
result = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
// log.info("method={}", request.getMethod());
// log.info("class={} and method name = {}",joinPoint.getSignature().getDeclaringTypeName(),joinPoint.getSignature().getName());
// 方法路径
record.setClassUrl(new StringBuilder(joinPoint.getSignature().getDeclaringTypeName()).append(joinPoint.getSignature().getName()).toString());
Object[] args = joinPoint.getArgs();
String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < args.length; i++) {
// log.info("参数:{},值:{}",paramNames[i],args[i]);
sb.append("参数:");sb.append(paramNames[i]);
sb.append("值:"); sb.append(args[i].toString()); sb.append(" ");
}
record.setParam(sb.toString());
// log.info("执行完毕:{}",LocalDateTime.now());
String time2 = df.format(LocalDateTime.now());
record.setCzEndTime(time2);// 操作结束时间
// 保存记录
record = null ;
df = null;
System.out.println("切面2"+request.getParameter("name")+"已做完功课@Around");
return result;
}
这里我们使用一个实体来记录本次请求方法的时间及参数等信息:
public class OperationRecordEntity {
private String czName;// 操作人
private String czStartTime;// 开始时间
private String classUrl;// 对应方法路径
private String param;// 对应参数
private String czEndTime;// 结束时间
public String getCzName() {
return czName;
}
public void setCzName(String czName) {
this.czName = czName;
}
public String getCzStartTime() {
return czStartTime;
}
public void setCzStartTime(String czStartTime) {
this.czStartTime = czStartTime;
}
public String getClassUrl() {
return classUrl;
}
public void setClassUrl(String classUrl) {
this.classUrl = classUrl;
}
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public String getCzEndTime() {
return czEndTime;
}
public void setCzEndTime(String czEndTime) {
this.czEndTime = czEndTime;
}
}
3.2 基于aop 配置数据库事务:
定义pointcut ,定义通知类型,定义事务的传播,回滚的类型,并交予jdbc 实现相应的事务
(1) 为哪些方法配置切点:
(2) 为切点方法配置具体通知:
(3) 为pointcut 类中以 delete,update,save,create,find,配置一条通知
(4)声明一个jdbc 的事务并配置transaction-manager 事务管理器
整体配置:
<!-- 使用JDBC事物 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- AOP配置事物 -->
<tx:advice id="transactionAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="delete*" rollback-for="java.lang.Exception" />
<tx:method name="update*" rollback-for="java.lang.Exception" />
<tx:method name="save*" rollback-for="java.lang.Exception" />
<tx:method name="create*" rollback-for="java.lang.Exception" />
<tx:method name="find*" read-only="true" />
<tx:method name="*" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 配置AOP切面 -->
<aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* com.dd2007..service.I*Service.*(..))" />
<aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
</aop:config>
这样配置后会在对 delete,update,save,create 开头的方法中添加事务,在这些方法进行对数据的插入,修改,删除 时,如果出现
java.lang.Exception 会对数据进行回滚,同时find 声明 read-only=“true”
后,其方法中只允许进行查询数据
扩展1:tx:method 标签:
<!-- tx:method的属性:
* name 是必须的,表示与事务属性关联的方法名(业务方法名),对切入点进行细化。通配符(*)可以用来指定一批关联到相同的事务属性的方法。
如:'get*'、'handle*'、'on*Event'等等.
* propagation 不是必须的 ,默认值是REQUIRED
表示事务传播行为, 包括REQUIRED,SUPPORTS,MANDATORY,REQUIRES_NEW,NOT_SUPPORTED,NEVER,NESTED
* isolation 不是必须的 默认值DEFAULT
表示事务隔离级别(数据库的隔离级别)
* timeout 不是必须的 默认值-1(永不超时)
表示事务超时的时间(以秒为单位)
* read-only 不是必须的 默认值false不是只读的
表示事务是否只读?
* rollback-for 不是必须的
表示将被触发进行回滚的 Exception(s);以逗号分开。
如:'com.foo.MyBusinessException,ServletException'
* no-rollback-for 不是必须的
表示不被触发进行回滚的 Exception(s);以逗号分开。
如:'com.foo.MyBusinessException,ServletException'
任何 RuntimeException 将触发事务回滚,但是任何 checked Exception 将不触发事务回滚
-->
如以下配置:
<tx:advice id="advice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"/>
<tx:method name="update*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"/>
<tx:method name="delete*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"/>
<!-- 其他的方法之只读的 -->
<tx:method name="*" read-only="true"/>
</tx:attributes>
<tx:advice id="advice" transaction-manager="txManager">
<tx:attributes>
图解:
扩展2: transaction-manager=“transactionManager”:
在进入tx method 标记的特殊方法时 ,会进入
DataSourceTransactionManager 类,将当前方法对事务的传播级别,隔离隔离级别,超时时间,回滚的一些参数进行传递。
扩展3 :对应一个方法定义多个通知,执行的顺序
这里进行了一个整理:即:around --> before -->方法执行–> around --> after -->AfterThrowing/AfterReturning
扩展4::对应一个方法定义多个独立的切面,每个切面有自己的通知,执行的顺序
这个实际上和我们项目中的类加载顺序有关系,先被加载的切面会先进入,后被加载的会后进入,在spring 里面我们可以使用注解@order(num) 来定义类加载的先后顺序。
或者 实现Ordered接口,重写getOrder方法
Aspect1 切面给出order:1
Aspect2 切面给出order:2
这里执行的顺序会是: 先进入切面1 的 @Around --> @Before --> 切面2的@Around --> @Before --> Method 切点 --> 切面2 @Around --> @After -->@AfterThrowing/ AfterReturning
–> 切面1@Around --> @After -->@AfterThrowing/ AfterReturning
下图为执行顺序同心圆参考
4 aop总结
Aop 是基于业务的横向切入,我们要实现一个切面,需要定义哪些类种的方法我们进行拦截,拦截的动作是什么,是进入前拦截还是方法执行完成后拦截,最后我们需要将这个切面类交由spring 容器进行管理:在运行期间通知对象通过在代理类中织入包裹切面,Spring 在运行期间将切面织入到 Spring 管理的 Bean 中。 代理类封装了目标类,并拦截被通知的方法调用,再将调用转发给真正的目标 Bean
当拦截到方法调用时,在调用目标 Bean 方法之前,代理会执行切面逻辑。
Spring 支持方法创建连接点 :
因为 Spring 基于动态代理,所以 Spring 只支持方法连接点。
Spring 缺失对字段连接点的支持,无法让我们更加细粒度的通知,例如拦截对象字段
的修改
Spring 缺失对构造器连接点支持,无法在 Bean 创建时候进行通知。