本博文主要講解如何更加優雅的做緩存,緩存服務器與基礎客戶端連接、緩存代碼在博主的博文《MemCached的安裝和JAVA客戶端連接Memcached示例代碼》有介紹。優雅緩存的代碼都是基於那篇博文基礎上完成的。主要業務代碼有如下:
/**
* 緩存客戶端接口類
*/
public interface CacheClient {
.......
}
/**
* memcached緩存客戶端持有者,交於Spring實例化
*/
@Component
public class MemcachedClientHolder implements CacheClient{
//實現緩存接口
}
有了如上的代碼,就可以在業務代碼中使用緩存功能了。
但是我們可以想想,是否可以更加簡單優雅的做緩存呢,是否可以讓業務方更加簡單的編寫緩存使用的業務代碼? 答案當然是可以的,請看如下分析。
業務場景描述
例如網易新聞客戶端,剛開始打開體育版塊的時候,每個人看到的內容都是一樣(暫不考慮千人千面推薦的情況),這個時候就非常適用緩存,但是業務代碼中使用緩存就顯得有些冗餘,其實完全可以在Web應用RestApi接口級別做緩存。大概思路如下:
1:即自定義編寫一個註解,用於收集緩存的時間、key前綴、排除的參數等信息。
2:在業務接口方法頭中,設置好註解。這樣就可以不用編寫任何使用緩存的業務代碼了。
各種購物網站首頁的 banner、icon都是一樣的道理,只是緩存的時間不同而已。如下詳細講解如上兩點如何去做。
自定義註解
package com.cloud.practice.demo.cache.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 對Controller接口進行緩存
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cachable {
/**
* Expire time in seconds. default 取配置文件中的expire時間
* @return expire time 時間爲秒,默認60秒
*/
int expire() default 60;
/**
* 緩存Key的前綴,以防不同Controller相同接口相同參數導致Key/Value混亂
* @return 默認tt
*/
String prefix() default "tt";
/**
* 排除掉那些參數
* @return 要排除的參數次序,從0開始
*/
int[] excludeArgs() default {};
}
在自定義註解上做AOP切面編程
package com.cloud.practice.demo.cache.aop;
/**
* 對Controller的接口方法進行AOP,實現HTTP 接口的cache。 請求的處理流程:
* 1.從cache中找,看是否hit,如果hit,直接返回結果
* 2.如果沒有命中,調用服務,調用完成後寫入cacle中
*/
@Aspect
@Component
public class ControllerCacheAop {
@Value("${cache.servers.memcached.expire:60}")
private int memcachedServiceExpire ;
@Autowired
CacheClient memecacheClientHolder;
@Autowired
private HttpSession session;
//定義一個切面在Controller上面
@Pointcut("within(@org.springframework.stereotype.Controller *)")
public void controllerPointcut() {
}
@Pointcut("within(@ org.springframework.web.bind.annotation.RestController *)")
public void restControllerPointcut() {
}
//定義一個切面在@Cachable註解上面
@Pointcut("@annotation(com.cloud.tasktrack.core.aop.Cachable)")
public void cacheAnnotationPointCut() {
}
/**
* 先從cache中取數據,找不到則調用controller原來的實現,後在寫入緩存
*/
@Around("(controllerPointcut() || restControllerPointcut()) && cacheAnnotationPointCut() ")
@Order(1)
public Object cacheAop(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//返回類型
final Class<?> returnType = signature.getMethod().getReturnType();
//獲得方法名、類名
String intefaceName=signature.getMethod().getName();
String className=joinPoint.getTarget().getClass().toString();
if(className.contains(".")){
String[] _classNameTmp=className.split("\\.");
className=_classNameTmp[_classNameTmp.length-1];
}
long start= System.currentTimeMillis();
//獲得@Cachable註解的相關信息
Cachable cacheAnnotation= signature.getMethod().getAnnotation(Cachable.class);
final String cacheKey = this.getCacheKey(joinPoint, cacheAnnotation);
//從緩存中獲取數據
try {
Object _obj = this.memecacheClientHolder.get(cacheKey, returnType);
if (_obj != null) {
long usedTimeMs= System.currentTimeMillis()-start;
User operationUser=(User)session.getAttribute("user");
String operationUserWebId=null,operationUserId=null;
if(null!=operationUser){
operationUserWebId=String.valueOf(operationUser.getWebId());
operationUserId=String.valueOf(operationUser.getId());
}
log.debug("============ Cache log: "+"webId "+ operationUserWebId + " userId "+ operationUserId);
log.debug("============ Cache hit for key [" + cacheKey + "]" + " in " + usedTimeMs + "ms. " + " value is [" + _obj +"]");
return _obj;
}
}catch (Exception e){
log.warn("============ Cache exception for key[" + cacheKey + "]"+" Exception is : "+e.getMessage());
}
//緩存沒有命中: 調用原來的請求,然後將結果緩存到cache中。
Object intefaceCallResult;
try {
intefaceCallResult = joinPoint.proceed();
if (intefaceCallResult != null) {
//一級緩存失效的時間,如果指定了,則使用指定的值。如果沒有指定,則使用配置的值
int expire = cacheAnnotation.expire();
if (expire <= 0) {
expire = this.memcachedServiceExpire;
}
this.memecacheClientHolder.set(cacheKey, expire, intefaceCallResult);
log.debug("============ Set cache value for key[" + cacheKey + "]" + " value in[" + intefaceCallResult +"]");
}
} catch (Exception e) {
intefaceCallResult=new ApiResponse.ApiResponseBuilder().code(ApiResponse.SERVICE_ERROR_CODE).message("服務調用異常"+e.getMessage()).build() ;
}
return intefaceCallResult;
}
/**
* 根據攔截的方法的參數,生成cache的key. prefix:METHOD_NAME:METHOD_PARAM
* @param joinPoint 攔截點
* @param cacheExpire
* @return key
*/
private String getCacheKey(ProceedingJoinPoint joinPoint, Cachable cacheExpire) {
//獲得要排除的方法參數
int[] excludeParams = cacheExpire.excludeArgs();
//獲得要換成Key的前綴
String prefix=cacheExpire.prefix();
String cacheKeyPrefix = prefix+":" + joinPoint.getSignature().getName();
//把參數連接起來
List<String> methodParams = new LinkedList<>();
Object arguments[] = joinPoint.getArgs();
if (ArrayUtils.isNotEmpty(arguments)) {
for (int i = 0; i < arguments.length; i++) {
//排除掉某些參數
if (ArrayUtils.contains(excludeParams, i)) {
continue;
}
Object arg = arguments[i];
//fix key contain ' ' || b == '\n' || b == '\r' || b == 0
String param = (arg == null ? "" : String.valueOf(arg));
methodParams.add(param);
}
}
return cacheKeyPrefix + ":" + String.join("-", methodParams);
}
}
業務代碼層面如何使用此種優雅緩存
package com.cloud.practice.demo.cache;
@Controller
@RequestMapping("/post")
public class CacheUsedDemoController {
@Autowired
private PostService postService;
/**
* 根據站點ID,站點ID(可有可無),查詢所有崗位信息,需要附帶返回站點下面的用戶信息
* @param request
* @return
*/
@RequestMapping("/getPostUser")
@Cachable(expire = 120, prefix = "post", excludeArgs = 2) //這種就使用了此種緩存
@Loggerable(interfaceDesc = "查詢崗位與崗位下用戶信息")
@ResponseBody
public ApiResponse getPostUser(HttpServletRequest request) {
ApiResponse apiResponse;
int webId = CloudRequestPropertyUtils.getWebIdFromRequest(request);
if (webId<1) {
apiResponse = new ApiResponse.ApiResponseBuilder().code(ApiResponse.DEFAULT_CODE).message("站點ID不能爲空,請重新輸入").build();
} else {
List<Post> postUsers = postService.getPostUser(webId);
apiResponse = new ApiResponse.ApiResponseBuilder().code(ApiResponse.DEFAULT_CODE).message(ApiResponse.DEFAULT_MSG)
.data(postUsers).dataCount(postUsers.size()).build();
}
return apiResponse;
}
}
是不是感覺很棒很方便,大家不妨自己試試,的確很方便。此種思想也可以應用到日誌記錄等功能當中。
業務場景描述
表示java虛擬機堆區內存初始內存分配的大小,通常爲操作系統可用內存的1/64大小即可,但仍需按照實際情況進行分配。表示java虛擬機堆區內存可被分配的最大上限,通常爲操作系統可用內存的1/4大小。