Resumen del uso del servicio de caché

1. Introducción

  1. Solución de caché distribuida
  2. Construcción del servicio de caché

2. Solución de caché distribuida (clave maestra)

2.1, que es caché

Por lo general, una copia de los datos se sincroniza desde la base de datos a la memoria, y el cliente consulta directamente los datos de la memoria, lo que reduce la cantidad de interacciones con la base de datos, mejora el rendimiento de las consultas (porque la memoria lee y escribe rápidamente), y reduce la presión sobre la base de datos

inserte la descripción de la imagen aquí

2.2 Qué datos son adecuados para el almacenamiento en caché

  1. Datos calientes consultados con frecuencia
  2. Cambiar datos con poca frecuencia (los cambios de datos harán que los datos en el caché cambien en consecuencia, si es más frecuente, la sobrecarga de rendimiento será relativamente grande)

2.3, el proceso de almacenamiento en caché

  1. Para la primera consulta, primero verifique si hay datos en el caché, si es así: regrese directamente
  2. Si no hay datos en el caché, vaya a la base de datos para consultar los datos
  3. Sincronizar una copia de los datos en el caché
  4. devolver datos

Nota: Los datos de la base de datos se modifican, el caché debe borrarse o restablecerse

2.4 Esquema y proceso de almacenamiento en caché tradicional

inserte la descripción de la imagen aquí

defecto:

  1. En un entorno de clúster, cada aplicación tiene un caché local. Cuando se modifica el caché, el caché no estará sincronizado.
  2. El propio caché local ocupa el espacio de memoria de la aplicación

2.5 Solución de caché distribuida

inserte la descripción de la imagen aquí

ventaja:

  1. Use Redis como un caché compartido para resolver el problema del caché no sincronizado
  2. Redis es un servicio independiente y el caché no ocupa el espacio de memoria de la aplicación en sí.

2.6 ¿Por qué se debe almacenar en caché la clasificación del curso?

La página de inicio del portal debe mostrar la clasificación del curso, y la página de inicio tiene una concurrencia relativamente alta, lo que conduce a una posibilidad muy alta de consulta de clasificación, y los datos de la clasificación del curso no cambiarán con frecuencia, por lo que no hay necesidad de vaya a Mysql para consultar la clasificación del curso nuevamente cada vez que visite, podemos considerar el almacenamiento en caché.

3. Construcción del servicio de caché

3.1 Dependencias de importación

<!--整合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>

Debe descartarse aquí: paquete de núcleo de lechuga, habrá problemas en alta concurrencia, por lo que usamos jedis

3.2, configuración 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, configuración de serialización de Redis

Al almacenar datos en Redis, utilizará el método de serialización predeterminado para la serialización. Este método de serialización no es muy bueno. Necesitamos modificarlo. Generalmente almacenamos datos en Redis en formato JSON. Este formato es todo El lenguaje de programación es universal, para que podamos configurar el método de serialización de Redis como JSON, de modo que no necesitemos convertir JSON por nosotros mismos

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 Extensión

Si los datos no serializados se almacenan en Redis, se mostrarán como caracteres ilegibles. De hecho, estos caracteres ilegibles son los números hexadecimales de estos datos.

El motivo de este problema es que Java es un lenguaje basado en objetos y Redis es una base de datos NoSql basada en el almacenamiento de cadenas y los objetos no se pueden almacenar en Redis.

solución:

Java proporciona un mecanismo de serialización. Siempre que una clase implemente la interfaz java.io.Serializable, significa que los objetos de la clase se pueden serializar. Al serializar los objetos de la clase, se pueden obtener caracteres binarios para que Redis pueda convertirlos. clases La cadena del objeto se almacena, y Java también puede sacar los datos y deserializarlos en un objeto.

El problema distorsionado se reproduce:

Inicie Redis primero, luego vaya al directorio de instalación de Redis y haga doble clic para ejecutar: redis-cli.exe,

Ingrese la contraseña: autenticación 123456

Valor de configuración: establecer k1 'China'

Obtener valor: obtener k1

Pero el resultado es confuso:

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-leeching, se recomienda guardar la imagen y subirla directamente (img-q48gFcbu-1689516159233)(images/image-20220117130028387.png)]

