缓存服务使用总结

1、介绍

  1. 分布式缓存方案
  2. 缓存服务搭建

2、分布式缓存方案(重点掌握)

2.1、什么是缓存

通常将数据从数据库中同步一份到内存中,客户端直接从内存中查询数据,减少了和数据库的交互次数,提高查询性能(因为内存读写很快),减轻数据库的压力

缓存服务使用总结_第1张图片

2.2、哪些数据适合缓存

  1. 经常查询的热点数据
  2. 不经常变的数据(数据变化会导致缓存中的数据跟着变,如果比较频繁,性能开销比较大)

2.3、缓存的流程

  1. 第一次查询,先看缓存是否有数据 , 如果有:直接返回
  2. 如果缓存没有数据,去数据库查询数据
  3. 把数据同步一份到缓存
  4. 返回数据

注意:数据库数据被修改,缓存要清空,或者重置

2.4、传统缓存方案及流程

缓存服务使用总结_第2张图片

缺点:

  1. 在集群环境中,每个应用都有一个本地缓存,当缓存发生修改会造成缓存不同步问题
  2. 本地缓存本身要占用应用的内存空间

2.5、分布式缓存方案

缓存服务使用总结_第3张图片

优点:

  1. 使用Redis作为共享缓存 ,解决缓存不同步问题
  2. Redis是独立的服务,缓存不用占应用本身的内存空间

2.6、为什么要缓存课程分类

门户首页需要展示课程分类,首页并发比较高,导致可能分类查询机率非常大,并且课程分类的数据不会经常变,没必要每次访问都重新去Mysql查询一次课程分类,我们可以考虑做缓存。

3、缓存服务搭建

3.1、导入依赖


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettucegroupId>
            <artifactId>lettuce-coreartifactId>
        exclusion>
    exclusions>
dependency>

<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
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

但是结果却是乱码:

缓存服务使用总结_第4张图片

此时可以这样操作:

在Redis目录上面直接打开cmd黑窗口,然后输入命令:

redis-cli.exe --raw

然后再取值就没有乱码,可以正常显示中文了:

缓存服务使用总结_第5张图片

4、分类缓存实现

4.1、实现思路

  1. 查询Redis中是否有课程分类
  2. 如果有就取Redis数据,进行TreeData处理并返回
  3. 如果没有就从Mysql查询课程分类
  4. 把课程分类存储到Redis
  5. 把课程分类进行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;
}

新增数据时可能发生:

  1. 删除Redis缓存
  2. 有人查询
  3. insert新增

缓存服务使用总结_第6张图片

这是非常经典的一个问题:怎么保证Redis和数据库中数据的一致性?

这也是面试经常问的

上面的问题我们可以改成下面的方案:

  1. 先insert新增
  2. 删除Redis缓存

这种解决方案可以很大程度的解决脏数据问题,如下图:

缓存服务使用总结_第7张图片

5、使用SpringCacha实现缓存

Spring提供了一些注解帮助我们简化了对缓存的使用,如下:

  1. @Cacheable:触发缓存写入。
  2. @CacheEvict:触发缓存清除。
  3. @CachePut:更新缓存(不会影响到方法的运行)。
  4. @Caching:重新组合要应用于方法的多个缓存操作。
  5. @CacheConfig:设置类级别上共享的一些常见缓存设置。

5.1、集成SpringCache

5.1.1、导入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettucegroupId>
            <artifactId>lettuce-coreartifactId>
        exclusion>
    exclusions>
dependency>


<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
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、作用

这个注解作用在某个方法上,干了两件事:

  1. 将方法的返回结果直接缓存到Redis中
  2. 后续再调用该方法时,直接会自动从缓存中取数据返回,不必再执行实际的方法

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、页面设计

缓存服务使用总结_第8张图片

新增页面:

缓存服务使用总结_第9张图片

缓存服务使用总结_第10张图片

缓存服务使用总结_第11张图片

这里的数据需要保存到三张表 ,基本信息保存到t_course ,营销信息保存到t_course_marker ,课程详情保存到t_course_detail

6.2、课程表设计

按照不同的维度对课程垂直分表

  1. t_course:保存课程基本信息
  2. t_course_resource:保存课程资源,比如相册
  3. t_course_market:保存课程营销信息
  4. t_course_detail:保存课程详情

缓存服务使用总结_第12张图片

6.3、课程类型问题

缓存服务使用总结_第13张图片

解决办法:

在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);
    }
}

列表显示如下:

缓存服务使用总结_第14张图片

我们发现时间显示是有问题的,那么怎么解决呢?

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方法中需要将所有数据全部查询出来返回给前端进行回显,这里我们介绍两种方式,大家任选其一即可:

  1. 后台新建CourseShowDto,包含t_course、t_course_detail、t_course_market三表的所有字段
  2. 还是复用上面的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如下:


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">
        
        <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>

        
        <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>

        
        <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;
},

你可能感兴趣的:(微服务,缓存)