目录
前言
该笔记是我在b站看动力节点的视频时对老师所讲的知识点的一个归纳总结
一、AOP概念(Aspect Orient Programming)
AOP(Aspect Orient Programming)面向切面编程,切面指的是:给你的目标类增加的功能,是业务方法以外的功能,叫切面(如事务、日志、统计信息、参数检查、权限验证等等)
AOP需要掌握:
- 找出切面:需要在分析项目功能时,找出切面。
- 安排切面的执行时间:分析切面是在目标方法之前还是在目标方法之后。
- 安排切面执行的位置:在什么类、什么方法上要使用切面
如下概念需要掌握
- Aspect(切面)
- JoinPoint(连接点)
- Pointct(切入点)
- 目标对象:需要执行切面的某一个类的函数,该类就是目标对象。
- Advice:通知,通知表示切面功能执行的时间。
一个切面有三个关键的要素:
- 切面的功能代码,切面干什么
- 切面的执行位置,使用Pointcut表示切面执行的位置
- 切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后。
二、如何使用
我们首先需要知道在什么类上加切面功能,然后开始步骤:
使用流程
先创建切面的包(可以不要这一步,大项目需要)
首先在项目中要创建出放切面类的包(以便于管理)
然后创建切面类,在类上加@Aspect注解
在类中创建切面方法,在方法上加入注解(@Before,@AfterReturning这些),这里以@Before为例:
@Before 是 前置通知,在目标方法执行之前先执行。import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class MyAspect {
@Before(value = "execution(public void com.webApp.settings.service.Impl.ExampleServiceImpl.doSome(String, Integer))")
public void myBefore(){
System.out.println("Hello,I'm myBefore");
}
}
当然,该注解写法是最全的写法,它代表:
先找到com.webApp.settings.service.Impl包下的ExampleServiceImpl类中的doSome方法,在这个方法执行之前(Before)先执行切面方法,我们有很多方法简写它,之后再总结。
在spring的xml配置文件中做如下配置
- 声明bean标签,管理我们创建的切面类
- 创建自动代理生成器
具体如下:
<!--声明切面类对象-->
<bean id="myAspectj" class="com.webApp.aspect.MyAspect" />
<!--声明自动代理生成器-->
<aop:aspectj-autoproxy />
@Before & 切入点表达式使用
在@Before注解中,我们需要写切入点表达式来告诉spring我们需要在哪些方法上执行切面功能,如上示例子中
@Before(value = “execution(public void com.webApp.settings.service.Impl.ExampleServiceImpl.doSome(String, Integer))”)
的value值就告诉了我们需要执行切面的具体方法在什么位置(可以是多个)
其中execution中写的就是切入点表达式,其结构如下:
execution(方法访问限 方法返回类型 方法声明(参数…) 异常类型)
- 方法访问限:如 public 、 private 这些 (注:可以省略不写)
- 方法返回类型:如int类型,void类型之类
- 方法声明:完整的写法是写出在项目中的完整结构(带包名、类名、方法名)
- 异常类型:该方法会抛出什么异常 (注:可以省略不写)
注意:以上的语法都可以用通配符来进行大规模的匹配,给很多个方法匹配切面功能。
如上述例子就还可以有如下写法:
例1:
@Aspect
public class MyAspect {
@Before(value = "execution(public void *..ExampleServiceImpl.doSome(String, Integer))")
public void myBefore(){
System.out.println("Hello,I'm myBefore");
}
}
如上写法带有 “ *.. ” ,表示任意包下的 ExampleServiceImpl 类的带参数String、Integer的、返回值为空(void)的dosome方法
例2:
@Aspect
public class MyAspect {
@Before(value = "execution(public * *..ExampleServiceImpl.doSome(String, Integer))")
public void myBefore(){
System.out.println("Hello,I'm myBefore");
}
}
这个写法表示除了是任意包下的 ExampleServiceImpl 类的带参数String、Integer的dosome方法,还表示返回类型可以是任意的
例3:
@Aspect
public class MyAspect {
@Before(value = "execution(public void *..ExampleServiceImpl.doSome(..))")
public void myBefore(){
System.out.println("Hello,I'm myBefore");
}
}
如上写法在方法参数(括号)中带有 “ .. ” ,表示可以匹配任意个参数。
符号 | 意义 |
---|---|
* | 0至任意多个字符 |
.. | 用在方法参数中,表示任意多个参数,用在包名后,表示当前包以及子包路径 |
+ | 用在类名后,表示当前类以及子类,用在接口后,表示当前接口以及实现类 |
再来几个例子:
表达式 | 意义 |
---|---|
execution(public * *(..)) | 任意公共方法 |
execution(* set*(..)) | 任何以set开始的方法 |
… | … |
这样我们的功能就加上去了,之后我们再来看更多的功能实现。
JoinPoint 类型(传递目标方法的参数)
经过前面的例子,我们发现,@Before是在该方法之前执行的,我们如果想在切面方法中获取到目标方法的参数,我们该怎么办呢?
答案是使用JoinPoint。
@Aspect
public class MyAspect {
@Before(value = "execution(public void com.webApp.vo.Test001.doSome())")
public void myBefore(JoinPoint joinPoint){
System.out.println("Hello,I'm myBefore");
}
}
JoinPoint是一个类,放在切面方法的参数的第一个位置(必须放在第一个位置,要么不放)
它可以获取到目标方法的一系列信息,使用方法如下:
@Aspect
public class MyAspect {
@Before(value = "execution(public void com.webApp.vo.Test001.doSome(..))")
public void myBefore(JoinPoint joinPoint){
System.out.println("Hello,I'm myBefore");
Object[] args = joinPoint.getArgs();//获取目标方法的参数列表
Signature signature = joinPoint.getSignature();//获取参数的完整定义
for (Object i : args)
System.out.println(i);
}
}
目标方法如下:
public void doSome(String name,int age) {
System.out.println("Hello ExampleServiceImpl.doSome!");
}
执行测试方法:
public class test {
@Test
public void test001(){
String config = "conf/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
Test001 test001 = (Test001) applicationContext.getBean("test");
test001.doSome("小猪猪", 19);
}
}
得到的结果是:
@AfterReturning(后置通知)
例子:
现在目标方法为:
public String doSome01() {
String a = "hello world";
System.out.println("========执行了doSome01=========\n原本的返回值为:" + a);
return a;
}
切面方法为:
@AfterReturning(value = "execution(public void com.webApp.vo.Test001.doSome01(..))", returning = "o")
public void myAfterReturning(Object o) {
}
注:1、使用位置也是在方法定义之上 。2、使用JoinPoint要放在参数的首位置
属性:
- value :和前置通知@Before一样,用于写切入点表达式的,表明该切面功能执行的位置
- returning:自定义的变量,表示目标方法的返回值的,不过,自定义变量名必须和通知方法的形参名一样(如下图,名字都必须是“o”)
特点:
- 在目标方法之后执行
- 能够获取目标方法的返回值,可以根据这个返回值做不同的处理功能
- 可以修改这个返回值
注意!!!如果值是简单类型,在此方法中直接修改res的值的话,原本的res值是不会发生改变的;但是如果传的是一个对象之类的引用类型,那么res的值就会发生改变!
如下是两个后置通知,第一个res是简单类型(String)第二个是引用类型(Student):
@AfterReturning(value = "execution(public * com.webApp.vo.Test001.doSome01(..))", returning = "res")
public void myAfterReturning(Object res) {
//Object res是目标方法的返回值,我们可以根据这个返回值做一些操作
System.out.println("后置通知方法执行,获取到的目标方法返回值对象是:" + res);
res = "I love U";
System.out.println("现在将后置通知里的res值改成:" + res);
//注意,如果值是简单类型,在此方法中直接修改res的值的话,原本的res值是不会发生改变的。
//但是如果传的是一个对象之类的引用类型,那么res的值就会发生改变!
}
@AfterReturning(value = "execution(public * com.webApp.vo.Test001.doSome02(..))", returning = "res")
public void myAfterReturning02(Object res) {
//Object res是目标方法的返回值,我们可以根据这个返回值做一些操作
System.out.println("后置通知方法执行,获取到的目标方法返回值对象是:" + res);
//注意,如果值是简单类型,在此方法中直接修改res的值的话,原本的res值是不会发生改变的。
//但是如果传的是一个对象之类的引用类型,那么res的值就会发生改变!
Student student = (Student) res;
student.setName("littlePig");
System.out.println("现在将后置通知里的res值改成:" + student);
}
如下分别是上两个后置通知的目标方法:
public String doSome01() {
String a = "hello world";
System.out.println("========执行了doSome01=========\n原本的返回值为:" + a);
return a;
}
public Student doSome02() {
Student student1 = new Student(19, "hxy");
System.out.println("========执行了doSome02=========\n原本的返回值为:" + student1);
return student1;
}
执行测试方法:
@Test
public void test003() {
String config = "conf/applicationContext.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(config);
Test001 test001 = (Test001) applicationContext.getBean("test");
System.out.println("执行方法:"+test001.doSome01());
System.out.println("执行方法:"+test001.doSome02());
}
得到的结果为:
========执行了doSome01=========
原本的返回值为:hello world
后置通知方法执行,获取到的目标方法返回值对象是:hello world
现在将后置通知里的res值改成:I love U
执行方法:hello world
========执行了doSome02=========
原本的返回值为:Student{age=19, name='hxy'}
后置通知方法执行,获取到的目标方法返回值对象是:Student{age=19, name='hxy'}
现在将后置通知里的res值改成:Student{age=19, name='littlePig'}
执行方法:Student{age=19, name='littlePig'}
由执行结果可知,在后置方法中改简单类型是不会对结果造成影响,而对引用类型会造成影响。
@Around (环绕通知)
环绕通知是功能最强大的,它既有前置通知部分也有后置通知部分,还可以改变目标方法的返回结果(无论简单类型还是引用类型),示范代码如下:
@Around(value = "execution(* *..Test001.doSome03(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
Object result;
System.out.println("在目标执行方法之前有前置的切面方法");
result = pjp.proceed();
System.out.println("目标方法执行完毕,现在得到目标方法的结果:result:" + result);
result = "i am result!";
System.out.println("现在改变目标方法的返回值,result:" + result);
return result;
}
有几点注意:
- 该注解的属性:只有value一个属性:@Around(value = “…”)
- 方法要声明为Object,返回的是改变后的目标方法的返回值。
- 该方法必须要有一个ProceedingJoinPoint类型的参数(ProceedingJoinPoint pjp)用于控制目标方法什么时候执行,和获取目标方法的返回值
- 该切面方法需要抛出 Throwable 异常
其他通知(异常通知 & 最终通知)
其他通知注解还有:异常通知, 最终通知
异常通知(@AfterThrowing)
发生异常的时候会执行该切面方法的代码,相当于try catch结构的catch部分代码
属性:
- value :和上述其他通知注解的value用法一样。
- throwing为自定义变量,表示目标方法抛出的异常对象,变量名必须和方法名的参数名称一样
特点:
- 在目标方法抛出异常的时候才会执行。
- 可以做异常的监控程序,监控目标方法是不是有异常发生(发生的话可以写发送邮件之类的代码通知开发人员)
- 要么有一个参数(异常参数),要么是第一个为JoinPoint参数而第二个是异常的参数
最终通知(@After)
一定会被执行,相当于try catch finally 中的finally部分,一般用于最后的资源清楚工作的(清除缓存之类的)
用法与其他通知注解大同小异,在此不作过多赘述
@Pointcut(辅助注解,复用value值)
这是一个辅助功能的注解,它有什么用呢?
如果我们写了很多的切面方法,但是他们的value值都是相同的,那么这个value就冗余了且不好大规模改动,因此出现了@Pointcut注解单独定义value值,定义方法如下:
@Pointcut(value = "execution(* *..Test001.doSome03(..))")
public void myPointCut(){
//此处为空就行
}
然后我们在之前的通知注解中这样使用:
@AfterReturning(value = "myPointCut()", returning = "res")
private void myAfterReturning02(Object res) {
...
}
注意:value的引号不能丢,而且一般情况都将其定义为私有的(private)
动态代理的方式(jdk & cglib)
我们知道,aop是通过动态代理来实现的,那么我们在使用spring框架的时候用的是什么动态代理方式呢?
我们需要知道,默认情况下,如果我们的目标方法是有接口的话,那么代理方式则是jdk的动态代理方式(通过实现接口来实现aop功能),如果我们的目标方法没有接口,就是一个普通类,则使用的是Spring框架提供的cglib代理方式(通过继承父类来实现aop功能)
有无接口 | 代理方式 | 采用方式 |
---|---|---|
有接口 | jdk动态代理 | 实现接口 |
无接口 | cglib动态代理 | 继承父类 |
如果想让项目强制都使用cglib动态代理的话,只需要在spring配置文件中将自动代理生成器添加一个属性proxy-target-class 设置其值为真(proxy-target-class=“true”),这种方式在执行的时候效率会更高一些
<aop:aspectj-autoproxy proxy-target-class="true" />
事务
事务,就是让某一个方法中执行的 很多个 sql语句要么一起执行,要么一起不执行,是绑定在一起的
我们有两种办法来实现事务功能,一是通过spring自带的注解方案添加事务功能( 机制是环绕通知(@Around)),还有一种是通过aspectj框架来配置事务,通过aspectj框架配置事务功能的方法更加适合于大项目的开发,因为方便管理。我们先来说第一种方案:通过注解配置事务功能
注解配置方式(小项目适用)
第一步:在配置文件中声明事务管理器
配置事务一定要先告诉spring我们用的数据库是什么,如何告诉呢?是通过告诉spring容器中管理mybatis容器的方法让其知道的,首先我们要连接数据库:
<!--声明数据源,连接数据库-->
<bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close" >
<property name="maxActive" value="${jdbc.maxActive}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
得知数据源的id值(为myDataSource)之后我们就可以来配置事务了:
<!--声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
<property name="dataSource" ref="myDataSource" />
</bean>
<!--开启事务注解驱动,告诉spring使用注解管理事务,创建代理对象-->
<tx:annotation-driven transaction-manager="transactionManager" />
然后在我们想要用事务的方法上加上注解就可以了:
@Transactional
public String doSome03() {
System.out.println("========执行了doSome03=========");
return "doSome03";
}
注意:最简单的添加方法就是加上**@Transactional**就可以了,但是我们也需要学习他的更详细的用法:
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
readOnly = false,
rollbackFor = {
NullPointerException.class,
LoginException.class
}
)
public String doSome03() {
System.out.println("========执行了doSome03=========");
return "doSome03";
}
- propagation:传播行为
- isolation:隔离级别
- readOnly:是否只读
- rollbackFor :碰到什么异常时回滚(除了这些还包括运行时异常:RunTimeException)
Aspectj框架配置方式(大项目适用)
这种方式区别于注解配置的一个特点就是:业务方法和事务配置完全分离
实现全部都是在配置文件中完成的,假设现在基本的ssm配置已经完成,现在来说配置事务的方式:
<!--声明事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
<property name="dataSource" ref="myDataSource" />
</bean>
<!--aspectj的事务配置方案-->
<tx:advice id="myAdvice" transaction-manager="transactionManager" >
<tx:attributes>
<tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT"
rollback-for="java.lang.NullPointerException" />
<tx:method name="add*" propagation="REQUIRED" isolation="DEFAULT"
rollback-for="java.lang.NullPointerException, com.webApp.excep.LoginException" />
<tx:method name="delete*" propagation="REQUIRED" isolation="DEFAULT"
rollback-for="java.lang.NullPointerException" />
<!--指定以上方法以外的则直接用*通配其他所有方法-->
<tx:method name="*" propagation="SUPPORTS" isolation="DEFAULT" read-only="true" />
</tx:attributes>
</tx:advice>
<aop:config>
<!-- 配置切入点,用于下面的代码和advisor关联 -->
<aop:pointcut id="servicePt" expression="execution(* com.webApp.*..*(..))"/>
<!--
配置增强器:关联advice和pointcut,
advice-ref:通知,上面tx:advice处的配置
pointcut-ref:切入点表达式的id
-->
<aop:advisor advice-ref="myAdvice" pointcut-ref="servicePt" />
</aop:config>
两种事务配置方法,各有各的优点,如果是小项目,一个注解就可以完成事务的配置,不用写一大堆配置文件,如果是大项目,大项目的类特别多,一个一个加注解特别麻烦,所以采用配置文件来配置的这种分离的方式,可以很快速且方便就能完成事务的配置
总结
本文我们总结了spring框架中aop的最粗略的用法,包括:
1、流程解析:从没有aop功能到添加aop功能的流程解析(前提是基本的ssm配置已经完成)
2、各种切面的介绍:介绍了各种切面注解(前置通知、后置通知、环绕通知、最终通知、异常通知)
3、事务的两种配置方法①小项目适用的通过注解配置。②大项目使用的通过配置文件配置。
之后我会总结SpringMVC的最基本的知识点,目的用于复习知识点,所以会挑重点说,喜欢的看客们留下你的赞吧~