一直以来对缓存都是一知半解,从没有正经的接触并使用一次,今天腾出时间研究一下缓存技术,开发环境为OpenJDK17SpringBoot2.7.5

SpringCache基础概念

接口介绍

首先看看SpringCache中提供的两个主要接口,第一个是CacheManager缓存管理器接口,在接口名的位置按F4(IDEA Eclipse快捷键)可查看接口的实现,其中最底下的ConcurrentMapCacheManager就是缓存管理器默认实现,在不进行任何配置的情况下直接使用缓存默认使用的就是基于Map集合的缓存

ConcurrentMapCacheManager实现类中可以看到,该实现类主要维护Map类型的cacheMap属性,Value为Cache类型的接口,点进该接口可以发现他同样有基于Map的实现类ConcurrentMapCache,开启Debug调试后简单测试了一下,代码走到了这个位置也确定了使用的就是该类

Cache接口就是就是第二个要了解的接口,梳理一下,CacheManager为缓存管理器并且管理着Cache对象,而被管理的Cache提供了操作缓存数据的方法

注解介绍

上面介绍了SpringCache中两个接口,这里来了解一下缓存需要的注解,开发中最常用的就是基于注解的缓存

名称 解释
@Cacheable 将方法的返回结果进行缓存,后续方法被调用直接返回缓存中的数据不执行方法,适合查询
@CachePut 将方法的返回结果进行缓存,无论缓存中是否有数据都会执行方法并缓存结果,适合更新
@CacheEvict 删除缓存中的数据
@Caching 组合使用缓存注解
@CacheConfig 统一配置本类的缓存注解的属性
@EnableCaching 用于启动类或者缓存配置类,表示该项目开启缓存功能

前三个注解用于对缓存数据进行增删改查操作,@Caching注解的作用就是将前三个注解组合使用,适用于有关联关系的缓存数据,@CacheConfig则是针对本类中的缓存做一些通用的配置

使用SpringCache

在写这篇文章之前也参考过一些其他博主的文章,好多文章都要求引用spring-boot-starter-cache启动器,但据我测试不引用该启动器也可以实现缓存功能,相关的类和注解在spring-context包中就已经存在了,我不太清楚为什么他们要引用spring-boot-starter-cache,如果您懂的话麻烦在评论区指点下

编写测试环境

项目添加的依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserEntity implements Serializable { @Serial
private static final long serialVersionUID = 1L; private Integer id;
private String name; }

Mapper 并没有查询数据库,而是模拟出来的假数据

@Repository
public class UserMapper { public final List<UserEntity> users = CollUtil.newArrayList(); @PostConstruct
public void init() {
users.add(new UserEntity(1, "用户" + 1));
users.add(new UserEntity(2, "用户" + 2));
users.add(new UserEntity(3, "用户" + 3));
users.add(new UserEntity(4, "用户" + 4));
} public List<UserEntity> list() {
return this.users;
} public UserEntity getOne(Integer id) {
return this.users.stream()
.filter(user -> NumberUtil.equals(user.getId(), id))
.findFirst()
.orElse(null);
} public void update(UserEntity entity) {
this.delete(entity.getId());
users.add(entity);
} public void delete(Integer id) {
UserEntity entity = this.getOne(id);
if (ObjectUtil.isNotNull(entity)) {
users.remove(entity);
}
} }

Service

@Slf4j
@Service
public class UserService { @Autowired
private UserMapper mapper; public List<UserEntity> selectList() {
List<UserEntity> list = mapper.list();
log.info("list:{}", list.size());
return list;
} public UserEntity getOne(Integer id) {
log.info("getOne:{}", id);
return mapper.getOne(id);
} public UserEntity update(UserEntity entity) {
log.info("update:{}", entity);
mapper.update(entity);
return entity;
} public void delete(Integer id) {
log.info("delete:{}", id);
mapper.delete(id);
} public void clear() {} }

Controller

@RestController
@RequestMapping("/user")
public class UserController { @Autowired
private UserService service; @GetMapping("selectList")
public Object selectList() {
return success(service.selectList());
} @GetMapping("getOne")
public Object getOne(Integer id) {
return success(service.getOne(id));
} @GetMapping("update")
public Object update(UserEntity entity) {
service.update(entity);
return success();
} @GetMapping("delete")
public Object delete(Integer id) {
service.delete(id);
return success();
} @GetMapping("clear")
public Object clear() {
service.clear();
return success();
} /* ---------工具方法 --------- */ public Map<String, Object> success(Object obj) {
Map<String, Object> result = success();
result.put("data", obj);
return result;
} public Map<String, Object> success() {
Map<String, Object> result = MapUtil.newHashMap();
result.put("code", 200);
result.put("msg", "success");
return result;
} }

最后在启动类使用 @EnableCache 注解启用缓存

@EnableCaching
@SpringBootApplication
public class BootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(BootCacheApplication.class, args);
}
}

测试环境的编写到此位置,可以试着请求一下接口看看是否搭建成功,以及控制台中是否有对应的日志打印

使用注解缓存

注解缓存主要是@Cacheable、@CachePut、@CacheEvict这三种,且参数都基本相同,用过一次的参数基本就不重复说了,接下来测试在Service层中添加缓存注解

@Cacheable

该注解适用于查询方法,将查询返回的结果放到缓存中,下次接收到请求直接返回缓存中的数据,先将注解按照下面的写法加到代码中,然后调用看看效果

// @Cacheable(cacheNames = "USERS", key = "#root.methodName")
@Cacheable(cacheNames = "USERS", key = "'selectList'")
public List<UserEntity> selectList() { ... } // @Cacheable(cacheNames = "USERS", key = "#id")
@Cacheable(cacheNames = "USERS", key = "#root.args[0]")
public UserEntity getOne(Integer id) { ... }

注解加上后反复请求这两个接口,可以发现相同的请求Service日志只打印了一次,因为数据已经添加到缓存不会在执行Service代码了

cacheNames

现在来看一下@Cacheable注解中的参数,首先来看cacheNames,该参数可以理解为一个组,cacheNames相同的缓存会放到同一个Cache对象中进行管理,例如USERS中只维护与用户相关的缓存,DEPTHS中只维护部门相关的缓存,就是ConcurrentMapCacheManager中cacheMap的结构

key与SpEL表达式

第二个参数key代表的是缓存数据在该组中的唯一标识,通过观察Cache对象可以看出来

需要注意的是key的参数需要使用SpEL表达式,如果想直接使用字符串作为key的话需要用单引号括起来

  • #root.methodName:获取方法名
  • #id:获取参数列表中的id属性
  • #root.args[0]:获取参数列表中第一个参数
  • #result:返回结果对象
  • 更多用法详见官方文档

condition

除了上面用到的两个参数之外,这里在介绍一个bool类型参数condition,该参数的作用是做条件判断,只有判断结果为true注解才会生效,现在修改一下Service中的注解,添加condition条件

/**
* 只有ID为1时才进行缓存,其他数据直接执行Service代码
*/
@Cacheable(cacheNames = "USERS", key = "#root.args[0]", condition = "#id == 1")
public UserEntity getOne(Integer id) { ... }

unless

condition的作用是只有判断结果为true结果才生效,同时有个与他相对的unless注解,判断结果为true时注解失效

/**
* 只有ID为1时不执行缓存,每次都会执行Service代码
* 其他数据正常缓存
*/
@Cacheable(cacheNames = "USERS", key = "#root.args[0]", unless = "#id == 1")
public UserEntity getOne(Integer id) { ... }

cacheManager

当系统中配置了多个缓存实现的时候,可以在注解中传入缓存管理器的bean名称来指定该缓存使用哪个实现,如下图所示,这里就不演示了

sync 了解即可

在多线程的情况下缓存数据可能会被重复操作多次,如果缓存数据比较敏感可以使用sycn属性将缓存数据设置为多线程安全,不过一般很少有人会将敏感数据存放到缓存中,所以sync默认为关闭状态,也很少会有人开启他,而且需要注意的是并不是所有缓存实现类都可以实现该功能,目前可以肯定的是Spring官方给出的所有CacheMapper实现类都支持这个属性,属性并不常用图省事儿这里也就不演示了

keyGenerator 了解即可

修改缓存key的生成规则,在注解中使用keyGenerator参数后就不能在使用key,缓存key的生成规则由keyGenerator来决定,如果想对某个接口使用自定义规则key,需要向ioc容器中注入SimpleKeyGenerator类型的bean,然后将bean的名称传入keyGenerator即可,如下图所示,这里就不演示了

@CachePut

在使用该注解之前先来做一个小测试,将getOne注解上的condition和unless条件清除,读取ID为1的用户数据,然后修改该用户的数据,修改过后在读取一次该用户的数据,看看效果如何

可以发现虽然我修改了ID为1的用户数据,但是查询该数据时返回的仍是旧数据,这时就需要使用@CachePut注解更新缓存数据,该注解会用方法的返回结果更新掉缓存中的旧数据

/**
* 在getOne中缓存数据的key为#root.args[0],代表的是参数列表中第一位,也就是用户ID
* 那么在update方法中用来更新缓存数据的也应该是用户ID,也就是返回结果中的id: #result.id
*/
@CachePut(cacheNames = "USERS", key = "#result.id")
public UserEntity update(UserEntity entity) { ... }

@CacheEvict

@CacheEvict注解的作用就是删除缓存中的旧数据,通过参数key来指定删除的是哪一条,同时该注解还有allEntries参数,在不使用key的情况下设置allEntries为true可以清空该cacheNames下所有缓存

@CacheEvict(cacheNames = "USERS", key = "#id")
public void delete(Integer id) { ... } @CacheEvict(cacheNames = "USERS", allEntries = true)
public void clear() {}

这里可以自行调用测试并查看日志打印情况,我就不上传截图了

@Caching

该注解的作用是组合其他注解使用,例如删除了该用户后,还需要删除该用户的登录信息,就可以使用到该注解

@Caching(evict= {
@CacheEvict(cacheNames = "USERS", key = "#id"),
@CacheEvict(cacheNames = "TOKENS", key = "#id")
})
public void delete(Integer id) { ... }

@CacheConfig

@CacheConfig可以针对当前类的所有缓存注解进行统一配置,例如之前在每个注解上都使用了cacheNames属性,在使用了@CacheConfig注解后只需要在该类上标注cacheNames那么类中的注解就可以省去该参数了

@Slf4j
@Service
@CacheConfig(cacheNames = "USERS")
public class UserService { @Autowired
private UserMapper mapper; @Cacheable(key = "'selectList'")
public List<UserEntity> selectList() { ... } @Cacheable(key = "#root.args[0]")
public UserEntity getOne(Integer id) { ... } @CachePut(key = "#result.id")
public UserEntity update(UserEntity entity) { ... } @Caching(evict= {
@CacheEvict(key = "#id"),
@CacheEvict(cacheNames = "TOKENS", key = "#id")
})
public void delete(Integer id) { ... } @CacheEvict(allEntries = true)
public void clear() {} }

使用编程式缓存

编程式缓存,指在代码中操作缓存数据,之前提到过SpringCache中有提供Cache接口,我们通过代码操作缓存靠的就是这个接口,直接在Controller里演示一下

