面試時問到用沒用過 AOP,很多回答都是用 AOP 做過日誌統一處理。[what],給人感覺就是沒做過啊
今天介紹一個用註解封裝 redis 緩存的 AOP 實戰
用 redis 加速資料庫訪問,一般會寫出如下代碼
@Servicepublic class UserServiceImpl implements UserService { private final UserMapper userMapper; private final RedisClient redisClient; @Autowired public UserServiceImpl(UserMapper userMapper, RedisClient redisClient) { this.userMapper = userMapper; this.redisClient = redisClient; } @Override public User get(Long id) { String key = String.format("USER:%d", id); String value = redisClient.get(key); if (StringUtils.isNotEmpty(value)) { return JSON.parseObject(value, User.class); } User user = userMapper.get(id); if (user != null) { redisClient.set(key, JSON.toJSONString(user)); } return user; }}
其實現邏輯如下
實際上用 redis 緩存加速資料庫查詢基本都是這樣的套路,如果在每個 service 都要這樣寫一遍,太繁瑣了,用註解來封裝一下吧
通過 Spring 的 AOP 技術,攔截從資料庫查詢的方法,在從資料庫獲取結果之前,先從 redis 獲取,如果 redis 命中,則直接返回;否則就繼續執行從資料庫獲取的方法,將返回值緩存到 reids 並返回
實際上不限於從資料庫獲取結果,如果是從遠程服務獲取值,也可以採用同樣的思路
很顯然用註解是個不錯的主義,定義如下注解
/** * 這個標註用來為redis的<b>通用化</b>存取設定參數 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Redis { /** 非null值 默認過期時間 **/ int DEFAULT_TTL = 4 * 60 * 60 + 10 * 60; /** null值 默認過期時間 **/ int NULL_TTL = 5 * 60; /** redsi key,見 {@link RedisKeys} **/ String value(); /** * <pre> * 指示方法的哪些參數用來構造key,及其順序(編號由0開始) * * 示例 * keyArgs = {1,0,2},表示用方法的第二,第一,第三個參數,按順序來構造key * * 默認值的意思是方法的前 n 個參數來構造key,n 最大為10 * 這樣如果構造 key 的參數不多於 10 個且順序也和方法參數一致,則可以用默認值 * </pre> */ int[] keyArgs() default { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; /** 執行何種操作,默認是先訪問 redis **/ RedisAction action() default RedisAction.REDIS_FIRST; /** 過期時間,默認250分鐘 **/ int ttl() default DEFAULT_TTL; /** 是否以同步的方式操作redis,默認是false **/ boolean sync() default false; /** 是否要緩存 null 值,默認為true **/ boolean cacheNull() default true; /** 如果要緩存null值,過期時間是多少,默認5分鐘 **/ int nullTtl() default NULL_TTL;}public enum RedisAction { // 優先從 redis 獲取 REDIS_FIRST, // 穿透 redis 到 db 獲取 STAB_REDIS;}
這個註解用在需要攔截的方法上,還附帶了一些元信息
接下來是在目標方法上使用註解
@Servicepublic class UserServiceImpl2 implements UserService { private final UserMapper userMapper; @Autowired public UserServiceImpl2(UserMapper userMapper) { this.userMapper = userMapper; } @Redis("USER:%d") @Override public User get(Long id) { return userMapper.get(id); }}
現在代碼就很簡潔明了
@Aspect@Componentpublic class RedisInterceptor { private static final Logger LOG = LoggerFactory.getLogger(RedisInterceptor.class); @Resource private RedisClient redisClient; @Around("@annotation(redis)") public Object doAround(ProceedingJoinPoint pjp, Redis redis) throws Throwable { MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); // 是否穿透 redis boolean stab = redis.action() == RedisAction.STAB_REDIS; Object[] keyArgs = getKeyArgs(pjp.getArgs(), redis.keyArgs()); String key = keyArgs == null ? redis.value() : String.format(redis.value(), keyArgs); Class<?> returnType = method.getReturnType(); Object result = stab ? null : get(key, returnType); if (result == null) { result = pjp.proceed(); if (result != null) { setex(key, redis.ttl(), result, redis.sync()); } else if (redis.cacheNull()) { setex(key, redis.nullTtl(), result, redis.sync()); } } return result; } /** 獲取構造 redis 的 key 的參數數組 */ private Object[] getKeyArgs(Object[] args, int[] keyArgs) { Object[] redisKeyArgs; int len = keyArgs.length; if (len == 0) { return null; } else { len = min(len, args.length); redisKeyArgs = new Object[len]; int i = 0; for (int n : keyArgs) { redisKeyArgs[i++] = args[n]; if (i >= len) { break; } } return redisKeyArgs; } } private int min(int i, int j) { return i > j ? j : i; } private <T> void setex(final String key, final int ttl, final T data, boolean sync) { try { redisClient.setex(key, ttl, data, sync); } catch (Exception e) { LOG.error("redis set error:{}", e.getMessage(), e); } } private <T> T get(String key, Class<T> clazz) { try { return redisClient.get(key, clazz); } catch (Exception e) { LOG.error("redis get error:{}", e.getMessage(), e); return null; } }}
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 開啟註解式 AOP --> <aop:aspectj-autoproxy proxy-target-class="false" /></beans>
這樣就 ok 了,在你想要使用 redis 緩存加速的方法上加上 redis 註解吧
通常情況下,都是優先從 redis 裡查詢結果。但也有時候需要穿透 redis,到 db 裡去獲取結果的。
@Redis 註解也支持這種操作,只需要設置 action 屬性為 STAB_REDIS 即可
但是,同一個方法,action 要麼是 STAB_REDIS,要麼是 REDIS_FIRST,攔截器只能實現其中一種操作,如何才能讓攔截器攔截同一個方法時,實現不同的 redis 操作呢?
有以下幾個辦法
經過考慮,最終採用了第2種辦法
在 Spring 配置文件裡開啟註解式 AOP,使用如下配置
<aop:aspectj-autoproxy proxy-target-class="false" />
自定義註解,如下 2 個元註解必須
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)
針對自定義的註解編寫攔截器代碼
攔截器要用到反射,泛型啥的,一切皆有可能
攔截器配置,注意如下幾個註解的使用
@Aspect@Component@Around("@annotation(******)")