参考自B站视频《孙哥说Spring5》
关于代理模式,详情可参考 https://blog.csdn.net/Kobe_k/article/details/105771530
在 JavaEE 分层开发开发中,哪个层次对于我们来讲最重要?
Service 层中包含了哪些代码?
将额外功能写在 Service 层好不好?
Controller
):需要在 Service 层书写额外功能。如果在 Service层 代码中将核心功能和额外功能混合在一起写,会造成代码的混乱,维护起来不方便,因此可以通过代理模式,在代理类中整合额外功能和核心功能,有利于代码的维护。
举个例子:
静态代理:
/* 实体类 */
public class User {}
/* Service接口 */
public interface UserService {
void register(User user);
boolean login(String name, String password);
}
/* 实现类 */
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("UserServiceImpl.register 业务运算 + DAO");
}
@Override
public boolean login(String name, String password) {
System.out.println("UserServiceImpl.login 业务运算 + DAO");
return true;
}
}
/**
* 静态代理类
*/
public class UserServiceProxy implements UserService { // 实现原始类相同的接口
// 代理类中必须有原始类,调用其核心功能
private UserService userService = new UserServiceImpl();
@Override
public void register(User user) {
// 通过代理类,添加额外功能,例如日志输出、事务开启关闭提交等
System.out.println("---log---"); // 额外功能
userService.register(user);
}
@Override
public boolean login(String name, String password) {
System.out.println("---log---"); // 额外功能
return userService.login(name, password);
}
}
从上可以将代理模式的框架提取一下:
// 目标类
public interface UserService {
// 方法
m1
m2
}
// 原始实现类
public UserServiceImpl implements UserServiceImpl {
m1 ---> 业务运算、调用DAO
m2
}
----------------------------------------------------
// 代理类:要实现目标类相同的接口
public UserServiceProxy implements UserService {
// 包含原始实现类
private UserServiceImpl userServiceImpl;
// 实现方法 方法中要调用到原始实现类,然后在此基础上再添加其他额外功能
m1
m2
}
如果一个类就有一个代理类和原始实现类,一旦类的数量多了起来,静态代理会造成类爆炸的问题,不利于项目管理;另一方面,如果额外功能较多,那么修改额外功能也比较麻烦
因此而引出了动态代理
Spring的两大特色之一:AOP,即面向切面编程,就是类似于动态代理的思想;
先举个例子:
引入依赖
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.1.14.RELEASEversion>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.8.13version>
dependency>
创建接口、原始对象
/* 接口 */
public interface UserService {
void register(User user);
boolean login(String name, String password);
}
/* 实现类 */
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("UserServiceImpl.register 业务运算 + DAO");
}
@Override
public boolean login(String name, String password) {
System.out.println("UserServiceImpl.login 业务运算 + DAO");
return true;
}
}
创建额外功能对象,实现 MethodBeforeAdvice
接口
public class Before implements MethodBeforeAdvice {
/**
* 作用: 把需要运行在原始方法执行之前运行的额外功能, 书写在 before 方法中
*/
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("---method before advice log---");
}
}
配置文件配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="com.yusael.aop.UserServiceImpl"/>
<bean id="before" class="com.yusael.aop.Before"/>
<aop:config>
<aop:pointcut id="pc" expression="execution(* * (..))"/>
<aop:advisor advice-ref="before" pointcut-ref="pc"/>
aop:config>
beans>
调用测试
/**
* 用于测试动态代理
*/
@Test
public void test1() {
// 获得 Spring 工厂创建的动态代理对象
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 获得Bean
UserService userService = (UserService) ctx.getBean("userService");
userService.login("admin", "1234");
userService.register(new User());
}
Spring 创建的动态代理类在哪里?
什么是 动态字节码技术?
结论:
动态代理编程能简化代理的开发
总结:动态代理不需要定义类文件,都是JVM运行过程中动态创建的,所以不会造成类文件数量过多,影响项目管理的问题,而且额外功能的维护性也大大增强,更换额外功能的类即可。
作用: 额外功能运行在原始方法执行之前,进行额外功能操作。
public class Before implements MethodBeforeAdvice {
/**
* 作用: 把需要运行在原始方法执行之前运行的额外功能, 书写在 before 方法中
*
* Method: 额外功能所增加给的那个原始方法
* login
* register
* --------
* showOrder
*
* Object[]: 额外功能所增加给的那个原始方法的参数
* 在 login 方法中的参数为:String name,String password
* 在 register 方法中的参数为:User user
* --------
*
* Object: 额外功能所增加给的那个原始对象
* UserServiceImpl
* ---------------
* OrderServiceImpl
*/
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("---new method before advice log---");
}
}
before()
方法的 3 个参数在实战中,该如何使用?
MethodInterceptor接口
:额外功能可以根据需要运行在原始方法执行 前、后、前后。
MethodInvocation
:额外功能所增加给的那个原始方法 (login, register)Object
:原始方法的返回值 (没有就返回 null)invocation.proceed()
:原始方法运行额外功能运行在原始方法 之前:
public class Around implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("---额外功能运行在原始方法执行之前---");
Object ret = methodInvocation.proceed(); // 原始方法运行, 获取原始方法的返回值
return ret;
}
}
额外功能运行在原始方法 之后:
public class Around implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object ret = methodInvocation.proceed(); // 原始方法运行, 获取原始方法的返回值
System.out.println("---额外功能运行在原始方法执行之后---");
return ret;
}
}
额外功能运行在原始方法 之前、之后:
public class Around implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("---额外功能运行在原始方法执行之前---");
Object ret = methodInvocation.proceed(); // 原始方法运行, 获取原始方法的返回值
System.out.println("---额外功能运行在原始方法执行之后---");
return ret;
}
}
额外功能运行在原始方法抛出异常的时候:
public class Around implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object ret = null;
try {
ret = methodInvocation.proceed(); // 原始方法运行, 获取原始方法的返回值
} catch (Throwable throwable) {
System.out.println("---额外功能运行在原始方法抛异常的时候---");
}
return ret;
}
}
MethodInterceptor
影响原始方法的返回值:
public class Around implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("---log---");
Object ret = methodInvocation.proceed();
// 根据需求直接返回即可,也可以返回另外一个值,这里的返回值就是调用后的返回值
return false;
}
}
切入点:决定了额外功能的切入位置(方法)
<aop:pointcut id="pc" expression="execution(* * (..))"/>
execution()
:切入点函数* *(..)
:切入点表达式定义一个方法的格式
public void add(int i, int j)
对应的表达式格式为
* * (..)
* 即通配符,任意都行
每个位置对应的含义:
* ---> 修饰符 返回值
* ---> 方法名
() ---> 参数表
.. ---> 对于参数没有要求 (参数有没有,参数有⼏个都行,参数是什么类型的都行)
举个例子
定义 login
方法作为切入点:
<aop:pointcut id="pc" expression="execution(* login (..))"/>
<aop:pointcut id="pc" expression="execution(* register (..))"/>
定义方法名为 login
且 有两个字符串类型的参数 作为切入点;
<aop:pointcut id="pc" expression="execution(* login (String,String))"/><
<aop:pointcut id="pc" expression="execution(* register (com.yusael.proxy.User))"/>
<aop:pointcut id="pc" expression="execution(* login(String, ..))"/>
精准方法切入点限定
修饰符 返回值 包.类.方法(参数)
<aop:pointcut id="pc" expression="execution(* com.yusael.proxy.UserServiceImpl.login(..))"/>
<aop:pointcut id="pc" expression="execution(* com.yusael.proxy.UserServiceImpl.login(String, String))"/>
指定 特定类作为切入点(额外功能加入的位置),这个类中的所有方法,都会加上对应的额外功能。
# 类中所有的方法加入了额外功能
<aop:pointcut id="pc" expression="execution(* com.yusael.proxy.UserServiceImpl.*(..))"/>
# 忽略包
1. 类只存在一级包 '*.'->只能处理一层包
<aop:pointcut id="pc" expression="execution(* *.UserServiceImpl.*(..))"/>
2. 类存在多级包 需要用 '*..' 来处理多层包结构
<aop:pointcut id="pc" expression="execution(* *..UserServiceImpl.*(..))"/>
指定包作为额外功能加入的位置,自然包中的所有类及其方法都会加入额外的功能。
# 切入点包中的所有类,必须在proxy中,不能在proxy包的⼦包中
<aop:pointcut id="pc" expression="execution(* com.yusael.proxy.*.*(..))"/>
# 切入点当前包及其⼦包都生效
<aop:pointcut id="pc" expression="execution(* com.yusael.proxy..*.*(..))"/>
# 和类切入点类似,也是使用 '..' 来表示多层结构
切入点函数:用于执行切入点表达式
execution
execution
是最为重要的切入点函数,功能最全;可以执行 方法切入点表达式、类切入点表达式、包切入点表达式;
弊端:execution 执⾏切入点表达式 ,书写麻烦
格式:execution(* com.yusael.proxy..*.*(..))
注意:其他的 切入点函数 简化的是 execution 的书写复杂度,功能上完全⼀致。
args
args
作用:主要用于 函数(方法) 参数的匹配;
切入点:方法参数必须得是 2 个字符串类型的参数
# 使用 execution
<aop:pointcut id="pc" expression="execution(* *(String, String))"/>
# 使用 args
<aop:pointcut id="pc" expression="args(String, String)"/>
within
within
作用:主要用于进行 类、包切入点表达式 的匹配。
切入点: UserServiceImpl 这个类
# 使用 execution
<aop:pointcut id="pc" expression="expression(* *..UserServiceImpl.*(..))"/>
# 使用 within
<aop:pointcut id="pc" expression="within(*..UserServiceImpl)"/>
---------------------------------------------------------------------------
切入点: com.yusael.proxy 这个包
# 使用 execution
<aop:pointcut id="pc" expression="execution(* com.yusael.proxy..*.**(..)"/>
# 使用 within
<aop:pointcut id="pc" expression="within(com.yusael.proxy..*)"/>
@annotation
作用:为 具有特殊注解的方法 加入额外功能。
例如我们自定义了一个注解:Log
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
然后我们要为使用了 Log
注解的方法加入额外功能。
<aop:pointcut id="pc" expression="@annotation(com.yusael.Log)"/>
切入点函数的逻辑运算 指的是:整合多个切入点函数⼀起配合工作,进⽽完成更为复杂的需求。
and 逻辑与
案例: 方法名叫 login 同时 参数是 2个字符串
# execution
<aop:pointcut id="pc" expression="execution(* login(String, String))"/>
# execution and args
<aop:pointcut id="pc" expression="execution(* login(..)) and args(String, String))"/>
注意:与操作不同⽤于同种类型的切⼊点函数
以下这个是错误的, 因为不存在同时叫 login 和 register 的方法
<aop:pointcut id="pc" expression="execution(* login(..)) and execution(* register(..))"/>
or 逻辑或
案例: 方法名叫 register 或 login 的⽅法作为切⼊点 如果用and就不存在了
<aop:pointcut id="pc" expression="execution(* login(..)) or execution(* register(..))"/>
POP (Producer Oriented Programing
)
OOP (Object Oritened Programing
)
AOP (Aspect Oriented Programing
)
AOP 的概念:
那为什么称为切面呢?
首先要明确两个核心问题:
Spring 动态代理类的创建有两种方式:JDK动态代理创建、CGlib动态代理创建
先上一段代码:
public class TestJDKProxy {
/**
1. 借⽤类加载器 TestJDKProxy 或 UserServiceImpl 都可以
2. JDK8.x 前必须加 final
final UserService userService = new UserServiceImpl();
*/
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 {
// 加入额外功能,例如日志记录
System.out.println("---- proxy log ----");
// 原始方法运行
Object ret = method.invoke(userService, args);
return ret;
}
};
// 创建代理类
UserService userServiceProxy = (UserService) Proxy.newProxyInstance(
TestJDKProxy.class.getClassLoader(),
userService.getClass().getInterfaces(),
handler);
// 代理类执行方法
userServiceProxy.login("zhenyu", "123456");
userServiceProxy.register(new User());
}
}
那么也许有人会问了,那第一个参数中的类加载器有什么作用呢?
还记得之前提到的动态字节码技术吗?其实这个类加载器就是利用了这个技术,动态地创建类的字节码加载到JVM中。
另外,类的加载是需要类加载器的,但是动态代理类是动态创建的,其动态字节码是没有真正的字节码文件的,因此JVM也就无法为其指定一个类加载器,但是类加载器又是必需的,所以可以通过借用类加载器来加载动态代理类。
CGlib 创建动态代理的原理
上代码:
public class TestCglib {
public static void main(String[] args) {
// 1. 创建原始对象
UserService userService = new UserService();
/*
2. 通过 cglib 方式创建动态代理对象
对比 jdk 动态代理 ---> Proxy.newProxyInstance(classLoader, interface, invocationHandler);
Enhancer.setClassLoader() <==> classLoader
Enhancer.setSuperClass() <==> interface
Enhancer.setCallBack() ---> 实现 MethodInterceptor(cglib) 接口 <==> invocationHandler
Enhancer.createProxy() ---> 创建代理对象
*/
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(TestCglib.class.getClassLoader());
enhancer.setSuperclass(userService.getClass());
MethodInterceptor interceptor = new MethodInterceptor() {
@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 userServiceProxy = (UserService) enhancer.create();
userServiceProxy.login("zhenyu", "123456");
userServiceProxy.register(new User());
}
}
Proxy.newProxyInstance
:通过接口创建代理的实现类Enhancer
:通过继承⽗类创建的代理类思路分析:主要通过 BeanPostProcessor
将原始对象加工为代理对象
有两个回调方法,一个在调用初始化方法之前被调用;一个在调用初始化方法之后被调用
开发流程
BeanPostProcessor
进行加工BeanPostProcessor
进行配置// 实现 BeanPostProcessor 进行加工
public class ProxyBeanPostProcessor implements BeanPostProcessor {
@Override
// bean初始化方法调用前被调用
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
// bean初始化方法调用后被调用
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// JDK动态代理创建代理类
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 额外功能
System.out.println("--- new log ---");
// 原始功能 传入参数
Object ret = method.invoke(bean, args);
// 返回
return ret;
}
};
// 生成代理类
return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), handler);
}
}
<bean id="userService" class="com.yusael.factory.UserServiceImpl"/>
<bean id="proxyBeanPostProcessor" class="com.yusael.factory.ProxyBeanPostProcessor"/>
id
值获取Bean的时候,由于BeanPostProcessor
的加工,所以我们获得到的是代理类。不管是不是使用注解开发,其步骤都是一样的:
举个例子
原始功能
// 接口
public interface UserService {
void register(User user);
boolean login(String name, String password);
}
// 原始实现类
public class UserServiceImpl implements UserService {
@Override
public void register(User user) {
System.out.println("UserServiceImpl.register 业务运算 + DAO");
// throw new RuntimeException("测试异常");
}
@Log
@Override
public boolean login(String name, String password) {
System.out.println("UserServiceImpl.login 业务运算 + DAO");
return true;
}
}
组装切面类(将剩余步骤简化,合为一体,将之看成一个对象)
/*
原来的开发步骤
1. 实现额外功能
public class MyAround implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) {
Object ret = invocation.invoke();
return ret;
}
}
2. 配置文件中组装切面
切入点
组装
*/
/* 现在可以简化上面的步骤,通过注解 */
@Aspect // 切面类
public class MyAspect {
@Around("execution(* login(..))") // 切入点
// 额外功能
// ProceedingJoinPoint <==> MethodInterceptor 两者的作用是一样的
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("---- aspect log ----");
Object ret = joinPoint.proceed();
return ret;
}
}
配置文件中配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="com.yusael.aspect.UserServiceImpl"/>
<bean id="around" class="com.yusael.aspect.MyAspect"/>
<aop:aspectj-autoproxy/>
beans>
当切入点表达式是一样的时候,如果有多个额外功能,那么在注解中需要写多次表达式,这会让代码维护起来不方便,可以通过切入点复用的方式来统一切入点;
切入点复用:在切面类中定义⼀个函数,上面用 @Pointcut
注解。
通过这种方式定义切入点表达式,后续更加有利于切入点复用。
@Aspect
public class MyAspect {
@Pointcut("execution(* login(..))") // 切入点复用
public void myPoincut() {}
@Around(value = "myPoincut()") // 直接引用上面的切入点
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("---- aspect log ----");
Object ret = joinPoint.proceed();
return ret;
}
@Around(value = "myPoincut()") // 直接引用上面的切入点
public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("---- aspect transaction ----");
Object ret = joinPoint.proceed();
return ret;
}
}
AOP 底层实现 2 种代理创建方式:
默认情况 AOP 编程 底层应用 JDK动态代理 创建方式。
那如何切换两种动态代理的创建方式呢?
<aop:aspectj-autoproxy proxy-target-class="true"/>
<aop:config proxy-target-class="true">
aop:config>
在同⼀个业务类中,进⾏业务方法间的相互调用,只有最外层的方法,才是加入了额外功能
AppicationContextAware接口
获得⼯厂,进而获得代理对象。// 实现 ApplicationContextAware 接口 Spring会自动调用 setApplicationContext 方法将工厂注入
public class UserServiceImpl implements UserService, ApplicationContextAware {
// 工厂是重量级资源,不应该多次创建工厂,而是拿到已经创建好了的工厂
private ApplicationContext ctx;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ctx = applicationContext;
}
@Override
public void register(User user) {
System.out.println("UserServiceImpl.register 业务运算 + DAO");
// this.login("zhenyu", "123456"); // 这么写调用的是本类的 login 方法, 即原始对象的 login 方法
// 为什么不在这里创建一个工厂获取代理对象呢?
// Spring的工厂是重量级资源, 一个应用中应该只创建一个工厂.
// 因此我们必须通过 ApplicationContextAware 拿到已经创建好的工厂
UserService userService = (UserService) ctx.getBean("userService");
userService.login("yusael", "123456");
}
@Override
public boolean login(String name, String password) {
System.out.println("UserServiceImpl.login 业务运算 + DAO");
return true;
}
}
测试类
public static void main(String[] args){
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = (UserService) cts.getBean("userService");
userService.register(new User());
}