代理模式和模版模式
# 代理模式和模版模式
# 代理模式
# 定义
代理模式(Proxy Pattern) 是一种结构型设计模式,它提供了一种控制对象访问的方式。通过引入一个代理对象来间接访问目标对象,可以在不改变目标对象的情况下,添加额外的功能或控制对目标对象的访问。
换成现实生活中的例子就是,看电视,最初我们需要去按电视上的一些按键来换台、调节音量。后来我们使用遥控器,通过按上面的按键来实现换台和调节音量。此时,遥控器就是一个代理对象,而电视机就是被代理的。
# 优势
- 增强功能:可以在不修改原始对象的情况下,为其添加新的功能,如日志记录、性能监控、事务管理等。
- 控制访问:提供了一种机制来限制对某些对象的访问,例如基于用户权限的访问控制。
- 延迟初始化:对于开销较大的对象,可以通过代理模式实现延迟加载,仅在真正需要时才实例化。
当然还有一些其他的,这里就不再列举。
代理模式中有两种,一个是静态代理
,另一个是动态代理
。
# 静态代理
# 定义
静态代理 是指在编译时就已经确定了代理类和被代理类之间的关系,并且代理类通常是一个具体的类,实现了与被代理类相同的接口或继承自被代理类。
你可能会问为什么需要有相同的接口或继承被代理类?
它有以下几个特点:
- 提前定义:代理类和被代理类的关系在编译期就已确定。
- 固定结构:每个被代理类都需要一个对应的代理类,因此如果有很多被代理类,就需要编写很多代理类。
- 代码冗余:对于多个具有相同接口的类,需要为每个类都写一个代理类,导致代码冗余。
以下是一个简单的实现:
// 抽象主题接口
interface Service {
void performOperation();
}
// 真实主题类
class RealService implements Service {
@Override
public void performOperation() {
System.out.println("Performing the operation.");
}
}
// 静态代理类
class StaticServiceProxy implements Service {
private final RealService realService;
// 构造函数注入真实主题
public StaticServiceProxy(RealService realService) {
this.realService = realService;
}
@Override
public void performOperation() {
// 执行前的日志记录
System.out.println("Logging before operation...");
// 调用真实主题的方法
realService.performOperation();
// 执行后的日志记录
System.out.println("Logging after operation.");
}
}
// 客户端代码
public class StaticProxyExample {
public static void main(String[] args) {
// 创建真实主题对象
RealService realService = new RealService();
// 创建静态代理对象,并传入真实主题
Service proxy = new StaticServiceProxy(realService);
// 通过代理调用方法
proxy.performOperation();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
上文中的问题如果还没想清楚,这里提供一个答案:
确实,没有相同的接口或不继承被代理类也可以实现代理类。但你会丧失以下特性:
丧失多态性:
- 如果你需要在不同场景下动态切换使用
RealService
或ProxyService
,会非常困难,因为它们没有共同的接口或父类。无法实现以下:
Service service = new ProxyService(new RealService());
service.doSomething(); // 无法编译,因为没有公共接口。
2
调用不统一:
- 调用方必须记住
RealService
调用doSomething()
,而ProxyService
调用proxyDoSomething()
,增加了使用复杂度。当然你可能会说,那我用相同的名称再加上一个Proxy前缀不就容易记忆了吗? - 那如果被调用者方法特别多呢?你岂不是每一个方法都要加,还不如直接实现同一个接口或继承它来得轻松
扩展性差:
- 如果未来需要对其他类进行代理,每个代理类都必须定义新方法(如
proxyDoSomethingElse()
),无法复用接口定义。 - 如果被代理类需要多个代理类,其中的方法岂不是都要重新编写,远远不如直接继承来的简单
# 优势和劣势
它的优势很明显,可以在不改变目标类的情况下,实现功能的扩展。
劣势同样的,需要为每个被代理类编写一个代理类,增加了代码量和维护成本。一旦被代理类发生改变,如果代理类有多个,则需要对每个类都进行检查和修改。
在 Spring 框架中,静态代理的应用相对较少,因为 Spring 更倾向于使用动态代理(如基于 JDK 动态代理或 CGLIB)来实现 AOP(面向切面编程)、事务管理等功能。下面就让我们再来看看动态代理。
# 动态代理
# 定义
动态代理(Dynamic Proxy) 是一种在运行时动态创建代理对象的技术,而不是在编译时就已经确定了代理类和被代理类之间的关系。动态代理允许你在不修改原始对象的情况下,为任意实现了特定接口的对象生成代理,并且可以在调用方法前后添加额外的逻辑,如日志记录、权限检查等。
# 优势
动态代理的主要优势在于它的灵活性和可扩展性。它不需要为每个需要代理的对象编写具体的代理类,而是在运行时根据需要动态生成代理对象。这使得代码更加简洁,减少了重复代码,并提高了系统的可维护性和扩展性。
- 灵活性高:可以在运行时为任意实现了特定接口的类生成代理,而无需为每个类单独编写代理类。
- 减少代码量:减少了重复代码,提高了代码的可维护性。
- 性能优化:对于实现了接口的对象,JDK 动态代理是首选,因为它直接利用了 JVM 的反射机制;对于没有实现接口的对象,CGLIB 提供了高效的代理机制。
- 易于扩展:动态代理可以根据需求轻松添加新的功能,如事务管理、缓存、安全控制等。
# 动态代理的工作原理
动态代理通常依赖于反射机制,在 Java 中可以通过 java.lang.reflect.Proxy
类或第三方库(如 CGLIB)来实现。Proxy
类允许你创建一个实现了指定接口的新类实例,而这个实例的行为可以由你提供的 InvocationHandler
接口来控制。
# 动态代理的类型
- 基于接口的动态代理:使用
java.lang.reflect.Proxy
实现,适用于实现了接口的对象(这就是常说的JDK代理)。再简单一点就是,它是通过接口+反射,如果被代理类没有实现对应接口,它是无法生效的。 - 基于类的动态代理:使用 CGLIB 库实现,适用于没有实现接口的类,通过继承的方式生成代理,然后直接调用。具体来说,CGLIB 通过在运行时生成目标类的子类,并重写其非 final 方法来实现代理功能。由于 CGLIB 是通过继承实现的,它无法代理 final 类或 final 方法,因为这些成员不能被子类覆盖。
Spring 中的 AOP 确实使用了动态代理机制,具体来说,默认情况下它会根据目标对象是否实现了接口来选择使用 JDK 动态代理或 CGLIB 动态代理。JDK 动态代理适用于实现了接口的对象,而 CGLIB 动态代理则可以处理没有实现接口的类。
你可以通过配置强制使用 CGLIB 代理,即使目标对象实现了接口。这可以通过设置 proxy-target-class
属性为 true
来实现。例如,在 Spring Boot 的 application.properties
文件中添加如下配置:
spring.aop.proxy-target-class=true
# 示例
# JDK代理
以下是基于JDK的动态代理示例代码:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 定义接口
interface Service {
void performOperation();
}
// 真实主题类
class RealService implements Service {
@Override
public void performOperation() {
System.out.println("Performing the operation.");
}
}
// 动态代理处理器
class DynamicServiceProxy implements InvocationHandler {
private final Object target;
public DynamicServiceProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 执行前的日志记录
System.out.println("Logging before " + method.getName() + "...");
// 调用目标对象的方法
Object result = method.invoke(target, args);
// 执行后的日志记录
System.out.println("Logging after " + method.getName() + ".");
return result;
}
}
// 客户端代码
public class DynamicProxyExample {
public static void main(String[] args) {
// 创建真实主题对象
Service realService = new RealService();
// 创建动态代理对象
Service proxy = (Service) Proxy.newProxyInstance(
realService.getClass().getClassLoader(),
realService.getClass().getInterfaces(),
new DynamicServiceProxy(realService)
);
// 通过代理调用方法
proxy.performOperation();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 使用 CGLIB 库
CGLIB(Code Generation Library)是一个高性能的字节码生成库,适用于为没有实现接口的类创建代理。以下是使用 CGLIB 的示例:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
// 没有实现接口的真实主题类
class RealService {
public void performOperation() {
System.out.println("Performing the operation.");
}
}
// 动态代理处理器
class CglibServiceProxy implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 执行前的日志记录
System.out.println("Logging before " + method.getName() + "...");
// 调用目标对象的方法
Object result = proxy.invokeSuper(obj, args);
// 执行后的日志记录
System.out.println("Logging after " + method.getName() + ".");
return result;
}
}
// 客户端代码
public class CglibProxyExample {
public static void main(String[] args) {
// 创建增强器
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(RealService.class);
enhancer.setCallback(new CglibServiceProxy());
// 创建动态代理对象
RealService proxy = (RealService) enhancer.create();
// 通过代理调用方法
proxy.performOperation();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 不足之处
额外的性能开销:
- 反射机制:基于接口的动态代理(如
java.lang.reflect.Proxy
)依赖于 Java 的反射机制来调用方法。反射操作通常比直接方法调用慢,因为它们涉及额外的查找和验证步骤。 - 字节码操作:基于类的动态代理(如 CGLIB)需要在运行时生成新的类,并且可能涉及复杂的字节码操作,这也会带来一定的性能开销。
# 模版模式
模板模式(Template Method Pattern) 是一种行为设计模式,它定义了一个算法的骨架,而将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下重定义算法的某些特定步骤。
它有如下几个主要特点:
- 封装不变部分:将公共的行为和逻辑封装在父类中,避免代码重复。
- 开放封闭原则:允许通过扩展来修改行为,而不是修改现有代码。
- 钩子方法:提供一些“钩子”方法,让子类可以决定是否执行某些操作或以何种方式执行。
# 示例
下面是一个使用模板模式的简单示例,假设我们要创建一个文本处理程序,该程序包括加载、处理和保存文本三个步骤:
// 抽象类定义了算法框架
abstract class TextProcessor {
// 模板方法,定义了算法的骨架
public final void processText() {
loadText();
if (needProcess()) {
processTextContent();
}
saveText();
}
// 具体方法,由父类实现
protected void loadText() {
System.out.println("Loading text from source...");
}
// 抽象方法,留给子类实现
protected abstract void processTextContent();
// 钩子方法,默认实现为空
protected boolean needProcess() {
return true;
}
// 具体方法,由父类实现
protected void saveText() {
System.out.println("Saving processed text...");
}
}
// 子类实现了抽象方法
class UpperCaseTextProcessor extends TextProcessor {
@Override
protected void processTextContent() {
System.out.println("Converting text to upper case...");
}
}
class LowerCaseTextProcessor extends TextProcessor {
@Override
protected void processTextContent() {
System.out.println("Converting text to lower case...");
}
// 重写钩子方法,决定是否需要处理
@Override
protected boolean needProcess() {
return false; // 不进行转换
}
}
// 客户端代码
public class TemplateMethodDemo {
public static void main(String[] args) {
TextProcessor upperCaseProcessor = new UpperCaseTextProcessor();
upperCaseProcessor.processText(); // 输出: Loading text... Converting text to upper case... Saving processed text...
TextProcessor lowerCaseProcessor = new LowerCaseTextProcessor();
lowerCaseProcessor.processText(); // 输出: Loading text... Saving processed text...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# springboot中的应用
在 Spring Boot 中,模板模式被广泛应用于多个地方,尤其是在以下方面:
- Spring MVC 控制器:控制器通常继承自
AbstractController
或其他抽象类,这些类提供了通用的功能,如请求映射、参数解析等,而具体的业务逻辑则由子类实现。 - Spring Data JPA:
JpaRepository
和CrudRepository
提供了一系列基本的 CRUD 操作方法,开发者只需定义自己的接口并继承这些接口即可获得默认实现。如果需要额外的操作,可以添加自定义的方法。 - 事务管理:
@Transactional
注解背后的实现也使用了模板模式。PlatformTransactionManager
提供了事务管理的基本框架,而具体的事务策略(如传播行为、隔离级别等)可以通过配置来定制。 - 某些xxTemplate类:通过名字就能看出它应该是一个模版方法的实现,而实际上他也是的,
RedisTemplate
是 Spring Data Redis 提供的一个用于与 Redis 进行交互的核心组件。它提供了对 Redis 操作的基本框架和默认行为,同时允许开发者通过扩展或配置来定制特定的行为。RedisTemplate
封装了与 Redis 通信的所有细节,包括连接管理、序列化/反序列化等。开发者不需要关心这些底层实现,只需调用高层 API 即可完成对 Redis 的读写操作。
RedisTemplate它链接redis的工具是什么,用的什么方法序列化,怎么重新设置连接工具、如何替换序列化方法?
文末有答案供参考
当然还有其他的一些地方这里就不再列举了。
# 优劣
# 扩展
RedisTemplate
默认使用的序列化器是 JDK 序列化器 (JdkSerializationRedisSerializer
)。为了提高性能、减少数据大小或增强跨语言互操作性,推荐根据具体需求选择合适的序列化器并进行相应的配置。例如,使用 JSON 序列化器如 Jackson2JsonRedisSerializer
可以提供更好的
虽然 RedisTemplate
提供了许多常用的操作方法,但有时你可能需要自定义行为。为此,RedisTemplate
允许你通过设置不同的序列化器、命令回调(如 execute
方法)等方式来扩展其功能,而不会改变现有的代码结构。
// 自定义序列化器
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
// 使用 execute 方法执行自定义命令
redisTemplate.execute((RedisCallback<Long>) connection -> {
// 执行自定义 Redis 命令
return connection.set("key".getBytes(), "value".getBytes());
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 连接工具
在 Spring Data Redis 中,默认的 Redis 连接工具并不是固定的,而是取决于你所使用的版本以及配置。不过,在较新的版本中(如 Spring Boot 2.x 和 Spring Data Redis 2.x),默认的连接工具已经从 Jedis 切换到了 Lettuce。
默认连接工具的变化
- 早期版本:在早期版本的 Spring Boot 和 Spring Data Redis 中,默认使用的是 Jedis 作为 Redis 客户端库。
- 当前版本:自 Spring Boot 2.0 和 Spring Data Redis 2.0 起,默认的 Redis 客户端库变更为 Lettuce。这是因为 Lettuce 提供了更好的非阻塞 I/O 支持和更高效的连接管理机制,特别适合于高并发环境。
替换这里就不再展开了,欢迎大家自行查阅资料。