专项攻克——重写 Redis 依赖包方法的 6 种实现方式

文章目录

    • 方式一:继承重写(最简单,优先考虑)
      • [示例:重写 Jedis 的 `set` 方法](#示例:重写 Jedis 的 set 方法)
      • [示例:重写 Spring Data Redis 的 RedisTemplate](#示例:重写 Spring Data Redis 的 RedisTemplate)
    • 方式二:装饰器模式(组合,比继承更灵活)
      • [示例:包装 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% 的场景用继承或装饰器就能解决。

相关推荐
跨境数据猎手1 小时前
京东开放平台商品详情接口(jd.item_get):签名、缓存与批量采集
java·spring·缓存
IvorySQL1 小时前
基于 PostgreSQL 的非线性回归原理与 AI 数据库融合实践
数据库·人工智能·postgresql
spencer_tseng1 小时前
kylin redis-6.2.14.tar.gz
linux·redis·kylin·edis-6.2.14
xbgRS1 小时前
Redis基本概念及应用
redis·缓存
赵渝强老师1 小时前
【赵渝强老师】高斯数据库(openGauss)的数据库对象
数据库·postgresql·opengauss·国产数据库·高斯数据库
DLYSB_1 小时前
交换机不会 HTTP 怎么办:用 SNMP Get / Trap 把指标和中断打到博灵声光 TTS
数据库·报警灯
wdfk_prog1 小时前
canopennode-rtt推荐,不只可以做从站,也可以承担主站角色
c语言·开发语言·数据库·学习·算法·深度优先
几层山下1 小时前
WPS的word文档转pdf java实现不乱格式生成-已解决
java·pdf·word·wps
昌原的儿子LEO2 小时前
Linux 网络编程:select 与 epoll IO 多路复用详解
linux·网络·数据库