声明:
本篇文章根据个人项目经验,进行的项目总结,仅代表个人意见,给大家提供个参考,如有不当之处,敬请提出,共同讨论,共同进步。
本篇文章的源码已上传百度网盘。
链接: https://pan.baidu.com/s/1lS6Ff6gZFk6mirEagP-rcQ
提取码: ric4
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。
Spring Boot应用中,数据库中有许多基础数据长时间是不会变化的。
当Spring Boot应用的并发变高时候,总是从数据库读取这些基础数据,会增加数据库的访问压力。
因此Spring Boot中定义了@Cacheable、@CachePut、@CacheEvict、@Caching用于缓存,有很多博客做了相关介绍和使用,这里不做介绍。
Spring Boot中提供的缓存方案,根据个人项目经验,看到的一些弊端:
1、@Cacheable定义在方法上,标记该方法的返回结果需要缓存。
a、如果需要缓存父类中某个方法的返回结果,子类中需要复写父类的方法,并加上@Cacheable。
b、对于多个方法、名字类似的方法,如果需要缓存,就要多次@Cacheable。
2、@CacheEvict定义在方法上,用于标记清除某个缓存。
a、对于多个方法、名字类似的方法,需要清除缓存时候,就要多次@CacheEvict。
b、对于有关联的功能(比如用户与部门),就要多个地方、多次的使用@CacheEvict。
我们软件开发的人也是很懒的,如果能够集中配置缓存,我们就不用在多个地方修改缓存注解了。
针对我看到的这些问题,使用spring的面向切面,自己设计了简单的缓存方案:
1、在类的定义上添加缓存注解。
2、缓存注解中
a、定义该类的缓存关键字key
b、受关联的缓存关键字key (多个)
c、调用哪些方法、名字类似方法时候,要清除缓存
d、调用哪些方法、名字类似方法时候,这些方法的返回结果需要缓存。
3、定义切面类,切入到这些方法中,根据缓存注解,实现相关逻辑。如果子类调用父类的方法,也能进行切面的相关缓存逻辑。
4、使用redis、memcached等技术,实现数据的缓存。
5、由于这里只是演示,使用final static Map缓存数据,方法直接new数据,不读取数据库。
在idea中,创建spring boot项目,项目中含有测试用例环境,很多博客有相关介绍,这里不再说明。
我也是个懒人,哈哈哈哈。
项目源码目录如下图:
自定义缓存注解@Cache,源码如下
package com.example.demo.cache.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 缓存注解
*
* @author zhanglinlin
* @Description:
* @date 2020年10月26日 17:04
*/
@Target({ElementType.TYPE})
@Retention(RUNTIME)
public @interface Cache {
/**
* 缓存key
*
* @return
*/
String key() default "";
/**
* 关联清除的key
*
* @return
*/
String[] cleanKeys() default {};
/**
* 方法名称含有指定字符串,则缓存返回结果
*
* @return
*/
String[] cacheMethods() default {"list"};
/**
* 方法名称含有指定字符串,则清除缓存
*
* @return
*/
String[] cleanMethods() default {"insert", "save", "update", "delete", "edit", "change"};
}
切面类CacheAspect,实现缓存的相关逻辑,源码如下
在实际使用中,发现不能对父类方法进行切面。
解决方案:定义多个切入点,将父类包含在切入点,
@Around("cacheAspect()") 改为 @Around("cacheAspect() || cacheAspect2() ")
package com.example.demo.cache.aop;
import com.example.demo.cache.annotations.Cache;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 缓存切面
*
* @author zhanglinlin
* @Description:
* @date 2020年10月26日 17:21
*/
@Aspect
@Component
public class CacheAspect {
/**
* 这里使用Map作为缓存,实际运用中可以使用redis、memcached等缓存
*/
private final static Map cacheMap = new HashMap<>();
/**
* 定义切入点,包及其子包下的所有方法
*/
@Pointcut("execution(public * com.example.demo.service..*.*(..)))")
public void cacheAspect() {
}
/**
* 拦截切入点,做缓存相关处理
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("cacheAspect()")
public Object doAspect(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
Cache cache = target.getClass().getAnnotation(Cache.class);
if (cache != null) {
String key = cache.key();
if (key != null && key.length() != 0) {
this.cleanKey(joinPoint);
return this.cache(joinPoint);
}
}
return joinPoint.proceed();
}
/**
* 根据调用方法,检查是否需要清除相关缓存
*
* @param joinPoint
* @throws Exception
*/
private void cleanKey(ProceedingJoinPoint joinPoint) {
Cache cache = joinPoint.getTarget().getClass().getAnnotation(Cache.class);
if (cache != null) {
String method = joinPoint.getSignature().getName();
String[] methods = cache.cleanMethods();
boolean methodFlag = this.methodFlag(method, methods);
if (methodFlag) {
this.cleanKey(cache.key());
String[] cleanKeys = cache.cleanKeys();
if (cleanKeys != null && cleanKeys.length != 0) {
for (String key : cleanKeys) {
this.cleanKey(key);
}
}
}
}
}
/**
* 根据调用方法,检查是否需要缓存
*
* @param joinPoint
* @throws Exception
*/
private Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
Cache cache = joinPoint.getTarget().getClass().getAnnotation(Cache.class);
if (cache != null) {
String key = cache.key();
if (key != null && key.length() != 0) {
String method = joinPoint.getSignature().getName();
String[] methods = cache.cacheMethods();
boolean methodFlag = this.methodFlag(method, methods);
if (methodFlag) {
String cacheKey = this.cacheKey(cache.key(), joinPoint);
Object object = CacheAspect.cacheMap.get(cacheKey);
if (object == null) {
object = joinPoint.proceed();
if (object != null) {
System.out.println("添加缓存key:" + cacheKey);
CacheAspect.cacheMap.put(cacheKey, object);
}
} else {
System.out.println("获取缓存key:" + cacheKey);
}
return object;
}
}
}
return joinPoint.proceed();
}
/**
* 清除key缓存
*
* @param key
* @throws Exception
*/
private void cleanKey(String key) {
System.out.println("清除前缀key:" + key);
if (key != null && key.length() != 0) {
Set cacheKeys = CacheAspect.cacheMap.keySet();
for (String cacheKey : cacheKeys) {
if (cacheKey.startsWith(key)) {
System.out.println("清除缓存key:" + cacheKey);
CacheAspect.cacheMap.remove(cacheKey);
}
}
}
}
/**
* 获取缓存key
*
* @param key
* @param joinPoint
* @return
*/
private String cacheKey(String key, ProceedingJoinPoint joinPoint) {
StringBuffer cacheKey = new StringBuffer();
cacheKey.append(key);
cacheKey.append(":");
cacheKey.append(joinPoint.getTarget().getClass().getName());
cacheKey.append(":");
cacheKey.append(joinPoint.getSignature().getName());
Object[] args = joinPoint.getArgs();
if (args != null) {
for (Object arg : args) {
if (arg != null) {
cacheKey.append(":");
cacheKey.append(arg.toString());
}
}
}
return cacheKey.toString();
}
/**
* 验证方法名称,是否在给定数组中
*
* @param method
* @param methods
* @return
*/
private boolean methodFlag(String method, String[] methods) {
boolean methodFlag = false;
if (method != null && method.length() != 0 && methods != null && methods.length != 0) {
method = method.toUpperCase();
for (String m : methods) {
if (method.contains(m.toUpperCase())) {
methodFlag = true;
break;
}
}
}
return methodFlag;
}
}
接口BaseService、TestService,实现类BaseServiceImpl、TestServiceImpl,用于演示调用子类的方法,在父类中有实现。
BaseService源码如下
package com.example.demo.service;
import java.util.List;
/**
* @author zhanglinlin
* @Description:
* @date 2020年10月26日 18:30
*/
public interface BaseService {
public void insert(String s);
public List list2(String s);
}
BaseServiceImpl源码如下:
package com.example.demo.service.impl;
import com.example.demo.service.BaseService;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author zhanglinlin
* @Description:
* @date 2020年10月26日 18:31
*/
public class BaseServiceImpl implements BaseService {
public void insert(String s) {
}
public List list2(String s) {
List list = new ArrayList<>();
list.add(new Date().toString());
return list;
}
}
TestService源码如下
package com.example.demo.service;
import java.util.List;
/**
* @author zhanglinlin
* @Description:
* @date 2020年10月26日 17:15
*/
public interface TestService extends BaseService {
}
TestServiceImpl的类定义上,添加自定义缓存注解,源码如下
package com.example.demo.service.impl;
import com.example.demo.cache.annotations.Cache;
import com.example.demo.service.TestService;
import org.springframework.stereotype.Service;
/**
* @author zhanglinlin
* @Description:
* @date 2020年10月26日 17:15
*/
@Service
@Cache(key = "testKey", cleanKeys = {"testCleanKey"})
public class TestServiceImpl extends BaseServiceImpl implements TestService {
}
测试用例DemoApplicationTests的源码如下
package com.example.demo;
import com.example.demo.service.TestService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class DemoApplicationTests {
@Autowired
TestService testService;
@Test
void test() throws InterruptedException {
System.out.println("第一次insert");
testService.insert("ss");
System.out.println();
Thread.sleep(2 * 1000);
System.out.println("第一次list2");
List list = testService.list2("ss");
System.out.println(list);
System.out.println();
Thread.sleep(2 * 1000);
System.out.println("第二次list2");
list = testService.list2("ss");
System.out.println(list);
System.out.println();
Thread.sleep(2 * 1000);
System.out.println("第二次insert");
testService.insert("ss");
System.out.println();
Thread.sleep(2 * 1000);
System.out.println("第三次list2");
list = testService.list2("ss");
System.out.println(list);
System.out.println();
Thread.sleep(2 * 1000);
System.out.println("第四次list2");
list = testService.list2("ss");
System.out.println(list);
System.out.println();
Thread.sleep(2 * 1000);
}
}
运行测试用例,结果如下图: