一、InitializingBean的介绍
我们知道,SpringBean的生命周期中,bean对象创建出来后会继续执行一系列操作进行属性注入和初始化,若bean对象所在的类实现了InitializingBean接口,那么该bean在初始化的时候,spring会调用其重写后的afterPropertiesSet()方法。通常,我们重写这个方法可以用来加载一些初始数据或初始化操作,如加载菜单树形列表、创建第三方系统的客户端连接对象等等,这样可以使得项目启动完成后,客户在请求数据时,后端可以立即执行业务逻辑,而不是先加载一些系统资源,导致请求响应缓慢或者超时,提高客户体验。
二、实现InitializingBean接口提前加载资源的项目实践
1、加载菜单树形列表并放入缓存中间件
通常情况下,菜单信息、商品分类信息以及母公司子公司的单位层级这种包含关系的数据,在数据表中是通过id和parent_id两个字段进行关联的,而我们要查询出这些信息并且将这种树形层级关系展示出来,需要执行大量的遍历和递归,时间复杂度和空间复杂度都比较高。如果我们每次访问这些数据都去执行数据库查询和递归数据封装,不仅响应时间很长,而且内存的资源消耗也会很高,我们希望对这类操作做一些优化。
事实上,我们可以发现,像这种层级数据,一旦保存到数据库后,一般就会比较少地去修改它,大多情况下我们都是查询操作。所以,我们可以联想到将这类层级数据放入到缓存中,提高查询速度。同时,我们可以通过服务启动的初始化操作,预先去加载数据,使用户在首次查询时也可以快速响应结果,提升用户体验。
因此,我的优化思路是:service实现类实现InitializingBean接口,重写afterPropertiesSet方法,在方法中执行数据库查询和层级结构封装,然后将封装后的数据保存到redis中,后续针对该数据的请求都会先从redis中获取。
1.1、缓存优化的具体代码实现及测试结果对比
以下是两种方案的代码以及jmeter压测的数据:封装树形结构的具体实现
不使用缓存:
@Override
public List<CategoryVo> findCategoryTreeNoCatch() {
// 从数据库中查询所有数据
List<CategoryVo> list = categoryMapper.selectAll();
// 对数据进行层级结构封装
return CollUtil.isNotEmpty(list) ? BaseTreeUtils.listTreeNodes(list) : CollUtil.newArrayList();
}
使用缓存进行优化:
@Service
public class CategoryServiceImpl implements CategoryService, InitializingBean {
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public List<CategoryVo> findCategoryTree() {
String jsonVos = redisTemplate.opsForValue().get(ProductConstant.CATEGORY_TREE_INFO);
List<CategoryVo> resultVos = null;
if (StrUtil.isNotBlank(jsonVos)) {
try {
ObjectMapper objectMapper = new ObjectMapper();
resultVos = objectMapper.readValue(jsonVos, new TypeReference<List<CategoryVo>>() {
});
return resultVos;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
List<CategoryVo> list = categoryMapper.selectAll();
resultVos = CollUtil.isNotEmpty(list) ? BaseTreeUtils.listTreeNodes(list) : CollUtil.newArrayList();
redisTemplate.opsForValue().set(
ProductConstant.CATEGORY_TREE_INFO,
JSON.toJSONString(resultVos),
30l,
TimeUnit.MINUTES
);
return resultVos;
}
@Override
public void afterPropertiesSet() throws Exception {
// todo: 实现InitializingBean接口,重写afterPropertiesSet方法在启动时加载商品分类信息
List<CategoryVo> list = categoryMapper.selectAll();
List<CategoryVo> resultVos = CollUtil.isNotEmpty(list) ? BaseTreeUtils.listTreeNodes(list) : CollUtil.newArrayList();
String jsonVos = JSON.toJSONString(resultVos);
redisTemplate.opsForValue().set(
ProductConstant.CATEGORY_TREE_INFO,
jsonVos,
30l,
TimeUnit.MINUTES
);
}
}
1.2、结果分析
通过对比我们不难发现,在不使用缓存的情况下,接口的吞吐量为51.3/sec,响应速度的平均值为2636ms;而使用缓存进行优化的情况下,接口的吞吐量为332.6/sec,响应速度的平均值为13ms。优化的效果是很明显的。因此,我们可以实现Spring框架的InitializingBean接口提前加载多读少写的数据,并使用缓存中间件来提升数据读取速度,实现查询优化。
在此基础上,还可以引入SpringCatch对逻辑代码进行简化:
<!--引入Spring Catch-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
@Service
public class CategoryServiceImpl implements ICategoryService, InitializingBean {
@Autowired
private CategoryMapper categoryMapper;
@Override
@Cacheable(value = ProductConstant.PRODUCT_CATEGORY_KEY, key = "'level1'")
public List<Category> getLevel1List() {
return categoryMapper.getLevel1List();
}
@Override
@Cacheable(value = ProductConstant.PRODUCT_CATEGORY_KEY, key = "'tree'")
public List<CategoryVo> findCategoryTree() {
List<CategoryVo> list = categoryMapper.selectAll();
return CollUtil.isNotEmpty(list) ? BaseTreeUtils.listTreeNodes(list) : CollUtil.newArrayList();
}
@Override
public void afterPropertiesSet() throws Exception {
initProductTree();
}
@CachePut(value = ProductConstant.PRODUCT_CATEGORY_KEY, key = "'tree'")
public List<CategoryVo> initProductTree() {
List<CategoryVo> list = categoryMapper.selectAll();
return CollUtil.isNotEmpty(list) ? BaseTreeUtils.listTreeNodes(list) : CollUtil.newArrayList();
}
}
2、预加载第三方系统的客户端连接
在项目中,我们会用到很多其他的工具或依赖来实现具体的功能。例如使用minio来存储用户上传的文件,使用ElasticSearch来实现全文检索等等。我们使用这些工具的大致步骤通常为:创建网络连接,进行数据交互,关闭连接通道,释放资源。如果我们每次访问这些第三方工具都要执行一整个完整的逻辑,很明显可以看出网络连接的频繁创建和释放会提高请求的响应时间,同时也会加剧系统资源的消耗。
在spring框架中,我们的service实现类对象通常都是单例模式的。因此,我们可以通过实现InitializingBean接口在项目启动时预加载第三方工具的客户端连接,并作为成员变量存放在系统中,接收请求后可以立即执行数据交互,多次请求复用已有的连接,提高响应速度。以文件上传功能使用minio进行存储为例,具体代码实现为:
@Service
public class FileUploadServiceImpl implements FileUploadService, InitializingBean {
@Autowired
private MinioProperties minioProperties;
private MinioClient minioClient;
@Override
public String fileUpload(MultipartFile multipartFile) {
......
}
@Override
public void afterPropertiesSet() {
// 创建一个Minio的客户端对象
minioClient = MinioClient.builder()
.endpoint(minioProperties.getEndpointUrl())
.credentials(minioProperties.getAccessKey(), minioProperties.getSecreKey())
.build();
}
}
三、总结
在后端项目中,我们可以通过实现SpringBean的InitializingBean接口在服务启动时预加载初始数据或其他客户端连接,优化程序响应速度。