@RestController
@RequestMapping("/user")
public class UserController { @Autowired
private CacheManager cacheManager; @GetMapping("/test")
public Object test() {
// 通过cacheManager获取维护用户缓存的Cache对象
Cache cache = cacheManager.getCache("USERS");
// 向缓存中添加数据
cache.put(8, new UserEntity(8, "用户8"));
cache.put(9, new UserEntity(9, "用户9"));
// 打印测试
System.out.println(cache.get(8, UserEntity.class));
System.out.println(cache.get(9, UserEntity.class));
// 移除其中一个缓存数据
cache.evict(8);
// 打印测试
System.out.println(cache.get(8, UserEntity.class));
System.out.println(cache.get(9, UserEntity.class));
// 清空缓存数据
cache.clear();
// 打印测试
System.out.println(cache.get(8, UserEntity.class));
System.out.println(cache.get(9, UserEntity.class)); return success();
} // 省略多余代码 ..... }

整合Redis为缓存实现

整合Redis需要引用Redis的场景启动器

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

添加Redis场景启动器后刷新Maven依赖,然后在回过头来看CacheManager接口的实现类,会发现多了基于Redis的缓存实现

之后在配置文件中添加Redis的连接信息,重启项目就可以请求接口进行测试了

spring:
redis:
host: 192.168.1.34
port: 6379
password: redis
database: 1

修改配置文件

我们可以通过配置文件来对缓存进行一些设置,找到缓存的自动配置类CacheAutoConfiguration可以看到类中启用了CacheProperties

CacheProperties类可以知道配置文件中可以设置哪些属性,例如指定使用Redis作为缓存实现,不过当我们引入Redis场景启动器后,缓存的默认实现已经被设置为Redis,所以这个设置也可以忽略

spring:
cache:
type: redis

指定cacheNames集

在缓存注解中使用的cacheNames的作用是对缓存数据进行分组,当缓存中没有这个组的时候会自动创建这个组,同时该属性可以在配置文件中设置,不过需要注意的是一旦在配置文件中指定cacheNames,那么缓存注解将不再提供自动创建的功能,使用不存在的cacheNames会报错

spring:
cache:
type: redis
cache-names:
- USERS
- DEPT
- ...

Redis配置项

配置项一个个介绍比较麻烦,这里直接将Redis的配置项列出来,配置项对应CacheProperties.redis属性

spring:
cache:
# 指定Redis作为缓存实现
type: redis
# 指定项目中的cacheNames
cache-names:
- USERS
redis:
# 缓存过期时间为10分钟,单位为毫秒
time-to-live: 600000
# 是否允许缓存空数据,当查询到的结果为空时缓存空数据到redis中
cache-null-values: true
# 为Redis的KEY拼接前缀
key-prefix: "BOOT_CACHE:"
# 是否拼接KEY前缀
use-key-prefix: true
# 是否开启缓存统计
enable-statistics: false

修改Redis缓存为JSON

缓存功能已经实现,但是根据之前的测试来看,存到Redis中的数据是一堆乱码不利于查看和维护,这里修改下Redis缓存的序列化

源码分析

源码分析部分比较枯燥,可跳过

在缓存的自动配置类CacheAutoConfiguration中可以看到使用@Import注解引用了CacheConfigurationImportSelector类,该类实现了ImportSelector接口,可以动态的向ioc容器中注入指定的bean

而在CacheConfigurationImportSelector类中遍历了CacheType枚举,这个CacheType正对应着一开始在配置文件中所写的spring.cache.type=redis,在代码结束的位置也看到了Redis的缓存配置类RedisCacheConfiguration

这里挑重点直接看RedisCacheConfiguration配置类,在该配置类中使用@Bean向ioc容器中添加了CacheManager的实现,被Bean标注的方法参数列表默认都是可以在ioc容器中找到的,而这个参数列表中包含RedisCacheConfiguration

需要注意当前所在的位置是autoconfigure.cache包,而参数列表中的RedisCacheConfiguration类是data.redis.cache包下的,这是两个不同的类

这里顺着该对象往下看调用,先是调用了determineConfiguration,而后调用了createConfiguration,在createConfiguration可以看出Redis默认使用的是JDK的序列化实现

回过头来看一眼,参数列表中好像并不是RedisCacheConfiguration对象,而是RedisCacheConfiguration类型的ObjectProvider对象,这里解释一下ObjectProvider是对象提供者,他会优先取ioc容器中该类型的Bean,如果没有就使用自己的对象,具体可以看determineConfiguration方法

这样一来事情思路就理清了,只要使用ObjectProvider的特点,自己向IOC容器中提供一个RedisCacheConfiguration对象就可以覆盖掉原本的配置了

功能实现代码

先创建个Redis缓存配置类,编写一个@Bean的方法返回RedisCacheConfiguration对象,为了防止配置文件中的配置项失效,这里直接将上面的createConfiguration方法体复制过来,将CacheProperties放到参数列表中,他会自己去ioc容器中取,另一个参数是JDK序列化用到的,这里用不上就不拿过来了

然后将代码中的JDK序列化删掉,通过Ctrl + P(IDEA Eclipse快捷键)可以看到他需要RedisSerializer类型的序列化对象

点开这个接口查看他的实现类,发现Redis提供了两个JSON序列化对象

下面的是带有泛型的序列化器,这里推荐通用的GenericJackson2JsonRedisSerializer,该类有空参构造直接new对象即可,替换掉JDK序列化

@Configuration
public class CustomRedisConfig { @Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
// 获取Redis配置信息
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
// 获取Redis默认配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 指定序列化器为GenericJackson2JsonRedisSerializer
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 过期时间设置
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
// KEY前缀配置
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
// 缓存空值配置
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
// 是否启用前缀
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
} }

