文章目录

  • 依赖和工具介绍
  • 项目代码
    • spring上下文工具类:
    • 自定义缓存注解
    • 缓存生成键工具类
    • 自定义缓存拦截器
    • 缓存处理器
    • 缓存结果和缓存信息实体封装
    • 开启声明式注解
    • controller层使用缓存
  • 总结

依赖和工具介绍

 <dependency><groupId> org.aspectj</groupId ><artifactId> aspectjweaver</artifactId ><version> 1.8.7</version ></dependency><dependency><groupId>net.oschina.j2cache</groupId><artifactId>j2cache-core</artifactId><version>2.8.0-release</version></dependency>

配置:


j2cache:cache-clean-mode: passiveallow-null-values: trueredis-client: lettuce #指定redis客户端使用lettuce,也可以使用Jedisl2-cache-open: true #开启二级缓存broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy#  broadcast: jgroupsL1: #指定一级缓存提供者为caffeineprovider_class: caffeineL2: #指定二级缓存提供者为redisprovider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProviderconfig_section: lettucesync_ttl_to_redis: truedefault_cache_null_object: falseserialization: fst  #序列化方式:fst、kyro、Java
caffeine:properties: /caffeine.properties   # 这个配置文件需要放在项目中
lettuce:mode: singlenamespace:storage: genericchannel: j2cachescheme: redishosts: ${pinda.redis.ip}:${pinda.redis.port}password: ${pinda.redis.password}database: ${pinda.redis.database}sentinelMasterId:maxTotal: 100maxIdle: 10minIdle: 10timeout: 10000

caffeine.properties

default=2000, 2h
rx=2000, 2h
addressBook=2000, 2h

j2cache是OSChina目前正在使用的两级缓存框架。

j2cache的两级缓存结构:

L1: 进程内缓存 caffeine/ehcache
L2: 集中式缓存 Redis/Memcached
j2cache其实并不是在重复造轮子,而是作资源整合,即将Ehcache、Caffeine、redis、Spring Cache等进行整合。

由于大量的缓存读取会导致L2的网络成为整个系统的瓶颈,因此L1的目标是降低对L2的读取次数。该缓存框架主要用于集群环境中。单机也可使用,用于避免应用重启导致的ehcache缓存数据丢失。

j2cache从1.3.0版本开始支持JGroups和Redis Pub/Sub两种方式进行缓存事件的通知。

数据读取顺序 -> L1 -> L2 -> DB

关于j2cache的region概念:
J2Cache 的 Region 来源于 Ehcache 的 Region 概念。

一般我们在使用像 Redis、Caffeine、Guava Cache 时都没有 Region 这样的概念,特别是 Redis 是一个大哈希表,更没有这个概念。

在实际的缓存场景中,不同的数据会有不同的 TTL 策略,例如有些缓存数据可以永不失效,而有些缓存我们希望是 30 分钟的有效期,有些是 60 分钟等不同的失效时间策略。在 Redis 我们可以针对不同的 key 设置不同的 TTL 时间。但是一般的 Java 内存缓存框架(如 Ehcache、Caffeine、Guava Cache 等),它没法为每一个 key 设置不同 TTL,因为这样管理起来会非常复杂,而且会检查缓存数据是否失效时性能极差。所以一般内存缓存框架会把一组相同 TTL 策略的缓存数据放在一起进行管理。
像 Caffeine 和 Guava Cache 在存放缓存数据时需要先构建一个 Cache 实例,设定好缓存的时间策略,如下代码所示:

Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
caffeine = caffeine.maximumSize(size).expireAfterWrite(expire, TimeUnit.SECONDS);
Cache<String, Object> theCache = caffeine.build()
这时候你才可以往 theCache 写入缓存数据,而不能再单独针对某一个 key 设定不同的 TTL 时间。

而 Redis 可以让你非常随意的给不同的 key 设置不同的 TTL。

J2Cache 是内存缓存和 Redis 这类集中式缓存的一个桥梁,因此它只能是兼容两者的特性。
J2Cache 默认使用 Caffeine 作为一级缓存,其配置文件位于 caffeine.properties 中。一个基本使用场景如下:

#########################################

# Caffeine configuration
# [name] = size, xxxx[s|m|h|d]

#########################################

default = 1000, 30m
users = 2000, 10m
blogs = 5000, 1h
上面的配置定义了三个缓存 Region ,分别是:

默认缓存,大小是 1000 个对象,TTL 是 30 分钟
users 缓存,大小是 2000 个对象,TTL 是 10 分钟
blogs 缓存,大小是 5000 个对象,TTL 是 1 个小时
例如我们可以用 users 来存放用户对象的缓存,用 blogs 来存放博客对象缓存,两种的 TTL 是不同的。

项目代码

现在上代码,Springbootzhenghej2cache进行缓存:

spring上下文工具类:


/*** Spring上下文工具类*/
@Primary
@Component
public class SpringApplicationContextUtils {private static ApplicationContext springContext;@Autowiredprivate ApplicationContext applicationContext;@PostConstructprivate void init() {springContext = applicationContext;}/*** 获取当前ApplicationContext** @return ApplicationContext*/public static ApplicationContext getApplicationContext() {return springContext;}}

自定义缓存注解

如果项目中很多模块都需要使用缓存功能,这些模块都需要调用j2cache的API来进行缓存操作,这种j2cache提供的原生API使用起来就比较繁琐了,并且操作缓存的代码和我们的业务代码混合到一起,即j2cache的API对我们的业务代码具有侵入性。那么我们如何更加简洁、优雅的使用j2cache提供的缓存功能呢?

答案就是使用声明式缓存。所谓声明式缓存,就是定义缓存注解,在需要使用缓存功能的方法上加入缓存注解即可自动进行缓存操作。

这种使用方式类似于我们以前使用的声明式事务,即在类的方法上加入事务注解就可以实现事务控制。

注意:j2cache原生API和我们实现的声明式缓存可以兼容,即在项目中可以同时使用,互为补充。例如在Controller的方法中需要将多类业务数据载入缓存,此时通过声明式缓存就无法做到(因为声明式缓存只能将方法的返回值载入缓存),这种场景下就需要调用j2cache的原生API来完成。

/*** 缓存注解*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {String region() default "rx";String key() default "";String params() default "";
}
/*** 清理缓存注解*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheEvictor {Cache[] value() default {};
}

自定义了上面注解以后只需要在controller层需要注解的方法上加对应注解即可

缓存生成键工具类

/*** 缓存键生成工具*/
public class CacheKeyBuilder {/*** 生成key** @param key      键* @param params   参数* @param args     参数值* @return* @throws IllegalAccessException 当访问异常时抛出*/public static String generate(String key, String params, Object[] args) throws IllegalAccessException {StringBuilder keyBuilder = new StringBuilder("");if (StringUtils.hasText(key)) {keyBuilder.append(key);}if (StringUtils.hasText(params)) {String paramsResult = ObjectAccessUtils.get(args, params, String.class, "_", "null");keyBuilder.append(":");keyBuilder.append(paramsResult);}return keyBuilder.toString();}
}

自定义缓存拦截器

注意这里的Interceptor是org.aopalliance.intercept包下的
Spring的AOP只能支持到方法级别的切入。换句话说,切入点只能是某个方法。

