全网疯传的Spring学习笔记【AOP】,看完我也想写一个了!
八、AOP编程
8.1、静态代理
8.1.1、问题引入
为什么需要代理设计模式
在JavaEE分层开发中,Service层(业务层)对我们来说是最重要的。
Service层中包含哪些代码
在Service中会出现两种类型的代码:
- 核心功能:业务运算、Dao操作。
- 附加功能(代码量少且不属于核心功能,可有可无):事务、日志、性能监控。
8.1.2、代理设计模式概述
目标类(原始类):类似于现实生活中的房东,指的是包含核心功能的业务类。
目标方法(原始方法):目标类(原始类)中的方法。
通过代理类,为原始类(目标类)增加额外的功能,好处是有利于原始类(目标类)的维护。
8.1.3、静态代理的实现
静态代理有一个原始类就必须有一个手工编写的代理类(源代码),每一个类都是程序员手动写的。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String username;
private String password;
}
public interface UserService {
void register(User user);
void login(String username,String password);
}
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("注册了");
}
@Override
public void login(String username, String password) {
System.out.println("登录了");
}
}
public class UserServiceProxy implements UserService {
// 原始类对象
UserServiceImpl userService = new UserServiceImpl();
@Override
public void register(User user) {
System.out.println("增加了日志的额外功能");
userService.register(user);
}
@Override
public void login(String username, String password) {
System.out.println("增加了登录日志的额外功能");
userService.login(username,password);
}
}
8.1.4、静态代理存在的问题
- 一个原始类就有一个手动书写的代理类,会导致静态类的文件过多,不利于项目管理。
- 额外功能的维护性差,修改复杂。
8.2、Spring动态代理
既然静态代理如此的复杂和麻烦,那么Spring就帮我们搞了一个动态代理,简化了我们的开发。
8.2.1、动态代理的实现
- 创建原始对象(目标对象)
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("注册了");
}
@Override
public void login(String username, String password) {
System.out.println("登录了");
}
}
<bean id="UserServiceImpl" class="com.proxy.dynamicProxy.UserServiceImpl"/>
- 书写额外功能
既然我们不需要手动书写代理类了,那么如何告诉Spring我们需要增加的额外方法呢?
我们需要写一个额外方法类,并且实现MethodBeforeAdvice
接口,在他规定的方法中书写额外功能
public class Before implements MethodBeforeAdvice {
// 额外功能书写在接口的实现中,运行在原始方法执行之前运行额外功能
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("---method before advice log---");
}
}
- 去配置文件中配置
<bean id="before" class="com.proxy.dynamicProxy.Before"/>
- 定义切入点
切入点:额外功能加入的位置,由程序员根据自己的需求,决定额外功能加入给哪个原始方法。
在测试阶段,所有方法都作为切入点,都加入额外功能。
<aop:config>
<!-- 切入点,id属性是唯一标识符,expression属性是切入点表达式,这个切入点表达式的意思是所有方法都作为切入点都加入额外功能-->
<aop:pointcut id="testaop" expression="execution(* *(..))"/>
</aop:config>
- 组装
组装的目的是将切入点和额外功能进行整合。
<!-- 组装:将切入点和额外功能进行整合-->
<aop:advisor advice-ref="before" pointcut-ref="testaop"/>
- 测试
利用原始对象的id
值,可以获取由Spring工厂创建的代理对象。获得代理对象后,可以声明接口类型进行对象的存储。
8.2.2、MethodBeforeAdvice
如果我们想实现动态代理,额外功能必须实现MethodBeforeAdvice
接口的before
方法,before
方法的参数解释
参数名 | 含义 |
---|---|
Method method | 额外功能所增加给的那个原始方法,给谁增加额外功能就是谁,这个参数是变化的,取决于给谁增加额外方法 |
Object[] objects | 额外功能所增加给的那个原始方法的参数。给login(String uername,Stirng password)方法增加额外功能,那么这个Object数组就是对于login方法的参数列表,和上一个参数息息相关。 |
Object o | 额外功能所增加给的那个原始对象。 |
8.2.3、注意事项
-
Spring创建的动态代理类在哪里?
Spring框架在运行时,通过动态字节码技术,在JVM里创建,等待程序结束后,会和JVM一起消失。
-
什么是动态字节码技术?
通过第三方动态字节码框架,直接生成JVM生成字节码,进而创建对象,当JVM结束时,动态字节码跟着消失。
-
动态代理不需要定义类文件,都是JVM运行过程中动态创建的,所以不会造成静态代理类文件数量过多、影响项目管理的问题。
-
在不改变功能的前提下,创建其他目标类(原始类)的代理对象时,只需要指定原始(目标)对象即可。
8.2.4、MethodInterceptor接口
MethodBeforeAdvice
接口的方法作用比较单一,仅仅只是可以在原始方法执行之前进行增加额外功能,Spring还提供了另一接口——MethodInterceptor
接口,他不仅仅可以在原始方法执行之前增加额外功能,还可以在原始方法执行之后增加额外功能,甚至执行前后都可以增加。
public class Arround implements MethodInterceptor {
/*
书写额外功能的方法
参数:MethodInvocation表示的是额外功能所增加给的那个原始方法。
运行原始方法:methodInvocation.proceed(),在原始方法前面写的代码就运行在原始方法之前,反之。
返回值:代表原始方法返回值
*/
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("前置增强");
Object proceed = methodInvocation.proceed();
System.out.println("后置增强");
return proceed;
}
}
8.2.5、切入点
切入点决定额外功能加入的位置。他分为两部分:
execution()
:切入点函数* *(..)
:切入点表达式
8.2.5.1、方法切入点表达式
* *(..)
:第一*
对应方法的修饰符(*
表示任意),第二个*
对应方法的方法名,(..)
对应方法的任意参数列表,所以这个切入点表达式表示的是所有方法。
定义login方法作为切入点
* login(..)
定义login方法且方法有两个字符串类型的参数作为切入点
* login(String,String)
这个方式有一个很致命的缺陷:切入的方法不够精准。我们需要使用精准方法切入点限定。所以我们在指定方法的时候,如果需要精准一点,需要指定包名+类名。
8.2.5.2、类切入点表达式
指定特定的类作为切入点(额外功能加入的位置),这个类中的所有方法都会加上对应的额外功能。
* com.domain.UserService.*(..)
8.2.5.3、包切入点表达式
指定包作为额外功能加入的位置,自然包中的所有类及其方法都会加入额外的功能。在实战中运用比较多。
# 切入点包中的所有类,必须在proxy中,不能在proxy包的子包中
* com.domain.proxy.*.*(..)
# 如果想要当前包及其当前包的子包都进行功能增强的话,必须要这样写
* com.domain.proxy..*.*(..)
8.2.5.4、切入点函数
切入点函数式用于执行切入点表达式,execution
是最为重要的切入点函数,功能最全,可以执行方法切入点表达式、类切入点表达式、包切入点表达式。
他的弊端是执行切入点表达式时,书写比较麻烦。所以Spring提供了其他切入点函数来进行简化execution
书写的复杂度。
8.2.5.4.1、args
他的主要作用是用于函数(方法)的参数匹配。
# 方法参数必须是两个字符串类型的参数
args(String,String)
8.2.5.4.2、within
主要用于类、包切入点表达式的匹配。
# 切入点想选为某个类(UserServiceImpl这个类作为切入点)
whithin(*..UserServiceImpl)
# 切入点想选为某个包
within(com.poroxy..*)
8.2.5.4.3、@annotation
为具有特殊注解的方法加入额外功能,语法格式:@annotation(注解所在的包的全限定名)
// 先写一个自定义注解
@Target(ElementType.METHOD) // 表示可以加在哪里
@Retention(RetentionPolicy.RUNTIME) // 表示什么时候起作用
public @interface Log {
}
<aop:pointcut id="testaop" expression="@annotation(com.anno.Log)"/>
@Log
@Override
public void register(User user) {
System.out.println("注册了");
}
8.2.5.5、切入点函数的逻辑运算
切入点函数的逻辑运算指的是整合多个切入点函数一起配合工作,可以完成更加复杂的需求。
8.2.5.5.1、与操作(and)
# 案例一:满足方法名为login且参数为两个字符串
execution (* login(..) and args(String,String))
注意:与操作不能用于同种类型的切入点函数。
# 案例二:满足方法名为login和register作为切入点
# 这是错误的,不可能一个方法同时叫login和register
execution (* login(..)) and execution(* register(..))
8.2.5.5.2、或操作(or)
# 案例一:满足方法名为login或register作为切入点
execution(* login(..)) or execution(* register(..))
8.2.6、总结
8.3、AOP概述
AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。切面=切入点+额外功能。
AOP 是 OOP 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
他的作用是在程序运行期间,在不修改源码的情况下对方法进行功能增强,优势是可以减少重复代码,提高开发效率,并且便于维护。
8.4、名词解释
Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。
- Target(目标对象):代理的目标对象。
- Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类。
- Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
- Pointcut(切入点):所谓切入点是指我们要对哪些 。
- Joinpoint:进行拦截的定义。
- Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
- Aspect(切面):是切入点和通知(引介)的结合,简单来说就是切入点+增强方法。
- Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
8.5、AOP底层实现(动态代理)
8.5.1、JDK的动态代理
public class TestJDKProxy {
public static void main(String[] args) {
// 1. 创建原始对象
UserService userService = new UserServiceImpl();
// 2. 创建JDK动态代理
InvocationHandler handler = new InvocationHandler() {
@Override
// 参数一:表示代理对象 参数二:额外功能所增加给的原始方法 参数三: 表示原始方法的参数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 原始对象的方法方法运行
Object ret = method.invoke(userService,args);
System.out.println("========after proxy log========");
return ret;
}
};
UserService userServiceProxy = (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),handler);
userServiceProxy.login("admin","123456");
userServiceProxy.register(null);
}
}
8.5.2、CGlib的动态代理
JDK的动态代理是通过实现接口从而保证代理类和原始类的方法一致,但是如果碰到没有接口的时候呢,那么就需要使用到CGlib的动态代理了,他和JDK的动态代理最大的区别是CGlib的动态代理是通过父子继承的手段来实现代理类和原始类方法一致的。
CGlib创建动态代理的原理:父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样既可以保证二者方法的一致,同时在代理类中也可以做新的实现。
package cn.linstudy.cglibProxy.service;
import cn.linstudy.cglibProxy.domain.User;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/25 18:44
*/
public interface UserServiceImpl {
void login(String username,String password);
void register(User user);
}
package cn.linstudy.cglibProxy.proxy;
import cn.linstudy.cglibProxy.domain.User;
import cn.linstudy.jdkProxy.service.impl.UserServiceImpl;
/**
* @Description
* @Author XiaoLin
* @Date 2021/2/25 18:45
*/
public class UserServiceProxy extends UserServiceImpl {
@Override
public void login(String username, String password) {
System.out.println("登录了"+username+password);
}
@Override
public void register(User user) {
System.out.println("注册了"+user);
}
}
public class TestCGlib {
public static void main(String[] args) {
// 创建原始对象
UserService userService = new UserService();
// 通过CGlib方式创建代理对象
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(userService.getClass().getClassLoader()); // 设置类加载器
enhancer.setSuperclass(userService.getClass()); // 设置父类
MethodInterceptor interceptor = new MethodInterceptor() {
// 等同于InvocationHandler 的 invoke 方法
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
System.out.println("=====cglib log=====");
Object ret = method.invoke(userService, args);
return ret;
}
};
enhancer.setCallback(interceptor);
UserService serviceProxy = (UserService) enhancer.create();
serviceProxy.login();
serviceProxy.register();
}
}
8.5.3、总结
- JDK动态代理:Proxy.newProxyInstance(),通过接口创建代理实现类。
- CGlib动态代理:Enhancer,通过继承父类的方式创建代理类。
8.6、用注解实现AOP
public interface UserService {
void register(User user);
void login(String username, String password);
}
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("注册了");
}
@Override
public void login(String username, String password) {
System.out.println("登录了");
}
}
@Aspect
public class MyAspect {
@Around("execution(* * (..))") // 写切入点表达式
// joinPoint 表示原始方法
public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("====aspect前置增强====");
Object ret = joinPoint.proceed(); // 代表原始方法执行
System.out.println("====aspect后置增强====");
return ret;
}
}
8.6.1、@Pointcut
如果我们想为多个方法配置同一个切入点表达式,那么就会出现冗余,这个时候我们能想到的就是将公共的切入点表达式提取出来,那么就需要使用到一个注解:@Pointcut
,注意的是,注解所在的方法必须是public void修饰,且没有方法体。
切入点复用就是在切面类中定义一个函数,用@Pointcut
注解,通过这种方式,定义切入点表达式,后续更加有利于切入点的复用。
@Aspect
public class MyAspect {
// 将公共的切入点表达式提取出来,注意的是,注解所在的方法必须是public void修饰,且没有方法体
@Pointcut("execution(* * (..))")
@Around(value = "MyPoint()") // 引入切入点表达式
public Object Around(ProceedingJoinPoint joinPoint) throws Throwable { // joinPoint 表示原始方法
System.out.println("====aspect前置增强====");
Object ret = joinPoint.proceed(); // 代表原始方法执行
System.out.println("====aspect后置增强====");
return ret;
}
@Around(value = "MyPoint()") // 引入切入点表达式
public Object Around1(ProceedingJoinPoint joinPoint) throws Throwable { // joinPoint 表示原始方法
System.out.println("====aspect tx====");
Object ret = joinPoint.proceed(); // 代表原始方法执行
System.out.println("====aspect tx====");
return ret;
}
}
8.6.2、动态代理的创建方式
AOP底层实现有两种:
- JDK的动态代理:通过接口实现,做新的实现方法来创建代理对象。
- CGlib动态代理:通过继承父类,做一个新的子类出来创建代理对象。
在默认情况下,我们dubug一下可以发现,默认使用的是JDK动态代理的方式来进行AOP编程的。
在某些情况下,我们想将默认的JDK动态代理的方式转变为CGlib动态代理的方式,那么该如何实现呢?
在配置文件中,我们之前写过一个配置<aop:aspectj-autoproxy />
,这段配置的作用是告诉Spring,我们要开始基于注解在进行AOP编程了,这段配置中有一个属性proxy-target-class="false"
,他的默认值是false
,表示默认使用JDK的动态代理,如果将中国值设置为true
,那么则表示使用CGlib动态代理。
这个标签只适用于基于注解的AOP开发。如果是基于传统的AOP开发的话,不基于注解,那么需要在<aop-config>
标签中,添加proxy-target-class="false"
属性配置即可。和注解的方式相比,属性是一样的,只是写的位置不一样。
8.7、总结
- 点赞
- 收藏
- 关注作者
评论(0)