这样一来JSON序列化功能就完成了,可以重启检查一下效果,由于反序列化的需要,JSON的每个对象中都添加了class信息

修改前缀生成规则

Redis的KEY一般用:来区分层级,这是个约定俗成的习惯,我使用的Redis可视化工具也会基于:将key放在不同文件夹下进行分组展示,可是RedisCache生成的KEY中间有个双冒号,导致可视化界面中有一层文件夹是空的,强迫症表示难以接受,这个问题必须解决

前缀的生成靠的是CacheKeyPrefix,点开这个类可以看到他将双冒号直接写死在代码中了,我们需要自定义接口去继承他,然后代替他

public interface CustomKeyPrefix extends CacheKeyPrefix {

    String SEPARATOR = ":";

    String compute(String cacheName);

    static CustomKeyPrefix simple() {
return (name) -> name + SEPARATOR;
} static CustomKeyPrefix prefixed(String prefix) {
Assert.notNull(prefix, "Prefix must not be null!");
return (name) -> prefix + name + SEPARATOR;
} }

回到刚刚创建的Redis缓存配置类中,

@Configuration
public class CustomRedisConfig { @Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
// 获取Redis配置信息
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
// 获取Redis默认配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 指定序列化器为GenericJackson2JsonRedisSerializer
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 过期时间设置
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
// 替换前缀生成器(有前缀和无前缀)
config = config.computePrefixWith(CustomKeyPrefix.simple());
if (redisProperties.getKeyPrefix() != null) {
config = config.computePrefixWith(CustomKeyPrefix.prefixed(redisProperties.getKeyPrefix()));
}
// 缓存空值配置
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
// 是否启用前缀
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
} }

问题结局,可以重启后检查一下效果,非常滴完美