package com.itheima.j2cache.aop;import com.itheima.j2cache.annotation.Cache;
import com.itheima.j2cache.annotation.CacheEvictor;
import com.itheima.j2cache.aop.processor.AbstractCacheAnnotationProcessor;
import com.itheima.j2cache.aop.processor.CacheEvictorAnnotationProcessor;
import com.itheima.j2cache.aop.processor.CachesAnnotationProcessor;
import com.itheima.j2cache.utils.SpringApplicationContextUtils;
import org.aopalliance.intercept.Interceptor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;/*** 缓存操作拦截器*/@Aspect
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true)//指定使用cglib方式为Controller创建代理对象,代理对象其实是目标对象的子类
@Import(SpringApplicationContextUtils.class)
public class CacheMethodInterceptor implements Interceptor{/*** 拦截方法上使用Cache注解的Controller* @param proceedingJoinPoint* @return* @throws Throwable*/@Around("@annotation(com.itheima.j2cache.annotation.Cache)")//环绕通知//@Around注解,表示这是一个环绕通知。环绕通知是所有通知里功能最为强大的通知,可以实现前置通知、后置通知、异常通知以及返回通知的功能。目标方法进入环绕通知后,通过调用ProceedingJoinPoint对象的proceed方法使目标方法继续执行,开发者可以在此修改目标方法的执行参数、返回值等,并且可以在此处理目标方法的异常。public Object invokeCacheAllMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{//获得方法前面对象MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();//获得当前拦截到的Controller方法对象Method method = signature.getMethod();//获得方法上的Cache注解信息Cache cache = AnnotationUtils.findAnnotation(method, Cache.class);if(cache != null){System.out.println("需要进行设置缓存数据处理...");//创建处理器,具体处理缓存逻辑CachesAnnotationProcessor processor = AbstractCacheAnnotationProcessor.getProcessor(proceedingJoinPoint, cache);return processor.process(proceedingJoinPoint);}//没有获取到Cache注解信息,直接放行return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());}/*** 拦截方法上使用CacheEvictor注解的Controller* @param proceedingJoinPoint* @return* @throws Throwable*/@Around("@annotation(com.itheima.j2cache.annotation.CacheEvictor)")//环绕通知public Object invokeCacheEvictorAllMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();Method method = signature.getMethod();CacheEvictor cacheEvictor = AnnotationUtils.findAnnotation(method, CacheEvictor.class);if(cacheEvictor != null){System.out.println("清理缓存处理...");//创建清理缓存的处理器CacheEvictorAnnotationProcessor processor = AbstractCacheAnnotationProcessor.getProcessor(proceedingJoinPoint, cacheEvictor);return processor.process(proceedingJoinPoint);}return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());}}

在上面的拦截器中使用了 CachesAnnotationProcessor processor = AbstractCacheAnnotationProcessor.getProcessor(proceedingJoinPoint, cache);来对缓存进行处理,需要自定义缓存处理器:

缓存处理器


import com.itheima.j2cache.annotation.Cache;
import com.itheima.j2cache.annotation.CacheEvictor;
import com.itheima.j2cache.model.AnnotationInfo;
import com.itheima.j2cache.utils.CacheKeyBuilder;
import com.itheima.j2cache.utils.SpringApplicationContextUtils;
import net.oschina.j2cache.CacheChannel;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.util.StringUtils;/*** 抽象缓存注解处理器*/
public abstract class AbstractCacheAnnotationProcessor {protected CacheChannel cacheChannel;/*** 初始化公共属性,供子类使用*///注意这里使用应用上下文来获得对应的CacheChannel这个beanpublic AbstractCacheAnnotationProcessor(){ApplicationContext applicationContext = SpringApplicationContextUtils.getApplicationContext();cacheChannel = applicationContext.getBean(CacheChannel.class);}/*** 封装注解信息* @param proceedingJoinPoint* @param cache* @return*/protected AnnotationInfo<Cache> getAnnotationInfo(ProceedingJoinPoint proceedingJoinPoint,Cache cache){AnnotationInfo<Cache> annotationInfo = new AnnotationInfo<>();annotationInfo.setAnnotation(cache);annotationInfo.setRegion(cache.region());try{annotationInfo.setKey(generateKey(proceedingJoinPoint,cache));}catch (Exception e){e.printStackTrace();}return annotationInfo;}/*** 动态解析注解信息,生成key* @param proceedingJoinPoint* @param cache* @return*/protected String generateKey(ProceedingJoinPoint proceedingJoinPoint,Cache cache) throws IllegalAccessException{String key = cache.key();//abif(!StringUtils.hasText(key)){//如果当前key为空串,重新设置当前可以为:目标Controller类名:方法名String className = proceedingJoinPoint.getTarget().getClass().getSimpleName();MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();String methodName = signature.getMethod().getName();key = className + ":" + methodName;}//ab:100key = CacheKeyBuilder.generate(key,cache.params(),proceedingJoinPoint.getArgs());return key;}/*** 抽象方法,处理缓存操作,具体应该由子类具体实现* @param proceedingJoinPoint* @return* @throws Throwable*/public abstract Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable;/*** 获得缓存注解处理器对象* @param proceedingJoinPoint* @param cache* @return*/public static CachesAnnotationProcessor getProcessor(ProceedingJoinPoint proceedingJoinPoint, Cache cache){return new CachesAnnotationProcessor(proceedingJoinPoint,cache);}/*** 获得清理缓存注解处理器对象* @param proceedingJoinPoint* @param cacheEvictor* @return*/public static CacheEvictorAnnotationProcessor getProcessor(ProceedingJoinPoint proceedingJoinPoint, CacheEvictor cacheEvictor){return new CacheEvictorAnnotationProcessor(proceedingJoinPoint,cacheEvictor);}
}

清理缓存注解的处理器:


/*** 清理缓存数据处理器*/
public class CacheEvictorAnnotationProcessor extends AbstractCacheAnnotationProcessor{/*** 封装注解信息集合*/private List<AnnotationInfo<Cache>> cacheList = new ArrayList<>();/*** 初始化清理缓存注解处理器对象,同时初始化一些缓存操作的对象* @param proceedingJoinPoint* @param cacheEvictor*/public CacheEvictorAnnotationProcessor(ProceedingJoinPoint proceedingJoinPoint, CacheEvictor cacheEvictor) {super();Cache[] value = cacheEvictor.value();for(Cache cache : value){AnnotationInfo<Cache> annotationInfo = getAnnotationInfo(proceedingJoinPoint, cache);cacheList.add(annotationInfo);}}/*** 具体清理缓存处理逻辑* @param proceedingJoinPoint* @return* @throws Throwable*/public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{for (AnnotationInfo<Cache> annotationInfo : cacheList) {String region = annotationInfo.getRegion();String key = annotationInfo.getKey();//清理缓存数据cacheChannel.evict(region,key);}//调用目标方法(就是Controller中的方法)return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());}
}

缓存注解处理器:


/*** 缓存注解处理器*/
public class CachesAnnotationProcessor extends AbstractCacheAnnotationProcessor {private static final Logger logger = LoggerFactory.getLogger(CachesAnnotationProcessor.class);private AnnotationInfo<Cache> annotationInfo;/*** 初始化处理器,同时将相关的对象进行初始化* @param proceedingJoinPoint* @param cache*/public CachesAnnotationProcessor(ProceedingJoinPoint proceedingJoinPoint, Cache cache) {super();//创建注解信息对象annotationInfo = getAnnotationInfo(proceedingJoinPoint,cache);}/*** 具体缓存处理逻辑* @param proceedingJoinPoint* @return* @throws Throwable*/public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{Object result = null;boolean existsCache = false;//1、获取缓存数据CacheHolder cacheHolder = getCache(annotationInfo);if(cacheHolder.isExistsCache()){//2、如果缓存数据存在则直接返回(相当于controller的目标方法没有执行)result = cacheHolder.getValue();//缓存结果数据existsCache = true;}if(!existsCache){//3、如何缓存数据不存在,放行调用Controller的目标方法result = invoke(proceedingJoinPoint);//4、将目标方法的返回值载入缓存setCache(result);}//5、将结果返回return result;}/*** 获取缓存数据* @param annotationInfo* @return*/private CacheHolder getCache(AnnotationInfo<Cache> annotationInfo){Object value = null;String region = annotationInfo.getRegion();String key = annotationInfo.getKey();boolean exists = cacheChannel.exists(region, key);if(exists){CacheObject cacheObject = cacheChannel.get(region, key);value = cacheObject.getValue();//获得缓存结果数据return CacheHolder.newResult(value,true);}return CacheHolder.newResult(value,false);}/*** 调用目标方法* @param proceedingJoinPoint* @return*/private Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());}/*** 设置缓存数据* @param result*/private void setCache(Object result){cacheChannel.set(annotationInfo.getRegion(),annotationInfo.getKey(),result);}
}

在上面的处理器中用 return CacheHolder.newResult(value,true);来获得缓存结果,需要自定义一个结果封装类

缓存结果和缓存信息实体封装

缓存信息封装


/*** Cache相关信息封装*/
public class AnnotationInfo<T extends Annotation> {private T annotation;private String region;private String key;//region:key:paramspublic T getAnnotation() {return annotation;}public void setAnnotation(T annotation) {this.annotation = annotation;}public String getRegion() {return region;}public void setRegion(String region) {this.region = region;}public String getKey() {return key;}public void setKey(String key) {this.key = key;}public String toString() {if (annotation == null) {return null;}return JSONObject.toJSONString(this);}
}

缓存结果封装


/*** 缓存结果封装*/
public class CacheHolder {private Object value;//缓存的数据private boolean existsCache;//缓存数据是否存在private Throwable throwable;/*** 初始化缓存占位*/private CacheHolder() {}/*** 获取值** @return*/public Object getValue() {return value;}/*** 是否存在缓存** @return*/public boolean isExistsCache() {return existsCache;}/*** 是否有错误** @return*/public boolean hasError() {return throwable != null;}/*** 生成缓存结果的占位** @param value       结果* @param existsCache 是否存在缓存* @return 缓存*/public static CacheHolder newResult(Object value, boolean existsCache) {CacheHolder cacheHolder = new CacheHolder();cacheHolder.value = value;cacheHolder.existsCache = existsCache;return cacheHolder;}/*** 生成缓存异常的占位** @param throwable 异常* @return 缓存*/public static CacheHolder newError(Throwable throwable) {CacheHolder cacheHolder = new CacheHolder();cacheHolder.throwable = throwable;return cacheHolder;}
}

开启声明式注解

/*** 开启声明式缓存功能*/@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(CacheMethodInterceptor.class)
public @interface EnableCache {}

注意自定义这个注解以后在主启动类上加@EnableCache即可表示开启注解

controller层使用缓存


/*** 地址簿*/
@Log4j2
@RestController
@RequestMapping("addressBook")
public class AddressBookController {@Autowiredprivate IAddressBookService addressBookService;@Autowiredprivate CacheChannel cacheChannel;private String region = "addressBook";/*** 新增地址簿** @param entity* @return*/@PostMapping("")public Result save(@RequestBody AddressBook entity) {if (1 == entity.getIsDefault()) {addressBookService.lambdaUpdate().set(AddressBook::getIsDefault, 0).eq(AddressBook::getUserId, entity.getUserId()).update();}boolean result = addressBookService.save(entity);if (result) {//载入缓存cacheChannel.set(region,entity.getId(),entity);return Result.ok();}return Result.error();}/*** 查询地址簿详情** @param id* @return*/@GetMapping("detail/{id}")@Cache(region = "addressBook",key = "ab",params = "id")public AddressBook detail(@PathVariable(name = "id") String id) {AddressBook addressBook = addressBookService.getById(id);return addressBook;}/*** 分页查询** @param page* @param pageSize* @param userId* @return*/@GetMapping("page")public PageResponse<AddressBook> page(Integer page, Integer pageSize, String userId, String keyword) {Page<AddressBook> iPage = new Page(page, pageSize);Page<AddressBook> pageResult = addressBookService.lambdaQuery().eq(StringUtils.isNotEmpty(userId), AddressBook::getUserId, userId).and(StringUtils.isNotEmpty(keyword), wrapper ->wrapper.like(AddressBook::getName, keyword).or().like(AddressBook::getPhoneNumber, keyword).or().like(AddressBook::getCompanyName, keyword)).page(iPage);return PageResponse.<AddressBook>builder().items(pageResult.getRecords()).page(page).pagesize(pageSize).pages(pageResult.getPages()).counts(pageResult.getTotal()).build();}/*** 修改** @param id* @param entity* @return*/@PutMapping("/{id}")@CacheEvictor(value = {@Cache(region = "addressBook",key = "ab",params = "1.id")})public Result update(@PathVariable(name = "id") String id, @RequestBody AddressBook entity) {entity.setId(id);if (1 == entity.getIsDefault()) {addressBookService.lambdaUpdate().set(AddressBook::getIsDefault, 0).eq(AddressBook::getUserId, entity.getUserId()).update();}boolean result = addressBookService.updateById(entity);if (result) {return Result.ok();}return Result.error();}/*** 删除** @param id* @return*/@DeleteMapping("/{id}")@CacheEvictor({@Cache(region = "addressBook",key = "ab",params = "id")})public Result del(@PathVariable(name = "id") String id) {boolean result = addressBookService.removeById(id);if (result) {return Result.ok();}return Result.error();}
}

总结

使用aspectj:AOP 技术利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
这里的切面即用户请求时先查询缓存这一过程。
注意:这里使用的是aspectj而非Springaop,故使用时用法有不一样。
使用j2cache框架的整体逻辑:自定义缓存注解,类似springboot自带的cache,但是这里粒度更细,而且更好控制超时时间

缓存层类似如下图:

然后需要用到aspectj的aop逻辑,自定义横切关注点,这里的连接点即是controller层的方法,需要判断每个方法上是否存在cahce注解,如果不存在则直接放行( proceedingJoinPoint.proceed),如果存在则交给缓存处理器进行处理,这里添加和删除缓存主要用的是j2cache组件的cachechannel,个人理解它这里类似一个连接到缓存服务器的通道,且有相应的api可以供增删操作(cacheChannel.set(annotationInfo.getRegion(),annotationInfo.getKey(),result))。在读取缓存时首先是从一级缓存中取,然后从二级缓存中取,如果没找到则查询数据库。对于缓存结果的获得通过封装一个缓存结果类和获得cache注解的信息类来获得( AnnotationInfo ,制定了这个类的数据类型是Annotation的子类)。

springboot使用j2cache框架和aspectj自定义缓存相关推荐

