文章目录
-
- 方式一:继承重写(最简单,优先考虑)
-
- [示例:重写 Jedis 的 `set` 方法](#示例:重写 Jedis 的
set方法) - [示例:重写 Spring Data Redis 的 RedisTemplate](#示例:重写 Spring Data Redis 的 RedisTemplate)
- [示例:重写 Jedis 的 `set` 方法](#示例:重写 Jedis 的
- 方式二:装饰器模式(组合,比继承更灵活)
-
- [示例:包装 RedisTemplate 的 opsForValue](#示例:包装 RedisTemplate 的 opsForValue)
- [方式三:Spring AOP 切面拦截(无侵入,推荐用于增强)](#方式三:Spring AOP 切面拦截(无侵入,推荐用于增强))
-
- [示例:拦截 RedisTemplate 的所有 set 操作](#示例:拦截 RedisTemplate 的所有 set 操作)
- 方式四:同包同类名覆盖(类加载优先级,黑科技)
-
- [示例:覆盖 Jedis 的某个类](#示例:覆盖 Jedis 的某个类)
- [方式五:Java Agent + 字节码增强(终极方案)](#方式五:Java Agent + 字节码增强(终极方案))
-
- [示例:用 ByteBuddy 拦截 Jedis 的 set 方法](#示例:用 ByteBuddy 拦截 Jedis 的 set 方法)
- [方式六:动态代理(JDK 动态代理 / CGLIB)](#方式六:动态代理(JDK 动态代理 / CGLIB))
-
- [示例:JDK 动态代理包装 JedisCommands 接口](#示例:JDK 动态代理包装 JedisCommands 接口)
- 方案选择决策树
- 面试口述标准答案
先确认你用的是哪个 Redis 客户端(Jedis / Lettuce / Spring Data Redis / Redisson),不同方式适用场景不同。下面按推荐优先级逐一给出可落地代码。
方式一:继承重写(最简单,优先考虑)
适用条件: 目标类不是 final,目标方法不是 final / static / private。
示例:重写 Jedis 的 set 方法
import redis.clients.jedis.Jedis;
public class CustomJedis extends Jedis {
public CustomJedis(String host, int port) {
super(host, port);
}
@Override
public String set(String key, String value) {
// 前置增强:比如加日志、统计、权限校验
System.out.println("自定义set方法被调用,key=" + key);
// 调用原方法
String result = super.set(key, value);
// 后置增强
System.out.println("set执行结果=" + result);
return result;
}
}
使用:
try (CustomJedis jedis = new CustomJedis("localhost", 6379)) {
jedis.set("key1", "value1"); // 走你重写的方法
}
示例:重写 Spring Data Redis 的 RedisTemplate
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
public class CustomRedisTemplate<K, V> extends RedisTemplate<K, V> {
@Override
public ValueOperations<K, V> opsForValue() {
// 包装一层,对 opsForValue 返回的对象做增强
return new CustomValueOperations<>(super.opsForValue());
}
}
坑点:
- 如果类是
final(比如 Lettuce 的很多类),此方式不可用 - 如果方法是
static,不能重写,只能隐藏(不推荐) - 框架内部如果直接 new 原类而不是用你的子类,重写不生效
方式二:装饰器模式(组合,比继承更灵活)
适用条件: 目标类是 final 无法继承,或只想增强部分方法。
示例:包装 RedisTemplate 的 opsForValue
import org.springframework.data.redis.core.ValueOperations;
import java.util.concurrent.TimeUnit;
public class CustomValueOperations<K, V> implements ValueOperations<K, V> {
private final ValueOperations<K, V> delegate; // 被包装的原对象
public CustomValueOperations(ValueOperations<K, V> delegate) {
this.delegate = delegate;
}
@Override
public void set(K key, V value) {
System.out.println("装饰器拦截 set,key=" + key);
delegate.set(key, value); // 委托给原对象
}
@Override
public Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit) {
System.out.println("装饰器拦截 setIfAbsent,key=" + key);
return delegate.setIfAbsent(key, value, timeout, unit);
}
// 其余方法全部委托给 delegate,不做改动
@Override public V get(Object key) { return delegate.get(key); }
@Override public Long increment(K key) { return delegate.increment(key); }
// ... 接口里所有方法都要实现,逐个委托
}
Spring 中注入使用:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
// 直接暴露包装后的 ValueOperations
@Bean
public ValueOperations<String, Object> customValueOperations(RedisTemplate<String, Object> redisTemplate) {
return new CustomValueOperations<>(redisTemplate.opsForValue());
}
}
坑点:
- 接口方法很多时,实现起来繁琐(可以用 Lombok
@Delegate简化) - 如果有多个重载方法,每个都要单独处理
方式三:Spring AOP 切面拦截(无侵入,推荐用于增强)
适用条件: 只想加日志、监控、缓存、权限等横切逻辑,不改原方法核心逻辑。
示例:拦截 RedisTemplate 的所有 set 操作
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RedisMethodAspect {
// 拦截 RedisTemplate 中所有以 set 开头的方法
@Around("execution(* org.springframework.data.redis.core.RedisTemplate.set*(..))")
public Object aroundSetMethods(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
System.out.println("[Redis AOP] 方法=" + methodName + ", 参数=" + java.util.Arrays.toString(args));
try {
Object result = pjp.proceed(); // 执行原方法
long cost = System.currentTimeMillis() - start;
System.out.println("[Redis AOP] 执行成功,耗时=" + cost + "ms");
return result;
} catch (Throwable e) {
System.out.println("[Redis AOP] 执行失败: " + e.getMessage());
throw e;
}
}
// 也可以拦截具体某个方法
@Around("execution(* org.springframework.data.redis.core.ValueOperations.set(..))")
public Object aroundValueSet(ProceedingJoinPoint pjp) throws Throwable {
// 你的增强逻辑
return pjp.proceed();
}
}
坑点:
- Spring AOP 只能拦截 Spring 容器管理的 Bean,自己
new出来的对象不生效 - 只能拦截
public方法(CGLIB 可以拦截 protected,但不推荐) - 同类内部方法调用(this.xxx())不会触发 AOP
- 如果要完全替换方法逻辑(不调用原方法),不调用
pjp.proceed()即可,但要自己返回正确类型
方式四:同包同类名覆盖(类加载优先级,黑科技)
适用条件: 目标类是 final + 方法是 final,且你想彻底替换整个类的实现。
原理: 在你的项目源码中创建一个包名、类名完全相同的类,编译后你的 class 会优先被类加载器加载(因为 target/classes 在 classpath 中排在依赖 jar 前面)。
示例:覆盖 Jedis 的某个类
假设要覆盖 redis.clients.jedis.Jedis 类:
// 在你的项目 src/main/java 下创建完全相同的包路径
package redis.clients.jedis; // 包名必须完全一致
public class Jedis { // 类名必须完全一致
// 重写整个类的实现,或者只改你要的方法
// 注意:这个类会完全替换 jar 包里的 Jedis 类
public String set(String key, String value) {
System.out.println("完全替换的set方法");
// 你的实现...
return "OK";
}
// 其他方法也要实现,否则调用会报错
}
坑点(极多,慎用):
- 这是完全替换,不是增强,原类的所有方法你都得自己实现
- 依赖包升级后,原类结构变了,你的覆盖类可能直接报错
- 团队协作时其他人很难发现这个"黑科技",维护成本极高
- 如果有类加载器隔离(比如 Tomcat、OSGi),可能不生效
- 除非万不得已,否则不要用这种方式
方式五:Java Agent + 字节码增强(终极方案)
适用条件: 需要在运行时动态修改任意类(包括 final 类、JDK 类),且无法通过其他方式实现。
工具: ByteBuddy(推荐)、ASM、Javassist
示例:用 ByteBuddy 拦截 Jedis 的 set 方法
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
public class RedisAgent {
public static void premain(String agentArgs, Instrumentation inst) {
new AgentBuilder.Default()
.type(ElementMatchers.named("redis.clients.jedis.Jedis"))
.transform((builder, typeDescription, classLoader, module, protectionDomain) ->
builder.method(ElementMatchers.named("set")
.and(ElementMatchers.takesArguments(String.class, String.class)))
.intercept(Advice.to(RedisSetAdvice.class))
)
.installOn(inst);
}
}
import net.bytebuddy.asm.Advice;
public class RedisSetAdvice {
@Advice.OnMethodEnter
public static void enter(@Advice.Argument(0) String key,
@Advice.Argument(1) String value) {
System.out.println("[ByteBuddy] 进入set方法,key=" + key + ", value=" + value);
}
@Advice.OnMethodExit(onThrowable = Exception.class)
public static void exit(@Advice.Return String result,
@Advice.Thrown Throwable throwable) {
if (throwable != null) {
System.out.println("[ByteBuddy] set方法异常: " + throwable.getMessage());
} else {
System.out.println("[ByteBuddy] set方法返回: " + result);
}
}
}
使用方式:
# 打成 agent jar,启动时附加
java -javaagent:redis-agent.jar -jar your-app.jar
坑点:
- 复杂度高,需要理解字节码和类加载机制
- 调试困难,出问题很难排查
- 对 JDK 版本敏感,不同 JDK 版本的字节码格式可能有差异
- 一般只有中间件、APM 工具(如 SkyWalking、Pinpoint)才会用
方式六:动态代理(JDK 动态代理 / CGLIB)
适用条件: 目标类实现了接口,用 JDK 动态代理;没有接口,用 CGLIB。
示例:JDK 动态代理包装 JedisCommands 接口
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCommands;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class RedisProxyFactory {
public static JedisCommands createProxy(Jedis jedis) {
return (JedisCommands) Proxy.newProxyInstance(
jedis.getClass().getClassLoader(),
new Class<?>[]{JedisCommands.class},
new RedisInvocationHandler(jedis)
);
}
static class RedisInvocationHandler implements InvocationHandler {
private final Object target;
public RedisInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("set".equals(method.getName())) {
System.out.println("[动态代理] 拦截set方法,参数=" + java.util.Arrays.toString(args));
}
return method.invoke(target, args);
}
}
}
使用:
Jedis jedis = new Jedis("localhost", 6379);
JedisCommands proxy = RedisProxyFactory.createProxy(jedis);
proxy.set("key", "value"); // 走代理
坑点:
- JDK 动态代理只能代理接口,不能代理具体类
- CGLIB 不能代理 final 类和 final 方法
- 代理对象和原对象是两个对象,框架内部如果用原对象,代理不生效
方案选择决策树
你想做什么?
├── 只加日志/监控/统计等横切逻辑 → 方式三:Spring AOP(推荐)
├── 改方法逻辑,类和方法都不是final → 方式一:继承重写(最简单)
├── 类是final,或只想增强部分方法 → 方式二:装饰器模式
├── 目标有接口 → 方式六:动态代理
├── 类是final + 方法是final,必须彻底替换 → 方式四:同包同类名覆盖(慎用)
└── 运行时动态修改,无法通过其他方式 → 方式五:Java Agent + ByteBuddy
面试口述标准答案
重写 Redis 依赖包的方法,按场景有六种方式。最常用的是继承重写,如果类不是 final、方法不是 final,直接继承后 @Override 就行。如果类是 final 或者只想增强部分方法,用装饰器模式,把原对象组合进来,接口方法逐个委托。如果只是加日志、监控这类横切逻辑,用 Spring AOP 拦截最方便,无侵入。如果目标有接口,可以用 JDK 动态代理;没有接口用 CGLIB。极端情况下类和方法都是 final,只能用同包同类名覆盖,但这个是黑科技,维护成本高,不推荐。终极方案是 Java Agent 加 ByteBuddy 做字节码增强,一般只有 APM 工具才会用。实际项目里 90% 的场景用继承或装饰器就能解决。