EasyExcel
EasyExcel:一行一行读取到内存
EasyExcel是阿里巴巴开源的一个excel处理框架,以使用简单、节省内存著称
POI:java里操作excel,读取、创建excel
POI的缺点:耗内存。因为会把所有数据一起加载到内存中
EasyExcl读写演示
1.加依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
2.@ExcelProperty,创建实体类Anchor用来和表格做映射
index第几列,value列名
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Anchor {
@ExcelProperty(index = 0,value = "主播姓名")
private String name;
@ExcelProperty(index = 1,value = "直播平台")
private String platform;
@ExcelProperty(index=2,value = "成名英雄")
private String hero;
}
演示创建excel表格
一行代码,调用3个方法 EasyExcel.write().sheet().doWrite();
public static void main(String[] args) {
Anchor anchor1 = new Anchor("北慕", "虎牙", "露娜");
Anchor anchor2 = new Anchor("骚白", "斗鱼", "花木兰");
Anchor anchor3 = new Anchor("赖神", "虎牙", "老夫子");
List<Anchor> anchorList = Arrays.asList(anchor1, anchor2, anchor3);
EasyExcel.write("C:\\Users\\lenovo\\Desktop\\主播列表.xlsx",Anchor.class)
.sheet("主播列表")
.doWrite(anchorList);
}
效果如下:
—————————————————————————————————————————
演示读取excel表格
EasyExcel采用一行一行的解析模式,
并将一行的解析结果以观察者的模式通知处理AnalysisEventListener
invoke方法用于处理每条数据,读者可以在这边进行业务逻辑处理
doAfterAllAnalysed方法是只处理完所有数据后进行的动作;
public class ExcelListener extends AnalysisEventListener<Anchor> {
//一行一行去读取excle内容
@Override
public void invoke(Anchor anchor, AnalysisContext analysisContext) {
System.out.println("***"+anchor);
}
//所有行读取完成后执行
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("读取完成后");
}
}
主函数:正真读取是按照第二行开始的
public static void main(String[] args) {
String fileName = "C:\\Users\\lenovo\\Desktop\\主播列表.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, Anchor.class, new ExcelListener())
.sheet("主播列表")
.doRead();
}
控制台打印如下:
数据字典导出
后端代码
可以理解为文件下载功能
DictEeVo,导出的数据字典格式
@Data
public class DictEeVo {
@ExcelProperty(value = "id" ,index = 0)
private Long id;
@ExcelProperty(value = "上级id" ,index = 1)
private Long parentId;
@ExcelProperty(value = "名称" ,index = 2)
private String name;
@ExcelProperty(value = "值" ,index = 3)
private String value;
@ExcelProperty(value = "编码" ,index = 4)
private String dictCode;
}
字典文件下载controller层,返回值为void就行,必须用response对象
@ApiOperation(value="导出")
@GetMapping(value = "/exportData")
public void exportData(HttpServletResponse response) {
dictService.exportData(response);
}
正真的实现是在service层。如下
public void exportData(HttpServletResponse response) {
try {
//设置响应头
response.setContentType("application/vnd.ms-excel"); //指示响应内容的格式
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("数据字典", "UTF-8");
// 指示响应内容以附件形式下载
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
//封装数据集合
List<Dict> dictList = baseMapper.selectList(null);
ArrayList<DictEeVo> dictEeVos = new ArrayList<>();
dictList.forEach(dict -> {
DictEeVo dictEeVo = new DictEeVo();
BeanUtils.copyProperties(dict, dictEeVo);
dictEeVos.add(dictEeVo);
});
//用流的方式,浏览器文件下载
EasyExcel.write(response.getOutputStream(), DictEeVo.class)
.sheet("数据字典")
.doWrite(dictEeVos);
} catch (IOException e) {
e.printStackTrace();
}
}
前端代码
在前台页面点击导出,
相关前端代码大致如下:<div class="el-toolbar-body" style="justify-content: flex-start"> <el-button type="primary" @click="exportData" ><i class="el-icon-download el-icon--right" /> 导出</el-button > ```
不需要写api部分,直接访问。也可以访问9001
exportData() {
window.open("http://localhost:8202/admin/cmn/dict/exportData");
},
导出效果:
数据字典导入
后端接口
导入的excel文件格式,要满足格式DictEeVo
Controller方法想接收一个文件,这里文件名称必须是file,可以使用@RequestParam(“file”)
@ApiOperation(value = "导入")
@PostMapping("importData")
public R importData(MultipartFile file) {
dictService.importDictData(file);
return R.ok();
}
1、创建监听器读取
invoke,每读一行,这个方法就调用一次
@Component
public class DictListener extends AnalysisEventListener<DictEeVo> {
@Autowired
private DictMapper dictMapper;
//一行一行读取
@Override
public void invoke(DictEeVo dictEeVo, AnalysisContext analysisContext) {
//调用方法添加数据库
Dict dict = new Dict();
BeanUtils.copyProperties(dictEeVo,dict);
dictMapper.insert(dict);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
2.service层实现业务逻辑
@Override
public void importDictData(MultipartFile file) {
try {
EasyExcel.read(file.getInputStream(), DictEeVo.class, dictListener)
.sheet()
.doRead();
} catch (IOException e) {
e.printStackTrace();
}
}
3.使用postman测试文件上传接口
查看数据库的字典表,导入成功
前端部分
点击弹窗框上选择导入的文件,visible.sync控制显示隐藏弹出框
<el-dialog title="导入" :visible.sync="dialogImportVisible" width="480px">
<el-form label-position="right" label-width="170px">
<el-form-item label="文件">
<el-upload
:multiple="false"
:on-success="onUploadSuccess"
:action="'http://localhost:8202/admin/cmn/dict/importData'"
class="upload-demo"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">
只能上传xls文件,且不超过500kb
</div>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogImportVisible = false">取消</el-button>
</div>
</el-dialog>
效果如下:
导入成功回调方法
importData() {
this.dialogImportVisible = true;
},
onUploadSuccess(response, file) {
this.$message.info("导入成功");
//关闭弹出框
this.dialogImportVisible = false;
//重新查询一级数据
this.getDictList(1);
},
Spring Cache
Spring Cache 是一个非常优秀的缓存组件;是Spring基于aop提供的自动缓存管理
只需要通过注解标注到查询的业务方法上 可以将查询方法返回的结果缓存起来,以后查询时有缓存不在执行业务代码
Spring Cache步骤:
1、为springcache提供一个缓存管理接口的实现
2、启动类/配置类 添加**@EnableCaching注解
3、在需要缓存管理的业务方法(查询)上使用@Cachable(…key )** 标注
spring会自动管理业务方法的数据缓存 调用第一步实现的缓存管理对象的生命周期方法管理缓存
1、缓存管理接口的实现
redis缓存:缓存的目的避免(减少)客户端从mysql中读取数据
1.添加依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
2.创建配置类(拷贝)
@EnableCaching 必须要加,否则spring-data-cache相关注解不会生效…
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 设置RedisTemplate规则
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//序列号key value
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 设置CacheManager缓存规则
* @param factory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
当然还有第二版,这一版是之后加的(这版是老师讲过的,还算是能看懂。。。)
@Configuration
public class RedisCacheConfig {
//1、RedisTemplate配置键和值的序列化器
@Autowired
RedisTemplate redisTemplate;
@PostConstruct
public void init(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
}
//2、缓存管理CacheManager接口的实现
//LettuceConnectionFactory commons-pool2包中提供的Redis连接池工厂类
@Bean
public CacheManager cacheManager(LettuceConnectionFactory connectionFactory){
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) //缓存过期时间
.disableCachingNullValues() //不缓存空值
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer())) //键序列化器
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));//值序列化器
RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(cacheConfig).build();
return cacheManager;
}
}
3.配置文件
spring.redis.host=192.168.2.108
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
#连接池最大连接数
spring.redis.lettuce.pool.max-active=20
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=5
#连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
2、使用@Cachable缓存管理
@Cacheable添加缓存
1、查询数据字典+@Cacheable
- 如果缓存存在,则直接读取缓存数据返回;
- 如果缓存不存在,则执行方法,并把返回的结果存入缓存中。
1.key 可选属性,可以使用 SpEL 标签自定义缓存的key, key中可以获取业务方法的形参值
2.#号代表这是一个 SpEL 表达式,此表达式可以遍历方法的参数对象
@Cacheable(value = "dictCache", key = "'dict_'+#id")
3. 缓存的key: 使用value和key的值拼接,如: dictCache::dict_id的值如果参数是对象Dict dict,可以这么获取
> key="'dict_'+#dict.value"
//value:命名空间
@Cacheable(value = "dictCache", key = "'dict_'+#id")
@Override
public List<Dict> findChildData(Long id) {
QueryWrapper<Dict> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id", id);
//向list集合每个dict对象中设置hasChildren
List<Dict> dicts = baseMapper.selectList(queryWrapper);
System.out.println("从mysql中查询id=" + id + "的数据");
dicts.forEach(dict -> {
dict.setHasChildren(this.isChildren(dict.getId()));
});
return dicts;
}
redis中效果:
@CacheEvict缓存驱逐
放在删除、更新方法上
使用该注解**@CacheEvict**标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
allEntries 是否清除当前 value值空间下的所有缓存数据。默认false
导入数据的时候,把redis中数据都清除
@CacheEvict(value = "dictCache", allEntries = true)
@Override
public void importDictData(MultipartFile file) {
try {
EasyExcel.read(file.getInputStream(), DictEeVo.class, dictListener)
.sheet()
.doRead();
} catch (IOException e) {
e.printStackTrace();
}
}
@CachePut更新缓存
用在修改方法或添加方法上
同步缓存的做法
当mysql中的数据发生改变,就会清空redis中对应命名空间下的缓存数据
3、手动缓存管理
由于缓存雪崩的问题,我们需要手动管理缓存。。使TTL设置的时间不那么集中。虽然这是另外一个方法,不要在意这些细节。。。
@Autowired
PmsClient pmsClient;//远程服务调用的feign接口
@Autowired
RedisTemplate redisTemplate;
//删除上面的Cacheable注解
@Override
public List<CategoryEntity> levelTwoAndSubsCates(String cid) {
//使用缓存管理
//1、从缓存中查询cid的二级三级分类集合 如果有直接返回
String key = "idx:cache:cates:"+cid;
Object obj = redisTemplate.opsForValue().get(key);
if(obj!=null){
return (List<CategoryEntity>) obj;
}
//2、如果缓存没有 再远程查询二三级分类集合
ResponseVo<List<CategoryEntity>> responseVo = pmsClient.lv2AndSubsCates(cid);
//将查询结果存到缓存中
redisTemplate.opsForValue()
.set(key,responseVo.getData() ,
1800+new Random().nextInt(200), TimeUnit.SECONDS);
return responseVo.getData();
}
4、缓存一致性
数据库数据更新后如何保证缓存的数据和数据库数据一致:
-
双写模式:数据库更新同时更新redis缓存数据
先写哪一个存在问题:redis和数据库之间的事务不容易保证
-
失效模式: 更新数据库时 让缓存失效
更新数据库业务执行时 缓存会失效,此时如果数据还未写成功 有请求查询数据查到了数据库中还未更新的数据到缓存中,此时才写成功 缓存的数据和数据库仍然不一致
-
双删模式:更新数据库前删除一次缓存 更新成功后再删除一次缓存
-
数据库同步中间件:基于mysql的binlog日志将数据库更新的数据同步到第三方的中间件(redis mq es)
- canal:阿里开源的一个框架
5、缓存并发问题
1.缓存雪崩
缓存雪崩:首页数据访问量大,数据缓存过期时间接近 会导致多个缓存同时失效,大量的请求可能直接查询数据库
解决:随机因子 让多个热门key失效的时间分散
2.缓存击穿
缓存击穿:单个热点key突然失效,导致大量的请求同时访问数据库查询数据
解决:控制只让一个线程查数据 其他的等待使用缓存 分布式锁
3.缓存穿透
缓存穿透 : 访问数据库一定不存在的数据时,请求每次都会先查询缓存,然后再查数据库
解决:
- 空数据也短暂的缓存:为了避免恶意攻击
- 布隆过滤器