En este punto puedes hacer esto:

Abra la ventana negra cmd directamente encima del directorio Redis y luego ingrese el comando:

redis-cli.exe --raw

Entonces no habrá caracteres ilegibles cuando se tome el valor nuevamente, y el chino se puede mostrar normalmente:

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-leeching, se recomienda guardar la imagen y subirla directamente (img-7pVcZMtf-1689516159233)(images/image-20220117130341612.png)]

4. Implementación de caché clasificada

4.1 Ideas de implementación

  1. Consultar si hay una categoría de curso en Redis
  2. Si los hay, tome los datos de Redis, realice el procesamiento de TreeData y devuelva
  3. Si no, consulta la clasificación del curso desde Mysql
  4. Almacenar la clasificación del curso en Redis
  5. Clasificar cursos en TreeData y regresar

4.2, implementación de código

¿Qué tipo de datos se colocan en el caché? Hay dos opciones:

  • datos en la base de datos
  • Los datos de la estructura de árbol después de procesar los datos en la base de datos.

No importa qué tipo de datos se puedan almacenar, de acuerdo con las necesidades reales, ambos tipos de datos también se pueden almacenar en caché.

Algunos escenarios pueden usar datos estructurados en árbol, pero algunos escenarios requieren datos no estructurados en árbol. Aquí tomamos como ejemplo el almacenamiento en caché de datos no estructurados en árbol. El código es el siguiente:

@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 Borrado de caché

Si la información de clasificación del curso se agrega, elimina y actualiza, entonces el caché en nuestro Redis debe eliminarse. Cuando consultemos la próxima vez, se agregará una nueva y completa información de clasificación del curso a Redis. El código se muestra a continuación:

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

Cuándo se pueden agregar nuevos datos:

  1. eliminar caché redis
  2. alguien pregunto
  3. insertar nuevo

inserte la descripción de la imagen aquí

Esta es una pregunta muy clásica: ¿cómo garantizar la coherencia de los datos en Redis y la base de datos?

Esto es lo que se suele preguntar en las entrevistas.

Podemos cambiar el problema anterior en la siguiente solución:

  1. primero inserte nuevo
  2. eliminar caché redis

Esta solución puede resolver en gran medida el problema de los datos sucios, como se muestra en la siguiente figura:

inserte la descripción de la imagen aquí

5. Usa Spring Cacha para implementar el almacenamiento en caché

Spring proporciona algunas anotaciones para ayudarnos a simplificar el uso de la memoria caché, de la siguiente manera:

  1. @Cacheable: desencadena escrituras en caché.
  2. @CacheEvict: activar la limpieza de caché.
  3. @CachePut: actualice el caché (no afectará la operación del método).
  4. @Caching: reagrupa varias operaciones de almacenamiento en caché para aplicarlas a un método.
  5. @CacheConfig: establece algunas configuraciones de caché comunes compartidas en el nivel de clase.

5.1 Integrar SpringCache

5.1.1 Dependencias de importación

<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, abrir caché

Anote la clase de inicio: @EnableCaching

@SpringBootApplication
@EnableEurekaClient
@EnableCaching //开启缓存注解
public class CourseApp1070 {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(CourseApp1070.class);
    }
}

5.1.3, configurar Redis

Consulte la sección 3.3 anterior

5.1.4, configuración yml

Consulte la sección 3.2 anterior

5.2, @ cacheable

5.2.1 Función

Esta anotación actúa sobre un método y hace dos cosas:

  1. Guarde en caché el resultado de retorno del método directamente en Redis
  2. Cuando se llame a este método más adelante, los datos se recuperarán automáticamente del caché y se devolverán sin ejecutar el método real.

5.2.1 Uso

El uso específico es el siguiente:

@Cacheable(cacheNames="books",key="'book1'")
public Book findBook(ISBN isbn) {
    
    ...}

Un método puede corresponder a múltiples nombres de caché, de la siguiente manera:

@Cacheable(cacheNames={
    
    "books", "isbns"},key="'book1'")//最终拼成一个key
public Book findBook(ISBN isbn) {
    
    ...} 

