- 根據請求參數生成Key,後面咱們會對生成Key的規則,進一步說明;
- 根據Key去緩存服務器中取數據,若是取到數據,則返回數據,若是沒有取到數據,則執行service中的方法調用dao從DB中獲取數據,同時成功後將數據放到緩存中。
- 刪除、新增、修改會觸發更新緩存的攔截類對緩存服務器進行更新。
1.首先貼上核心註解類redis
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public @interface RedisLogService { enum CACHE_OPERATION { FIND, // 查詢緩存操做 UPDATE, // 須要執行修改緩存的操做 INSERT; // 須要執行新增緩存的操做 } /** 存儲的分組 */ String[] group(); /** 當前緩存操做類型 */ CACHE_OPERATION cacheOperation() default CACHE_OPERATION.FIND; /** 存儲的Key 默認加入類名跟方法名 */ String key() default ""; /** 是否使用緩存 */ boolean use() default true; /** 超時時間 */ int expire() default 0; enum LOG_OPERATION { ON, // 開啓日誌記錄 OFF, // 關閉日誌記錄 } /** 當前緩存操做類型 */ LOG_OPERATION logOperation() default LOG_OPERATION.ON; /** 操做名稱 */ String name() default ""; /** 操做參數 */ String param() default ""; /** 日誌參數 操做人操做IP,操做IP歸屬地 */ String logParam() default "";
2.使用註解案例。spring
@RedisLogService(group = {
"group.news" }, key = "#record", name = "網站維護-公司新聞管理-分頁查詢公司新聞", param = "#record", logParam = "#map")
解釋下上面註解:根據業務的須要,將緩存key進行分組,第一個group參數便是分組,用來標識某個模塊,例如新聞模塊統一是group.news;第二個key是根據參數拼接成的key,第三個name只是一個名稱而已,沒什麼太大的做用,主要是用於給其它開發人員理解, 第四個param則是操做參數,這個很重要,到時候會用它來拼接key,第五個logParam是日誌。數據庫
3.貼上具體攔截類緩存
@Aspect @Order(value = 1) @Component("redisLogServiceInterceptor") public class RedisLogServiceInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(RedisLogServiceInterceptor.class); @Autowired private UserLogRecordService userLogRecordService; @Autowired private RedisTemplate<String, Object> redisTemplate; /** * * * @Title: execute * @Description: 切入點業務邏輯 * @param proceedingJoinPoint * @return */ @Around("@annotation(RedisLogService)") public Object execute(ProceedingJoinPoint proceedingJoinPoint) throws ServiceException { Object result = null; try { Method method = getMethod(proceedingJoinPoint); // 獲取註解對象 RedisLogService redisLogService = method.getAnnotation(RedisLogService.class); // 判斷是否使用緩存 boolean useRedis = redisLogService.use(); if (useRedis) { // 使用redis ValueOperations<String, Object> operations = redisTemplate.opsForValue(); // 判斷當前操做 switch (redisLogService.cacheOperation()) { case FIND: result = executeDefault(redisLogService, operations, proceedingJoinPoint, method); break; case UPDATE: result = executeUpdate(redisLogService, operations, proceedingJoinPoint); break; case INSERT: result = executeInsert(redisLogService, operations, proceedingJoinPoint); break; default: result = proceedingJoinPoint.proceed(); break; } } else { result = proceedingJoinPoint.proceed(); } } catch (ServiceException e) { throw e; } catch (Throwable e) { throw new ServiceException(new Result<Object>("500", e.getMessage()), e); } return result; }
/**
*
* @Title: getMethod
* @Description: 獲取被攔截方法對象
* @param joinPoint
* @return
*/
protected Method getMethod(JoinPoint joinPoint) throws Exception {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
return method;
}
上面的代碼使用了@Around環繞切面這個註解,爲何不用@Befor或者@After呢?服務器
因爲@Befor是在方法執行開始前才進行切面,而@After是方法結束後進行切面。 根據業務場景的須要,@Around 能夠在所攔截方法的先後執行一段邏輯,例如在查詢前先去Redis查數據,發現沒有數據再回到service層去執行查db,查完了以後須要把數據從新放到Redis,此時其餘線程的請求就能夠直接從Redis得到數據,減小頻繁對數據庫的操做。app
4.下面貼上查詢的具體實現方法ide
/** * * @Title: executeDefault * @Description: 默認操做的執行 * @param redisLogService * @param result * @param operations * @param proceedingJoinPoint * @param method * @throws Throwable */ @SuppressWarnings("unchecked") private Object executeDefault(RedisLogService redisLogService, ValueOperations<String, Object> operations, ProceedingJoinPoint proceedingJoinPoint, Method method) throws Throwable { Object result = null; Object[] args = proceedingJoinPoint.getArgs(); // 獲取被攔截方法參數名列表(使用Spring支持類庫) LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paraNameArr = u.getParameterNames(method); // 獲取key的後綴的參數名 String key = redisLogService.key(); if (StringUtils.isNotBlank(key)) { // 使用SPEL進行key的解析 ExpressionParser parser = new SpelExpressionParser(); // SPEL上下文 StandardEvaluationContext context = new StandardEvaluationContext(); // 把方法參數放入SPEL上下文中 for (int i = 0; i < paraNameArr.length; i++) { context.setVariable(paraNameArr[i], args[i]); } Object object = parser.parseExpression(key).getValue(context); if (null != object) { if (object instanceof Map<?, ?>) { key = GzdtlStringUtil.transMapToString((Map<String, Object>) object); } else if (object instanceof Collection<?>) { Collection<Object> collection = (Collection<Object>) object; StringBuffer stringBuffer = new StringBuffer(); for (Object o : collection) { stringBuffer.append(o.toString()); } key = stringBuffer.toString(); } else { key = object.toString(); } } } String className = proceedingJoinPoint.getTarget().getClass().getName(); if (className.indexOf(".") >= 0) { className = className.substring(className.lastIndexOf(".") + 1, className.length()); } String methodName = method.getName(); String[] group = redisLogService.group(); if (null != group && group.length > 0) { if (StringUtils.isNotBlank(key)) { key = group[0] + ":" + className + ":" + methodName + ":" + key; } else { key = group[0] + ":" + className + ":" + methodName; } } else { if (StringUtils.isNotBlank(key)) { key = "group" + ":" + className + ":" + methodName + ":" + key; } else { key = "group" + ":" + className + ":" + methodName; } } result = operations.get(key); // 若是緩存沒有數據則更新緩存 if (result == null) { result = proceedingJoinPoint.proceed(); int expire = redisLogService.expire(); // 更新緩存 if (expire > 0) { operations.set(key, result, expire, TimeUnit.SECONDS); } else { operations.set(key, result); } } return result; }
proceedingJoinPoint.getArgs() 做用,瞭解過aop 以及反射相關的都知道這是從方法內取出傳入參數,例如傳入的是 (String user,String age), 經過這個方法能夠分別獲得user和age的值。網站
例如以下方法:lua
public Result<PageInfo<WebInfoBase>> findPageByParam(WebInfoFindParam record, Map<String, String> map)spa
String[] paraNameArr = u.getParameterNames(method);
從paraNameArr獲取參數的別名分別是record和map, 注意在上面的代碼裏裏我用到了自定義redis註解: key = "#record" , 接着使用SPEL表達式進行解析取出record的值,爲何要取出record的值是由於須要拼接key存放到redis,因此record做爲key拼接的條件之一是必須的。
假設用戶id = 1,分頁查詢了訂單信息,這時候 record 參數爲:pageSize:10,pageNum:2,id:1。key的最終格式 : grop+namespace+record(這樣基本是惟一不會重複)。