1、介绍
- 分布式缓存方案
- 缓存服务搭建
2、分布式缓存方案(重点掌握)
2.1、什么是缓存
通常将数据从数据库中同步一份到内存中,客户端直接从内存中查询数据,减少了和数据库的交互次数,提高查询性能(因为内存读写很快),减轻数据库的压力
2.2、哪些数据适合缓存
- 经常查询的热点数据
- 不经常变的数据(数据变化会导致缓存中的数据跟着变,如果比较频繁,性能开销比较大)
2.3、缓存的流程
- 第一次查询,先看缓存是否有数据 , 如果有:直接返回
- 如果缓存没有数据,去数据库查询数据
- 把数据同步一份到缓存
- 返回数据
注意:数据库数据被修改,缓存要清空,或者重置
2.4、传统缓存方案及流程
缺点:
- 在集群环境中,每个应用都有一个本地缓存,当缓存发生修改会造成缓存不同步问题
- 本地缓存本身要占用应用的内存空间
2.5、分布式缓存方案
优点:
- 使用Redis作为共享缓存 ,解决缓存不同步问题
- Redis是独立的服务,缓存不用占应用本身的内存空间
2.6、为什么要缓存课程分类
门户首页需要展示课程分类,首页并发比较高,导致可能分类查询机率非常大,并且课程分类的数据不会经常变,没必要每次访问都重新去Mysql查询一次课程分类,我们可以考虑做缓存。
3、缓存服务搭建
3.1、导入依赖
<!--整合Redis , 底层可以用jedis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
这里需要排除:lettuce-core包,在高并发时,会存在问题,所以我们采用jedis
3.2、yml配置Redis
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
jedis:
pool:
max-wait: 2000ms
min-idle: 2
max-idle: 8
3.3、Redis的序列化配置
将数据存入Redis时,他会采用默认的序列化方式进行序列化,这样的序列化方式不是很好,我们需要修改一下,我们通常以JSON格式将数据存储到Redis中,这种格式是所有编程语言通用的,所以我们可以把Redis的序列化方式配置为JSON,这样的话我们就可以不用自己去转JSON了
package cn.itsource.config.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @description: Redis缓存配置
*/
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
@Autowired
private RedisConnectionFactory factory;
/**
* 向Spring环境中声明一个 RedisTemplate 对象
*
* Redis默认使用 JdkSerializationRedisSerializer 对象进行序列化,可能会产生16进制的数据(看起来像乱码),被序列化的对象必须实现Serializable接口
*
* 为了方便我们查看,我们可以使用 JacksonJsonRedisSerializer 或GenericJackson2JsonRedisSerializer
* 上面两者都能序列化成JSON,但是后者会在JSON中加入@class属性,类的全路径包名,方便反系列化。
* 前者如果存放了List,则在反系列化的时候如果没指定TypeReference,会报错java.util.LinkedHashMap cannot be cast to xxxxx(某dto)
* 原因:序列化带泛型的数据时,会以map的结构进行存储,反序列化是不能将map解析成对象。
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
//序列化器
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//String数据key的序列化
redisTemplate.setKeySerializer(genericJackson2JsonRedisSerializer);
//String数据value的序列化
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
//hash结构key的序列化
redisTemplate.setHashKeySerializer(genericJackson2JsonRedisSerializer);
//hash结构value的序列化
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
return redisTemplate;
}
//缓存解析器
@Bean
@Override
public CacheResolver cacheResolver() {
return new SimpleCacheResolver(cacheManager());
}
//缓存管理器
@Bean
public CacheManager cacheManager() {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues() //不缓存null
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));//存到Redis的数据使用JSON进行序列化,方便我们查看
return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
}
}
3.4、扩展
没有序列化的数据存到Redis中的话,会显示成乱码,实际上这些乱码就是这些数据的16进制
出现这个问题的原因是,Java是一个基于对象的语言,而Redis是基于字符串存储的NoSql数据库,对象是无法存储到Redis中的
解决方案:
Java提供了序列化机制,只要类实现了java.io.Serializable接口,就代表类的对象能够进行序列化,通过将类对象序列化,就能够得到二进制的字符了,这样Redis就可以将这些类对象的字符串进行存储了,Java也可以将这些数据取出来反序列化成对象。
乱码问题重现:
先启动Redis,然后到Redis安装目录双击运行:redis-cli.exe,
输入密码:auth 123456
设置值:set k1 ‘中国’
获取值:get k1
但是结果却是乱码:
此时可以这样操作:
在Redis目录上面直接打开cmd黑窗口,然后输入命令:
redis-cli.exe --raw
然后再取值就没有乱码,可以正常显示中文了:
4、分类缓存实现
4.1、实现思路
- 查询Redis中是否有课程分类
- 如果有就取Redis数据,进行TreeData处理并返回
- 如果没有就从Mysql查询课程分类
- 把课程分类存储到Redis
- 把课程分类进行TreeData处理并返回
4.2、代码实现
将什么样的数据放入缓存呢?有两种方案:
- 数据库中的数据
- 把数据库中的数据处理过后的树状结构数据
不管存那种数据都可以,根据实际需求定,也可以两种类型的数据都缓存进去
有些场景可能用到树状结构数据,但有些场景需要非树状结构,我们这里以缓存非树状结构数据为例,代码如下:
@Override
public JSONResult treeData() {
//1、先从Redis中查询课程分类数据
List<CourseType> redisResult = (List<CourseType>) redisTemplate.opsForValue().get(BaseConstants.CourseConstants.COURSE_TYPE_REDIS_KEY);
//装一级分类
List<CourseType> firstCourseTypes = new ArrayList<>();
if(redisResult != null && redisResult.size() > 0){
log.info("从Redis取数据");
//Redis有数据
firstCourseTypes = redisResult;
}else{
log.info("从Mysql取数据");
//Redis没有数据
//先查询所有分类
List<CourseType> allCourseTypes = super.selectList(null);
for (CourseType courseType : allCourseTypes) {
if(courseType.getPid() == null || courseType.getPid().longValue() == 0){
//查找一级分类
firstCourseTypes.add(courseType);
}
else{
//非一级:二级、三级、四级、N级
//此时需要找自己的上级,再次遍历所有数据,根据pId查找id相等的记录,就查到上级了
for (CourseType parentCourseType : allCourseTypes) {
if(courseType.getPid().longValue() == parentCourseType.getId().longValue()){
//此时就找到了上级,那么就将自己信息添加到上级的children
parentCourseType.getChildren().add(courseType);
//已经找到上级了,不需要继续查找了,所以break
break;
}
}
}
}
//数据存Redis
redisTemplate.opsForValue().set(BaseConstants.CourseConstants.COURSE_TYPE_REDIS_KEY, firstCourseTypes);
}
return JSONResult.success(firstCourseTypes);
}
4.3、缓存清除
如果对课程分类信息进行新增、删除、更新操作的话,那么我们Redis中的缓存就需要删除,下次再查询的时候,就又会添加一份新的全的课程分类信息到Redis中了,代码如下:
@Override
public boolean insert(CourseType entity) {
boolean result = super.insert(entity);
//删除缓存
redisTemplate.delete(BaseConstants.RedisConstants.KEY_COURSE_TYPE);
return result;
}
@Override
public boolean deleteById(Serializable id) {
boolean result = super.deleteById(id);
//删除缓存
redisTemplate.delete(BaseConstants.RedisConstants.KEY_COURSE_TYPE);
return result;
}
@Override
public boolean update(CourseType entity, Wrapper<CourseType> wrapper) {
boolean result = super.update(entity, wrapper);
//删除缓存
redisTemplate.delete(BaseConstants.RedisConstants.KEY_COURSE_TYPE);
return result;
}
新增数据时可能发生:
- 删除Redis缓存
- 有人查询
- insert新增
这是非常经典的一个问题:怎么保证Redis和数据库中数据的一致性?
这也是面试经常问的
上面的问题我们可以改成下面的方案:
- 先insert新增
- 删除Redis缓存
这种解决方案可以很大程度的解决脏数据问题,如下图:
5、使用SpringCacha实现缓存
Spring提供了一些注解帮助我们简化了对缓存的使用,如下:
- @Cacheable:触发缓存写入。
- @CacheEvict:触发缓存清除。
- @CachePut:更新缓存(不会影响到方法的运行)。
- @Caching:重新组合要应用于方法的多个缓存操作。
- @CacheConfig:设置类级别上共享的一些常见缓存设置。
5.1、集成SpringCache
5.1.1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--springboot2.0以后data-redis底层使用的是lettuce,可以更换成jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
5.1.2、开启Cache
在启动类打上注解:@EnableCaching
@SpringBootApplication
@EnableEurekaClient
@EnableCaching //开启缓存注解
public class CourseApp1070 {
public static void main(String[] args) {
SpringApplication.run(CourseApp1070.class);
}
}
5.1.3、配置Redis
见上面3.3章节
5.1.4、yml配置
见上面3.2章节
5.2、@Cacheable
5.2.1、作用
这个注解作用在某个方法上,干了两件事:
- 将方法的返回结果直接缓存到Redis中
- 后续再调用该方法时,直接会自动从缓存中取数据返回,不必再执行实际的方法
5.2.1、用法
具体用法如下:
@Cacheable(cacheNames="books",key="'book1'")
public Book findBook(ISBN isbn) {...}
一个方法可以对应多个缓存名称,如下:
@Cacheable(cacheNames={"books", "isbns"},key="'book1'")//最终拼成一个key
public Book findBook(ISBN isbn) {...}
@Cacheable的缓存名称是可以配置动态参数的(key的值其实不是一个字符串,是一个SpEL表达式),比如选择传入的参数,如下:
@Cacheable(cacheNames="books", key="#isbn") //参数值作为Key
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable还可以设置根据条件判断是否需要缓存
- condition:取决于给定的参数是否满足条件
- unless:取决于返回值是否满足条件
例子:
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
@Cacheable(cacheNames="book",condition="#name.length()<32", unless="#result.hardback")
public Book findBook(String name)
5.3、改写之前代码
//从缓存或者数据库中查询所有课程分类数据
@Override
@Cacheable(cacheNames = "course_type", key="'course_type_tree_data'")
public JSONResult treeData() {
//先查询所有课程分类数据
List<CourseType> allCourseTypes = super.selectList(null);
//装一级分类
List<CourseType> firstCourseTypes = new ArrayList<>();
//把所有分类存到一个HashMap中
HashMap<Long, CourseType> allCourseTypesMaps = new HashMap<>(allCourseTypes.size());
for (CourseType obj : allCourseTypes) {
allCourseTypesMaps.put(obj.getId(), obj);
}
for (CourseType courseType : allCourseTypes) {
if(courseType.getPid() == null || courseType.getPid().longValue() == 0){
//查找一级分类
firstCourseTypes.add(courseType);
}
else{
//非一级分类,肯定有父分类
CourseType parentType = allCourseTypesMaps.get(courseType.getPid());
parentType.getChildren().add(courseType);
}
}
return JSONResult.success(firstCourseTypes);
}
再次测试就可以了
5.3、@CacheEvict用法
为了保证Redis数据和数据库数据的一致性,我们需要在数据新增/删除/编辑操作时,清空Redis缓存,此时,我们需要用@CacheEvict注解,具体用法如下:
@CacheEvict(cacheNames = "course_type", key = "'course_type_tree_data'")
public boolean deleteById(Serializable id) {
return super.deleteById(id);
}
这样方法执行完之后,就会删除对应的缓存信息了。
如果想在方法执行前就清空缓存的话,可以设置beforeInvocation属性为true即可,如下:
@CacheEvict(cacheNames = "course_type", key = "'course_type_tree_data'", beforeInvocation = true)
public boolean deleteById(Serializable id) {
return super.deleteById(id);
}
6、课程添加
6.1、页面设计
新增页面:
这里的数据需要保存到三张表 ,基本信息保存到t_course ,营销信息保存到t_course_marker ,课程详情保存到t_course_detail
6.2、课程表设计
按照不同的维度对课程垂直分表
- t_course:保存课程基本信息
- t_course_resource:保存课程资源,比如相册
- t_course_market:保存课程营销信息
- t_course_detail:保存课程详情
6.3、课程类型问题
解决办法:
在CourseType这个实体类里的children字段上面添加一个@JsonInclude注解,然后指定JsonInclude.Include.NON_EMPTY,这样空值就可以忽略了
代码如下:
@JsonInclude(JsonInclude.Include.NON_EMPTY)
6.4、代码实现
6.4.1、前端代码
//查询课程等级
getGrades(){
this.$http.get("/system/systemdictionarydetail/listBySn/dj").then(result=>{
this.grades = result.data.data;
});
},
//查询课程类型
getCourseTypes(){
this.$http.get("/course/courseType/treeData").then(result=>{
this.courseTypes = result.data.data;
});
},
6.4.2、后台代码
后台SystemdictionarydetailController需要编写接口listBySn,如下:
/**
* 根据字典编号查询字典详情
*/
@GetMapping(value = "/listBySn/{sn}")
public JSONResult listBySn(@PathVariable(value = "sn") String sn){
return JSONResult.success(systemdictionarydetailService.listBySn(sn));
}
对应的service实现类如下:
@Override
public List<Systemdictionarydetail> listBySn(String sn) {
return baseMapper.listBySn(sn);
}
xml里的SQL如下:
<select id="listBySn" resultType="cn.itsource.hrm.domain.Systemdictionarydetail">
select
t1.dic_key, t1.dic_value
from t_systemdictionarydetail t1
left join t_systemdictionarytype t2 on t1.type_id = t2.id
where t2.sn = #{sn}
</select>
6.5、驼峰自动转换
如果查询数据为空的话,说明上面字段没有对应上,这里我们需要设置一下驼峰字段转换,在yml配置文件中添加下面配置:
#MyBatis-Plus相关配置
mybatis-plus:
#指定Mapper.xml路径,如果与Mapper路径相同的话,可省略
mapper-locations: classpath:cn/itsource/hrm/mapper/*Mapper.xml
configuration:
map-underscore-to-camel-case: true #开启驼峰大小写自动转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启控制台sql输出
这里课程添加的信息需要保存到三张表,所以自动生成的save方法的入参不合适,我们需要自己定义一个DTO,里面分别放三个对象,如下:
@PostMapping(value="/save")
public JSONResult save(@RequestBody CourseAddDto dto){
courseService.save(dto);
return JSONResult.success();
}
CourseAddDto如下:
//课程新增时接收前端传来的相关信息
@Data
public class CourseAddDto {
//接收课程基本信息
private Course course;
//课程详情
private CourseDetail courseDetail;
//课程营销相关信息
private CourseMarket courseMarket;
}
然后重写save方法,实现类如下:
@Override
public void saveOrUpdate(CourseAddDto dto) {
//这里省去相关入参校验
Course course = dto.getCourse();
Long courseId = course.getId();
//这里模拟从Redis中获取的用户ID、用户姓名、所属机构ID和机构名称
Long user_id = 1L;
String user_name = "yhptest1";
Long tenant_id = 27L;
String tenant_name = "老面牌SPA";
//设置到course对象中
course.setTenantId(tenant_id);
course.setTenantName(tenant_name);
course.setUserId(user_id);
course.setUserName(user_name);
//获取课程详情
CourseDetail courseDetail = dto.getCourseDetail();
//获取课程营销信息
CourseMarket courseMarket = dto.getCourseMarket();
//如果课程ID不为空,那就做更新操作,否则就做新增操作
if(courseId != null){
baseMapper.updateById(course);
courseDetail.setId(courseId);
courseDetailMapper.updateById(courseDetail);
courseMarket.setId(courseId);
courseMarketMapper.updateById(courseMarket);
}
else{
baseMapper.insert(course);
courseDetail.setId(course.getId());
courseDetailMapper.insert(courseDetail);
courseMarket.setId(course.getId());
courseMarketMapper.insert(courseMarket);
}
}
列表显示如下:
我们发现时间显示是有问题的,那么怎么解决呢?
6.6、日期时间格式化
6.6.1、局部格式化(Date)
在实体类中,哪些字段需要进行时间格式化的话,加上@JsonFormat注解即可:
@TableField("start_time")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
private Date startTime;
用上面方式可以解决问题,但是如果每个实体类都需要这样打注解的话,会非常繁琐
此时我们可以使用全局日期时间格式化,看下面介绍
6.6.2、全局格式化(推荐)(Date)
在需要全局格式化的SpringBoot项目中,新建如下配置类:
package cn.itsource.config.date;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.context.annotation.Bean;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;
/**
* @description: 日期时间全局格式化
* @auth: wujiangbo
* @date: 2022-01-21 16:38
*/
@JsonComponent
public class LocalDateTimeSerializerConfig {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String pattern;
/**
* Date 类型全局时间格式化
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilder() {
return builder -> {
TimeZone tz = TimeZone.getTimeZone("UTC");//获取时区
DateFormat df = new SimpleDateFormat(pattern);//设置格式化模板
df.setTimeZone(tz);
builder.failOnEmptyBeans(false)
.failOnUnknownProperties(false)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.dateFormat(df);
}; }
/**
* LocalDate 类型全局时间格式化
*/
@Bean
public LocalDateTimeSerializer localDateTimeDeserializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> builder.serializerByType(LocalDateTime.class, localDateTimeDeserializer());
}
}
就这样就OK了,但是人会问,这样设置的话,那不就所有的字段都格式化成:yyyy-MM-dd HH:mm:ss 格式了吗,万一我需要展示:yyyy-MM-dd 格式的呢?
不要慌,这时就可以配合第一种@JsonFormat(timezone = “GMT+8”,pattern = “yyyy-MM-dd”)联合使用了,哪些字段需要特殊对待的,就可以单独使用这个@JsonFormat注解进行处理了
nice,非常好用
6.6.3、全局格式化(推荐)(LocalDateTime)
如果你的实体类中日期字段类型为【LocalDateTime】或【LocalDate】,那么全局格式化配置类就不一样了,只需要在项目中新增下面配置类即可:
package cn.itsource.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@Configuration
public class JacksonConfig {
/** 默认日期时间格式 */
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/** 默认日期格式 */
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
/** 默认时间格式 */
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
@Bean
public ObjectMapper objectMapper(){
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
javaTimeModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
objectMapper.registerModule(javaTimeModule).registerModule(new ParameterNamesModule());
return objectMapper;
}
}
如果你想对某个字段单独格式化后向前端传输的话,需要使用【@JsonSerialize】注解,使用方式如下:
@JsonSerialize(using = CustomLocatDateSerializer.class)
private LocalDateTime startTime;
需要自定义一个类:CustomLocatDateSerializer
package cn.itsource.date;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CustomLocatDateSerializer extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime value,
JsonGenerator gen,
SerializerProvider serializers) throws IOException {
if (value != null) {
gen.writeString(value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
}
}
扩展:如果想对其他字段也格式化输出的话,可以这样做:
@JsonSerialize(using = StatusSerialize.class)
private Integer status;
package cn.itsource.config.serialize;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class StatusSerialize extends JsonSerializer<Integer> {
@Override
public void serialize(Integer value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
String str = "";
if (value != null) {
//课程状态,下线:0 , 上线:1
if(value == 0){
str = "已下线了";
}
else if(value == 1){
str = "已上线了";
}
else {
str = "未知状态";
}
}
gen.writeString(str);
}
}
6.7、编辑回显
做编辑回显时,我们发现前端数据做回显时,数据不够,所以我们后台pageList方法中需要将所有数据全部查询出来返回给前端进行回显,这里我们介绍两种方式,大家任选其一即可:
- 后台新建CourseShowDto,包含t_course、t_course_detail、t_course_market三表的所有字段
- 还是复用上面的CourseAddDto,将数据全部查询出来封装到CourseAddDto中返回即可
1、方式1-新建CourseShowDto(便于理解)
新建CourseShowDto,代码:
package cn.itsource.hrm.dto;
import lombok.Data;
import java.util.Date;
@Data
public class CourseShowDto {
private Long id;
/**
* 课程名称
*/
private String name;
/**
* 适用人群
*/
private String forUser;
/**
* 课程分类
*/
private Long courseTypeId;
private String gradeName;
/**
* 课程等级
*/
private Long gradeId;
/**
* 课程状态,下线:0 , 上线:1
*/
private Integer status;
/**
* 教育机构
*/
private Long tenantId;
private String tenantName;
/**
* 添加课程的后台用户的ID
*/
private Long userId;
/**
* 添加课程的后台用户
*/
private String userName;
/**
* 课程的开课时间
*/
private Date startTime;
/**
* 课程的节课时间
*/
private Date endTime;
/**
* 封面
*/
private String pic;
private Integer saleCount;
private Integer viewCount;
/**
* 评论数
*/
private Integer commentCount;
private Date onlineTime;
private Date offlineTime;
/**
* 收费规则:,收费1免费,2收费
*/
private Integer charge;
/**
* 营销截止时间
*/
private Date expires;
/**
* 咨询qq
*/
private String qq;
/**
* 价格
*/
private Float price;
/**
* 原价
*/
private Float priceOld;
/**
* 详情
*/
private String description;
/**
* 简介
*/
private String intro;
}
对自动生成的pageList方法改造,如下:
@PostMapping(value = "/pagelist")
public JSONResult pageList(@RequestBody CourseQuery query)
{
Page<CourseAddDto> page = courseService.selectMyPage(query);
return JSONResult.success(new PageList<CourseAddDto>(page.getTotal(), page.getRecords()));
}
service方法如下:
Page<CourseAddDto> selectMyPage(CourseQuery query);
实现类如下:
@Override
public Page<CourseDto> selectMyPage(CourseQuery query) {
Page<CourseDto> page = new Page<>(query.getPage(), query.getRows());
List<CourseDto> courseAddDtoList = courseMapper.selectMyPage(page, query.getKeyword());
return page.setRecords(courseAddDtoList);
}
Mapper类如下:
List<CourseDto> selectMyPage(Page<CourseDto> page, @Param("keyword") String keyword);
xml代码如下:
<select id="selectMyPage" resultType="cn.itsource.hrm.dto.CourseShowDto">
select t1.*, t2.*, t3.* from t_course t1
left join t_course_detail t2 on t1.id = t2.id
left join t_course_market t3 on t1.id = t3.id
<where>
<if test="keyword != null and keyword != '' ">
and t1.name like concat('%' , #{keyword} ,'%')
</if>
</where>
order by t1.id desc
</select>
前端代码回显:
//编辑回显
editRow(row){
this.addFormVisible = true;//显示编辑框
this.addForm = row;
},
2、方式2-复用CourseAddDto(更高级点)
对自动生成的pageList方法改造,如下:
@PostMapping(value = "/pagelist")
public JSONResult pageList(@RequestBody CourseQuery query)
{
Page<CourseAddDto> page = courseService.selectMyPage(query);
return JSONResult.success(new PageList<CourseAddDto>(page.getTotal(), page.getRecords()));
}
service方法如下:
Page<CourseAddDto> selectMyPage(CourseQuery query);
实现类如下:
@Override
public Page<CourseDto> selectMyPage(CourseQuery query) {
Page<CourseDto> page = new Page<>(query.getPage(), query.getRows());
List<CourseDto> courseAddDtoList = courseMapper.selectMyPage(page, query.getKeyword());
return page.setRecords(courseAddDtoList);
}
Mapper类如下:
List<CourseDto> selectMyPage(Page<CourseDto> page, @Param("keyword") String keyword);
xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.itsource.hrm.mapper.CourseMapper">
<!-- 通用查询映射结果 -->
<resultMap id="ResultMap" type="cn.itsource.hrm.dto.CourseAddDto">
<!--Course对象映射-->
<association property="course" javaType="cn.itsource.hrm.domain.Course">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="for_user" property="forUser" />
<result column="course_type_id" property="courseTypeId" />
<result column="grade_name" property="gradeName" />
<result column="grade_id" property="gradeId" />
<result column="status" property="status" />
<result column="tenant_id" property="tenantId" />
<result column="tenant_name" property="tenantName" />
<result column="user_id" property="userId" />
<result column="user_name" property="userName" />
<result column="start_time" property="startTime" />
<result column="end_time" property="endTime" />
<result column="pic" property="pic" />
<result column="sale_count" property="saleCount" />
<result column="view_count" property="viewCount" />
<result column="comment_count" property="commentCount" />
<result column="online_time" property="onlineTime" />
<result column="offline_time" property="offlineTime" />
</association>
<!--CourseMarket对象映射-->
<association property="courseMarket" javaType="cn.itsource.hrm.domain.CourseMarket">
<id column="id" property="id" />
<result column="charge" property="charge" />
<result column="expires" property="expires" />
<result column="qq" property="qq" />
<result column="price" property="price" />
<result column="price_old" property="priceOld" />
</association>
<!--CourseDetail对象映射-->
<association property="courseDetail" javaType="cn.itsource.hrm.domain.CourseDetail">
<id column="id" property="id" />
<result column="description" property="description" />
<result column="intro" property="intro" />
</association>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, name, for_user AS forUser, course_type_id AS courseTypeId, grade_name AS gradeName, grade_id AS gradeId, status, tenant_id AS tenantId, tenant_name AS tenantName, user_id AS userId, user_name AS userName, start_time AS startTime, end_time AS endTime, pic, sale_count AS saleCount, view_count AS viewCount, comment_count AS commentCount, online_time AS onlineTime, offline_time AS offlineTime
</sql>
<select id="selectMyPage" resultMap="ResultMap">
select t1.*, t2.*, t3.* from t_course t1
left join t_course_detail t2 on t1.id = t2.id
left join t_course_market t3 on t1.id = t3.id
<where>
<if test="keyword != null and keyword != '' ">
and t1.name like concat('%' , #{keyword} ,'%')
</if>
</where>
order by t1.id desc
</select>
</mapper>
前端代码回显:
//编辑回显
editRow(row){
this.addFormVisible = true;//显示编辑框
this.addForm = row.course;
this.addForm.chargeId = row.courseMarket.charge;
this.addForm.expires = row.courseMarket.expires;
this.addForm.qq = row.courseMarket.qq;
this.addForm.price = row.courseMarket.price;
this.addForm.priceOld = row.courseMarket.priceOld;
this.addForm.description = row.courseDetail.description;
this.addForm.intro = row.courseDetail.intro;
},
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/188518.html