目录
一.概念
二.手写AOP框架
三.Spring支持的AOP的实现
1.Spring的AOP的类型?
2.AOP的常用术语
3.AspectJ框架
(1)?Aspect的通知类型
(2)AspectJ的切入点表达式
(3)使用AspectJ的环境
4.前置通知
(1)前置通知流程分析
(2)前置通知切面方法开发
5.后置通知
(1)后置通知流程分析
(2)后置通知切面方法开发
6.环绕通知
(1)环绕通知执行流程分析
(2)环绕通知切面方法开发
7.最终通知
8.为一个方法添加多种通知
一.概念
AOP(Aspect Orient Programming)。
切面:公共的,通用的,重复的功能称为切面,面向切面编程就是将切面提取出来,单独开发,在需要调用的方法中通过动态代理方式进行织入。
二.手写AOP框架
版本1:
//图书购买业务和事务切面耦合在一起
public class BookServiceImpl {
public void buy(){
try{
System.out.println("事务开启");
System.out.println("图书购买业务功能实现");
System.out.println("事务提交");
}catch(Exception e){
System.out.println("事务回滚");
}
}
}
?版本2:
public class BookServiceImpl {
//todo 在父类中只有业务
public void buy(){
System.out.println("图书购买业务功能实现");
}
}
//todo 子类是代理类,将父类的图书购买功能添加事务的切面
public class SubBookServiceImpl extends BookServiceImpl{
@Override
public void buy() {
try{
//todo 事务的切面
System.out.println("事务开启");
//todo 主业务实现
super.buy();
//todo 事务切面
System.out.println("事务提交");
}catch(Exception e){
System.out.println("事务回滚");
}
}
}
将业务和切面分开,实现了相同的功能
版本3:
静态代理,实现业务灵活切换,切面的功能在代理中体现。
public interface Service {
//todo 业务功能
void buy();
}
//todo 业务功能的具体实现
public class BookServiceImpl implements Service{
@Override
public void buy() {
System.out.println("图书购买业务实现");
}
}
public class FoodServiceImpl implements Service{
@Override
public void buy() {
System.out.println("购买食品业务");
}
/*todo 静态代理已经实现了目标对象的灵活切换,如图书购买业务,食品购买业务*/
public class Agent implements Service{
//todo 设计成员变量为接口,为了灵活切换目标对象
public Service target;
//todo 使用构造方法传入目标对象
public Agent(Service target){
this.target = target;
}
@Override
public void buy() {
try{
//切面功能
System.out.println("事务开启");
//业务功能
target.buy();
//切面功能
System.out.println("事务开启");
}catch(Exception e){
System.out.println("事务回滚");
}
}
}
public class Test {
@org.junit.Test
public void test(){
Agent bookAgent = new Agent(new BookServiceImpl());
bookAgent.buy();
Agent foodAgent = new Agent(new FoodServiceImpl());
foodAgent.buy();
}
}
版本4:
在静态代理中,会发现代理实现的切面功能是死的只能实现事务功能,不能灵活调用其他的切面功能比如日志权限验证功能。
有了接口使业务的功能实现更加灵活。
?代理要实现切面接口而不是将切面接口的对象传进来。
要是传对象的话,业务接口和切面接口耦合在代理对象中,业务有变化或切面有变化,代理都得改变。
public interface AOP {
//default 实现类没必要实现该接口中所有的方法
default void before(){};
default void after(){};
default void exception(){};
}
public class LogAOP implements AOP{
@Override
public void before() {
System.out.println("日志输出");
}
}
public class TransactionAOP implements AOP{
@Override
public void before() {
System.out.println("事务开启");
}
@Override
public void after() {
System.out.println("事务提交");
}
@Override
public void exception() {
System.out.println("事务回滚");
}
}
/*todo 静态代理已经实现了目标对象的灵活切换,如图书购买业务,食品购买业务*/
public class Agent implements Service,AOP{
//todo 设计成员变量为接口,为了灵活切换目标对象
public Service target;
public AOP aop;
//todo 使用构造方法传入目标对象
public Agent(Service target,AOP aop){
this.target = target;
this.aop = aop;
}
@Override
public void buy() {
try{
//切面功能
aop.before();
//业务功能
target.buy();
//切面功能
aop.after();
}catch(Exception e){
aop.exception();
}
}
}
public class Test {
@org.junit.Test
public void test(){
pro4.Agent bookAgent = new pro4.Agent(new BookServiceImpl(),new TransactionAOP());
bookAgent.buy();
Agent foodAgent = new pro4.Agent(new FoodServiceImpl(),new LogAOP());
foodAgent.buy();
}
}
?一个业务增加多个切面功能
public class Test {
@org.junit.Test
public void test(){
pro4.Agent bookAgent = new pro4.Agent(new BookServiceImpl(),new TransactionAOP());
//代理也是实现业务的一部分
Agent bookAgent1 = new Agent(bookAgent,new LogAOP());
bookAgent1.buy();
}
}
版本5:
动态代理
拆掉代理对象和业务的耦合即不再让代理对象实现业务的接口。
动态代理实现了业务功能的灵活改变,在静态代理时,要完成buy业务代理也需要实现buy方法,使用了动态代理后,代理不需要实现业务的任何方法。
public class ProxyFactory {
public static Object getAgent(Service target,AOP aop){
//返回生成的动态代理对象
return Proxy.newProxyInstance(
//类加载器
target.getClass().getClassLoader(),
//目标对象实现的所有的接口
target.getClass().getInterfaces(),
//代理功能的实现
new InvocationHandler() {
@Override
public Object invoke(
//生成的代理对象
Object proxy,
//正在被调用的目标方法如buy()
Method method,
//目标方法的参数
Object[] args
) throws Throwable {
Object obj = null;
try{
//切面
aop.before();
//业务
obj = method.invoke(target,args);
//切面
aop.after();
}catch(Exception e){
aop.exception();
}
return obj;
}
});
}
}
业务随意添加,都不会影响代理的代码
public interface Service {
//todo 业务功能
void buy();
default String show(int num){return null;}
}
public class Test {
@org.junit.Test
public void test(){
Service agent = (Service) ProxyFactory.getAgent(new BookServiceImpl(),new TransactionAOP());
//agent.buy();
String show = agent.show(5);
System.out.println(show);
}
}
三.Spring支持的AOP的实现
1.Spring的AOP的类型?
Advice:通知
Interceptor:拦截
2.AOP的常用术语
?
3.AspectJ框架
AspectJ是一个优秀的切面框架,它扩展了Java语言,提供了强大的切面实现。它基于Java语言开发的,可以无缝扩展原有的功能。
对于AOP编程思想,很多框架都进行了实现,Spring就是其中之一,可以完成面向切面编程,然而AspectJ也实现了AOP的功能,且实现方式更为简捷,使用更为方便,而且还支持注解开发,所以Spring将AspectJ的对于AOP的实现引入到了自己的框架中,在Spring使用AOP开发时,一般使用AspectJ的实现方式。
(1)?Aspect的通知类型
1.前置通知@Before
2.后置通知@AfterReturning
3.环绕通知@Around
事务就是环绕通知
4.最终通知@After
5.定义切入点@Pointcut(了解)
(2)AspectJ的切入点表达式
?AspectJ定义了专门的表达式用于指定切入点。
表达式原型
?
规范的公式
execution(访问权限? 方法返回值 方法声明(参数) 异常类型)
表达式简化
execution (方法返回值 方法声明(参数))
例子
1.execution(public * *(..))
访问权限类型:public
返回值类型:*
方法声明:*
参数:..
切入点:任意公共方法
2.execution(* set*(..))
返回值类型:*
方法声明:set*
参数:..
切入点:以set开头的任意方法
3.execution(* com.xyz.service.impl.*.*(..))
返回值类型:*
包名类名: com.xyz.service.impl.*? ? ? ? ?这里的*是类
方法名:*
参数:..
切入点:service包下的impl包下的任意类的任意方法
4.execution(* com.xyz.service..*.*(..))
返回值类型:*
包名类名:com.xyz.service..*
方法名:*
参数:..
service包下的任意路径(包括本路径和子路径)的任意方法
5.execution(* com.xyz.service.*(..))
这个没有类名,本意是想找service包下的所有类的所有方法,但会报错
6.execution(* com.xyz.service..*(..))
service路径包括子路径的所有类的所有方法,不会报错。注意本例中是..? 而上例是.
切入点表达式的用法
(3)使用AspectJ的环境
依赖
//spring依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.19</version>
</dependency>
//aspectJ依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.19</version>
</dependency>
将配置文件编译时加载到target文件中,不然会显示找不到配置文件
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>
Maven当中resources标签的用法_怪咖软妹@的博客-CSDN博客_maven resources标签 ?
4.前置通知
(1)前置通知流程分析
前置通知在目标方法前执行,所以在前置方法中无法获取目标方法执行后的结果,但能获取目标方法的签名,也就是public后面的一堆,因为只有知道目标方法的签名,才能作为他的前置通知。
(2)前置通知切面方法开发
?前置通知切面方法开发
Step1:创建业务接口
public interface SomeService {
String doSome(String name,int age);
}
Step2:创建业务实现
@Component
public class SomeServiceImpl implements SomeService{
@Override
public String doSome(String name, int age) {
System.out.println("doSome功能实现");
return "doSome";
}
}
Step3:创建切面类,实现切面方法
//切面类
@Component
@Aspect //交给AspectJ框架去识别的切面类
public class MyAspect {
/*
前置通知的规范
1.访问权限是public
2.方法返回值是void
3.方法名称自定义
4.方法没有参数,如果有也只能是JoinPoint类型
5.必须使用@Before注解来声明切入的时机是前切功能和切入点
参数:value 指定切入点表达式
public String doSome(String name, int age)
*/
@Before(value = "execution(public String Before.SomeServiceImpl.doSome(String,int))")
public void myBefore(){
System.out.println("前置通知功能实现");
}
}
Step4:在applicationContext.xml文件中进行切面绑定
<!--创建业务对象-->
<bean id="someService" class="Before.SomeServiceImpl"></bean>
<!--创建切面对象-->
<bean id="myAspect" class="Before.MyAspect"></bean>
<!--绑定,之前是通过代理来绑定的,等同于ProxyFactory中的代码-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
或用包来扫描类
<context:component-scan base-package="Before"></context:component-scan>
<!--绑定,之前是通过代理来绑定的,等同于ProxyFactory中的代码-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
Step:测试
public class Test {
@org.junit.Test
public void test(){
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
//取出代理对象
SomeService someService = (SomeService) ac.getBean("someServiceImpl");
System.out.println(someService.getClass());//class jdk.proxy2.$Proxy10
//表示这个someService是加入了切面功能的对象,而不是普通的someService对象
someService.doSome("1",1);
}
}
JDK动态代理和CGLib动态代理
JDK动态代理只能用接口接收代理对象,而CGLib可以用接口实现类接收代理对象也就是用子类来接收代理对象,可以使用标签来灵活改变代理方式
CGLib代理
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
SomeServiceImpl someService = (SomeServiceImpl) ac.getBean("someService");
JDK代理
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
SomeService someService = (SomeService) ac.getBean("someService");
方法参数JoinPoint解析
JoinPoint来获取目标参数的信息
@Component
@Aspect //交给AspectJ框架去识别的切面类
public class MyAspect {
@Before(value = "execution(public String Before.SomeServiceImpl.doSome(String,int))")
public void myBefore(JoinPoint joinPoint){
System.out.println("前置通知功能实现");
System.out.println("目标方法的签名"+joinPoint.getSignature());
System.out.println("目标方法的参数"+ Arrays.toString(joinPoint.getArgs()));
}
}
5.后置通知
(1)后置通知流程分析
?后置通知可以修改目标方法的返回值,但也是要分两种情况。
如果目标方法的返回值类型是8种基本类型或String类型,则不可改变,如果目标方法的返回值是引用类型则可以改变。可变与不可变是在测试代码中体现的。
(2)后置通知切面方法开发
/*
后置通知的规范
1.访问权限是public
2.方法返回值是void
3.方法名称自定义
4.如果目标方法有返回值,需要写参数,没有返回值则不需要写参数。写参数也可以处理没有返回值的情况,所以一般要写参数
5.必须使用@AfterReturning注解来声明切入的时机是后切功能和切入点
参数:value 指定切入点表达式
returning:指定目标方法的返回值的名称,此名称必须与切面方法的名称一致。
public String doSome(String name, int age)
*/
@AfterReturning(value = "execution(* After.*.*(..))",
returning = "obj")
public void myAfterReturning(Object obj){
System.out.println("后置通知功能实现");
if(obj != null){
if(obj instanceof String){//判断是否是String类型
obj = obj.toString().toUpperCase();
}
}
System.out.println(obj);
}
判断目标方法的返回值是否是String类型,如果是则转为大写。在后置切面方法中输出的obj为大写,但是在测试类中,测试目标方法的返回值还是小写。
6.环绕通知
拦截目标方法,在目标方法前后增强功能的通知。是功能最强大的通知,一般事务使用此通知。
(1)环绕通知执行流程分析
(2)环绕通知切面方法开发
/*
环绕通知的规范
1.访问权限是public
2.方法返回值是是目标方法的返回值
3.方法名称自定义
4.方法有参数,参数就是目标方法
5.回避异常
6.使用@Around注解声明是环绕通知
参数:value:指定切入点表达式
public String doSome(String name, int age)
*/
@Around(value = "execution(* Around.*.*(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
//前切功能实现
System.out.println("环绕通知前切功能实现");
//目标方法调用
Object obj = pjp.proceed(pjp.getArgs());
//后切功能实现
System.out.println("环绕通知后切功能实现");
return obj.toString().toUpperCase();
}
环绕通知可以随意修改目标方法的返回值。
7.最终通知
无论目标方法是否正常执行,最终通知的代码都会被执行。相当于try-catch-finally中的finally。
/*
最终通知的规范
1.访问权限是public
2.方法没有返回值
3.方法名称自定义
4.方法不需要参数,若需要写则只能写JoinPoint
5.使用@After注解声明是最终通知
参数:value:指定切入点表达式
public String doSome(String name, int age)
*/
@After(value = "execution(* fin.*.*(..))")
public void myAfter(){
System.out.println("最终通知功能实现");
}
8.为一个方法添加多种通知
可以为一个方法绑定若干通知,而已统一通知类型也可以绑定多个
//切面类
@Component
@Aspect //交给AspectJ框架去识别的切面类
public class MyAspect {
@Before(value = "execution(* *(..))")
public void myBefore(JoinPoint joinPoint){
System.out.println("前置通知功能实现");
System.out.println("目标方法的签名"+joinPoint.getSignature());
System.out.println("目标方法的参数"+ Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(value = "execution(* fin.*.*(..))",
returning = "obj")
public void myAfterReturning(Object obj){
System.out.println("后置通知功能实现");
if(obj != null){
if(obj instanceof String){//判断是否是String类型
obj = obj.toString().toUpperCase();
}
}
}
@Around(value = "execution(* fin.*.*(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable{
//前切功能实现
System.out.println("环绕通知前切功能实现");
//目标方法调用
Object obj = pjp.proceed(pjp.getArgs());
//后切功能实现
System.out.println("环绕通知后切功能实现");
return obj.toString().toUpperCase();
}
@After(value = "execution(* fin.*.*(..))")
public void myAfter(){
System.out.println("最终通知功能实现");
}
}
?可以参见出各个通知的执行顺序
环绕通知前切
前置通知
后置通知
最终通知
环绕通知后切
如果多个切面切入同一个切入点,可给切入点起别名简化开发
使用@Pointcut注解,创建空方法,此方法的名称就是别名。
@After(value = "myCut()")
public void myAfter(){
System.out.println("最终通知功能实现");
}
@Pointcut(value = "execution(* fin.*.*(..))")
public void myCut(){
}
?
|