Springboot 整合 SpringCache 使用 Redis 作为缓存的更多相关文章

  1. 项目总结10:通过反射解决springboot环境下从redis取缓存进行转换时出现ClassCastException异常问题

    通过反射解决springboot环境下从redis取缓存进行转换时出现ClassCastException异常问题 关键字 springboot热部署  ClassCastException异常 反射 ...

  2. Spring-Boot项目中配置redis注解缓存

    Spring-Boot项目中配置redis注解缓存 在pom中添加redis缓存支持依赖 <dependency> <groupId>org.springframework.b ...

  3. SpringBoot30 整合Mybatis-Plus、整合Redis、利用Ehcache实现二级缓存、利用SpringCache和Redis作为缓存

    1 环境说明 JDK: 1.8 MAVEN: 3. SpringBoot: 2.0.4 2 SpringBoot集成Mybatis-Plus 2.1 创建SpringBoot 利用IDEA创建Spri ...

  4. Springboot使用Shiro-整合Redis作为缓存 解决定时刷新问题

    说在前面 (原文链接: https://blog.csdn.net/qq_34021712/article/details/80774649)本来的整合过程是顺着博客的顺序来的,越往下,集成的越多,由 ...

  5. springboot整合mybatis,mongodb,redis

    springboot整合常用的第三方框架,mybatis,mongodb,redis mybatis,采用xml编写sql语句 mongodb,对MongoTemplate进行了封装 redis,对r ...

  6. e3mall商城的归纳总结9之activemq整合spring、redis的缓存

    敬给读者 本节主要给大家说一下activemq整合spring,该如何进行配置,上一节我们说了activemq的搭建和测试(单独测试),想看的可以点击时空隧道前去查看.讲完了之后我们还说一说在项目中使 ...

  7. springboot(七).springboot整合jedis实现redis缓存

    我们在使用springboot搭建微服务的时候,在很多时候还是需要redis的高速缓存来缓存一些数据,存储一些高频率访问的数据,如果直接使用redis的话又比较麻烦,在这里,我们使用jedis来实现r ...

  8. SpringBoot整合Shiro使用Ehcache等缓存无效问题

    前言 整合有缓存.事务的spring boot项目一切正常. 在该项目上整合shiro安全框架,发现部分类的缓存Cache不能正常使用. 然后发现该类的注解基本失效,包括事务Transaction注解 ...

  9. 【转载】Springboot整合 一 集成 redis

    原文:http://www.ityouknow.com/springboot/2016/03/06/spring-boot-redis.html https://blog.csdn.net/plei_ ...

  10. springBoot整合Spring-Data-JPA, Redis Redis-Desktop-Manager2020 windows

    源码地址:https://gitee.com/ytfs-dtx/SpringBoot Redis-Desktop-Manager2020地址: https://ytsf.lanzous.com/b01 ...

随机推荐

  1. 关于在PyCharm中使用虚拟环境

    Python虚拟环境的概念对于管理项目用到的第三方包真是好处多多,所以也想在PyCharm使用虚拟环境. 在这个过程中,遇到很多问题: 第一是使用Python创建虚拟环境,然后在PyCharm创建项目 ...

  2. Init Container(初始化容器)

    在很多应用场景中,应用在启动之前都需要进行如下初始化操作. ◎ 等待其他关联组件正确运行(例如数据库或某个后台服务). ◎ 基于环境变量或配置模板生成配置文件. ◎ 从远程数据库获取本地所需配置,或者 ...

  3. Beats在Kibana中的集中管理

    前提条件: 1.es版本是白金版 2.es开启安全设置,kibana访问es需要密码 操作步骤汇总: 1-3步是基础环境配置 4-9步是注册beats到集中管理平台,然后启动beats,只是单纯启动b ...

  4. 监控告警之elastalert部署及配置全解

    一.安装elastalert 1.环境 CentOS:7.4 Python:3.6.9 pip:19.3 elastalert:0.2.1 elk:7.3.2 2.配置Python3.6.9环境 安装 ...

  5. 改变一个数组内元素的位置,不通过splice方法。

    这个数据 现在已经完成了,将本来在第一位的18代金券改到第31位,下面说一下怎么实现的. //currHotRightsTypeSorted这个是数据源头,legalRightsType这个是数据的分 ...

  6. 关于vmware虚拟机的ova/ovf转换成aws上的AMI镜像

    很多时候,我们会有这样的需求,需要将DC中vmware虚拟化的服务器,迁移到aws上,我们就得先将vmware虚拟机导出,然后转换 关于vmvare虚拟的导出备份,一般有ova(Open Virtua ...

  7. Spring bean装配流程和三级缓存

    马士兵 源码方法论 不要忽略源码中的注释 先梳理脉络,再深入细节 大胆猜测.小心求证 见名知意 hold on 对源码有兴趣的都是变态 为了钱! Spring IoC Spring容器帮助管理对象,不 ...

  8. Selenium+Python系列(二) - 元素定位那些事

    一.写在前面 今天一实习生小孩问我,说哥你自动化学了多久才会的,咋学的? 自学三个月吧,真的是硬磕呀,当时没人给讲! 其实,学什么都一样,真的就是你想改变的决心有多强罢了. 二.元素定位 这部分内容可 ...

  9. 我要手撕mybatis源码

    传统的JDBC编程中的一般操作: 1.注册数据库驱动类,指定数据库的URL地址.数据库用户名.密码等连接信息 2.通过DriverManager打开数据库连接 3.通过数据库连接创建Statement ...

  10. 齐博X1-栏目的调用2

    fun('sort@fathers',$fid,'cms')  获取上层多级栏目这样的,比如我们现在所属第三级栏目,现在可以利用这个函数获取第二级和第一级的栏目,当然自身也会被调用出来,所以此函数用的 ...