  1. 【SpringBoot应用篇】SpringBoot集成j2cache二级缓存框架

    [SpringBoot应用篇]SpringBoot集成j2cache二级缓存框架 j2cache介绍 j2cache入门使用 pom application.yml caffeine.properti ...

  2. SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis

    场景 若依前后端分离版本地搭建开发环境并运行项目的教程: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108465662 基于上 ...

  3. springboot+mybatis集成自定义缓存ehcache用法笔记

    今天小编给大家整理了springboot+mybatis集成自定义缓存ehcache用法笔记,希望对大家能有所办帮助! 一.ehcache介绍 EhCache 是一个纯Java的进程内缓存管理框架,属 ...

  4. springboot mybatis 项目框架源码 shiro 集成代码生成器 ehcache缓存

    1.代码生成器: [正反双向](单表.主表.明细表.树形表,快速开发利器) freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本.处理类.service等完整模块 ...

  5. SpringBoot和日志框架:缘由,日志框架的选择,使用,自定义配置,日志框架切换

    日志框架 缘由 如果像我们初学者,想知道代码运行到哪里了,一般都是会System.out.println() 进行输出到控制台查看代码运行的情况,好知道代码错误在哪里 但是大型的系统里面,如果有很多的 ...

  6. springboot基于VUE框架的商城综合项目设计与实现毕业设计源码111612