El nombre de caché de @Cacheable se puede configurar con parámetros dinámicos (el valor de la clave no es una cadena, sino una expresión SpEL), como seleccionar los parámetros entrantes, de la siguiente manera:

@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 también se puede configurar para determinar si se requiere el almacenamiento en caché en función de las condiciones

  • condición: depende de si los parámetros dados cumplen las condiciones
  • a menos que: depende de si el valor devuelto cumple la condición

ejemplo:

@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 Reescribir el código anterior

//从缓存或者数据库中查询所有课程分类数据
@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);
}

Solo pruébalo de nuevo

5.3, uso de @CacheEvict

Para garantizar la coherencia de los datos de Redis y los datos de la base de datos, debemos borrar la memoria caché de Redis al agregar/eliminar/editar datos. En este momento, debemos usar la anotación @CacheEvict. El uso específico es el siguiente:

@CacheEvict(cacheNames = "course_type", key = "'course_type_tree_data'")
public boolean deleteById(Serializable id) {
    
    
    return super.deleteById(id);
}

Después de ejecutar este método, se eliminará la información de caché correspondiente.

Si desea borrar el caché antes de que se ejecute el método, puede establecer la propiedad beforeInvocation en verdadero, de la siguiente manera:

@CacheEvict(cacheNames = "course_type", key = "'course_type_tree_data'", beforeInvocation = true)
public boolean deleteById(Serializable id) {
    
    
    return super.deleteById(id);
}

6. Adición de cursos

6.1 Diseño de página

inserte la descripción de la imagen aquí

Nueva pagina:

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

inserte la descripción de la imagen aquí

Los datos aquí deben guardarse en tres tablas, la información básica se guarda en t_course, la información de marketing se guarda en t_course_marker y los detalles del curso se guardan en t_course_detail

6.2 Diseño Curricular

Divide verticalmente los cursos de acuerdo a diferentes dimensiones.

  1. t_course: guarda la información básica del curso
  2. t_course_resource: guarde los recursos del curso, como álbumes de fotos
  3. t_course_market: retener información de marketing del curso
  4. t_course_detail: retener los detalles del curso

[Falló la transferencia de la imagen del enlace externo, el sitio de origen puede tener un mecanismo anti-leeching, se recomienda guardar la imagen y subirla directamente (img-ATkWR6mX-1689516159236)(images/image-20210906162956788.png)]

6.3 Preguntas sobre tipos de cursos

inserte la descripción de la imagen aquí

Solución:

Agregue una anotación @JsonInclude en el campo secundario en la clase de entidad de CourseType y luego especifique JsonInclude.Include.NON_EMPTY, para que los valores vacíos se puedan ignorar

el código se muestra a continuación:

@JsonInclude(JsonInclude.Include.NON_EMPTY)

6.4, implementación de código

6.4.1, código de entrada

//查询课程等级
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, código de fondo

El SystemdictionarydetailController de fondo necesita escribir la interfaz listBySn, de la siguiente manera:

/**
* 根据字典编号查询字典详情
*/
@GetMapping(value = "/listBySn/{sn}")
public JSONResult listBySn(@PathVariable(value = "sn") String sn){
    
    
    return JSONResult.success(systemdictionarydetailService.listBySn(sn));
}

La clase de implementación de servicio correspondiente es la siguiente:

@Override
public List<Systemdictionarydetail> listBySn(String sn) {
    
    
    return baseMapper.listBySn(sn);
}

El SQL en xml es el siguiente:

<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, conversión automática de joroba

Si los datos de consulta están vacíos, significa que los campos anteriores no se corresponden, aquí debemos establecer la conversión del campo de joroba, agregue la siguiente configuración en el archivo de configuración 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输出

La información agregada en el curso aquí debe guardarse en tres tablas, por lo que los parámetros de entrada del método de guardado generado automáticamente no son adecuados. Necesitamos definir un DTO por nosotros mismos y colocar tres objetos en él, de la siguiente manera:

@PostMapping(value="/save")
public JSONResult save(@RequestBody CourseAddDto dto){
    
    
    courseService.save(dto);
    return JSONResult.success();
}

CourseAddDto es el siguiente:

//课程新增时接收前端传来的相关信息
@Data
public class CourseAddDto {
    
    

