本博文主要讲解如何更加优雅的做缓存,缓存服务器与基础客户端连接、缓存代码在博主的博文《MemCached的安装和JAVA客户端连接Memcached示例代码》有介绍。优雅缓存的代码都是基于那篇博文基础上完成的。主要业务代码有如下:
/**
* 缓存客户端接口类
*/
public interface CacheClient {
.......
}
/**
* memcached缓存客户端持有者,交于Spring实例化
*/
@Component
public class MemcachedClientHolder implements CacheClient{
//实现缓存接口
}
有了如上的代码,就可以在业务代码中使用缓存功能了。
但是我们可以想想,是否可以更加简单优雅的做缓存呢,是否可以让业务方更加简单的编写缓存使用的业务代码? 答案当然是可以的,请看如下分析。
业务场景描述
例如网易新闻客户端,刚开始打开体育版块的时候,每个人看到的内容都是一样(暂不考虑千人千面推荐的情况),这个时候就非常适用缓存,但是业务代码中使用缓存就显得有些冗余,其实完全可以在Web应用RestApi接口级别做缓存。大概思路如下:
1:即自定义编写一个注解,用于收集缓存的时间、key前缀、排除的参数等信息。
2:在业务接口方法头中,设置好注解。这样就可以不用编写任何使用缓存的业务代码了。
各种购物网站首页的 banner、icon都是一样的道理,只是缓存的时间不同而已。如下详细讲解如上两点如何去做。
扫描二维码关注公众号,回复:
3469767 查看本文章
自定义注解
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大小。