    基于VUE框架的商城综合项目设计与实现 摘  要 随着科学技术的飞速发展,社会的方方面面.各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,商城综合项目当然也不能排除在外.商城综合项 ...

  7. springboot基于VUE框架的商城综合项目设计与实现毕业设计源码

    基于VUE框架的商城综合项目设计与实现 摘  要 随着科学技术的飞速发展,社会的方方面面.各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,商城综合项目当然也不能排除在外.商城综合项 ...

  8. SpringBoot后台管理系统框架

    SpringBoot后台管理系统框架 SpringBoot后台管理系统功能介绍 登录 注册 用户列表和添加功能 只是个框架 实现了shiro权限控制, 详细的shiro使用 一个模板项目系统 只有少量 ...

  9. spring-boot的spring-cache中的扩展redis缓存的ttl和key名

    原文地址:spring-boot的spring-cache中的扩展redis缓存的ttl和key名 前提 spring-cache大家都用过,其中使用redis-cache大家也用过,至于如何使用怎么 ...

最新文章

  1. 初学者必学的C++项目!花3天搞定
  2. 目前的Android恶意软件分类
  3. 运营人必备的7大技能:数据分析能力是未来运营的分水岭
  4. mysql利用cpu率高_MySQL高CPU使用率
  5. 5下载的demo在哪_归类专业能力水平评价练习盘!快来下载呀
  6. 如何在MFC线程中使用控件的成员变量和函数
  7. Python自动化运维工具fabric的安装
  8. 三、解决ie缓存问题
  9. day32 并发编程之锁
  10. 点云上的卷积神经网络及其部分应用
  11. 嵌入式系——软件管理工程
  12. IBatis.Net学习笔记三--两种常用的DAO
  13. c语言计算器程序设计,c语言计算器代码程序设计报告.docx
  14. 20205月6日服务器维护,国服12月6日维护公告:各大区服务器分时段维护
  15. oppoa11android版本是什么,oppoa11x处理器是什么?oppoA11x配置介绍
  16. EPMS- 让企业应用更加轻便!
  17. mysql和oceanbase区别,OceanBase基本概念
  18. 视觉享受,兼顾人文观感和几何特征的字体「GitHub 热点速览 v.22.46」
  19. Python IO编程详解
  20. 笔记本电脑维修90个精选实例

热门文章

  1. c 结构体在声明时赋值_Java基础知识 初识Java 循环结构进阶 数组 数据类型 各种运算符...
  2. springmvc提供RestController方法接口返回json数据中文乱码
  3. sql语言和php,SQL语言快速入门(三)_php
  4. php 获取发票内容,php – 如何从发票ID获取PayPal交易ID
  5. Oracle查找包共用,oracle – 用于查找包的多级依赖关系的脚本
  6. 六十四、Vue项目去哪儿网App开发准备
  7. 物理化学 化学 动力学(上)
  8. Hadoop 文件命令
  9. 它来了,它来了,最强目标检测算法YOLO v4,它真的来了!!!
  10. OpenNRE 2.0:可一键运行的开源关系抽取工具包