    //接收课程基本信息
    private Course course;

    //课程详情
    private CourseDetail courseDetail;

    //课程营销相关信息
    private CourseMarket courseMarket;
}

Luego reescriba el método save y la clase de implementación es la siguiente:

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

La lista aparece de la siguiente manera:

inserte la descripción de la imagen aquí

Descubrimos que hay un problema con la visualización de la hora, entonces, ¿cómo resolverlo?

6.6 Formato de fecha y hora

6.6.1, formateo parcial (Fecha)

En la clase de entidad, qué campos deben formatearse en el tiempo , simplemente agregue la anotación @JsonFormat:

@TableField("start_time")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
private Date startTime;

El método anterior puede resolver el problema, pero si cada clase de entidad debe anotarse de esta manera, será muy engorroso.

En este punto, podemos usar el formato de fecha y hora global, consulte la introducción a continuación.

6.6.2 Formato global (recomendado) (Fecha)

En el proyecto SpringBoot que requiere formato global, cree la siguiente clase de configuración:

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

Está bien así, pero la gente preguntará, si esto está configurado, entonces todos los campos tendrán el formato: aaaa-MM-dd formato HH:mm:ss, en caso de que necesite mostrar: aaaa-MM-dd ¿Qué pasa con ¿el formato?

No entre en pánico, puede usarlo con el primer @JsonFormat (zona horaria = "GMT+8", patrón = "yyyy-MM-dd"). Si algún campo necesita un tratamiento especial, puede usar este @JsonFormat solo. Las anotaciones son procesada

agradable, muy fácil de usar

6.6.3 Formato global (recomendado) (LocalDateTime)

Si el tipo de campo de fecha en su clase de entidad es [LocalDateTime] o [LocalDate], entonces la clase de configuración de formato global es diferente, simplemente agregue la siguiente clase de configuración al proyecto:

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

Si desea formatear un campo por separado y transmitirlo al front-end, debe usar la anotación [@JsonSerialize], de la siguiente manera:

@JsonSerialize(using = CustomLocatDateSerializer.class)
private LocalDateTime startTime;

Necesita personalizar una clase: 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")));
        }
    }
}

Extensión: si desea formatear la salida para otros campos, puede hacer esto:

@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, editar eco

Al editar y hacer eco, descubrimos que cuando los datos del front-end están haciendo eco, no hay suficientes datos, por lo que debemos consultar todos los datos en el método pageList de fondo y devolverlos al front-end para hacer eco. introduce dos métodos, y puedes elegir uno de ellos. Puedes:

  1. Cree un nuevo CourseShowDto en segundo plano, incluidos todos los campos de las tres tablas t_course, t_course_detail y t_course_market
  2. O reutilice el CourseAddDto anterior, consulte todos los datos, encapsúlelos en CourseAddDto y devuélvalos

1. Método 1: cree un nuevo CourseShowDto (fácil de entender)

Cree un nuevo CourseShowDto, código:

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

Transforme el método pageList generado automáticamente de la siguiente manera:

@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()));
}

El método de servicio es el siguiente:

Page<CourseAddDto> selectMyPage(CourseQuery query);

La clase de implementación es la siguiente:

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

La clase Mapper es la siguiente:

List<CourseDto> selectMyPage(Page<CourseDto> page, @Param("keyword") String keyword);

El código xml es el siguiente:

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

Eco de código de front-end:

//编辑回显
editRow(row){
    
    
    this.addFormVisible = true;//显示编辑框
    this.addForm = row;
},

2. Método 2 - Multiplexing CourseAddDto (punto más avanzado)

Transforme el método pageList generado automáticamente de la siguiente manera:

@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()));
}

El método de servicio es el siguiente:

Page<CourseAddDto> selectMyPage(CourseQuery query);

La clase de implementación es la siguiente:

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

La clase Mapper es la siguiente:

List<CourseDto> selectMyPage(Page<CourseDto> page, @Param("keyword") String keyword);

xml es el siguiente:

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

Eco de código de front-end:

//编辑回显
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;
},

Supongo que te gusta

Origin blog.csdn.net/qq_45525848/article/details/131755901
Recomendado
Clasificación