《瑞吉外卖》账号禁用功能引出SpringMVC消息转换器

在练习《瑞吉外卖》这个经典项目时,看到了这个经典的问题,觉得也还是挺有意思,虽然黑马的老师已经讲得挺清楚了,但是我还是想记录一下,就当做笔记了,最后也加入了一些我自己的思考。

问题引出:

目标效果:

先简单说一下要实现的效果:在员工列表,点击“禁用”后,将“账号状态”改为“禁用”,原本的“禁用”按钮变为“启用”按钮,数据库中的“status”由“1”修改为“0”.

代码实现:

实现方式比较简单,大部分工作由前端完成,后端接收前端的请求及数据,调用service修改员工信息即可。

 /**
     * 根据id修改员工信息
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
        Long empId = (Long) request.getSession().getAttribute("employee");
        //设置更新时间
        employee.setUpdateTime(LocalDateTime.now());

        //设置更新人
        employee.setUpdateUser(empId);
        employeeService.updateById(employee);
        return R.success("员工信息修改成功!");
    }

遇到的问题:

在实现员工账号禁用功能时,写好了controller,代码实现逻辑没有问题,可以正常运行,点击“确认禁用”后,网页也没有报错,但是禁用后,账号状态依然显示“正常”,数据库中员工状态“status”也还是“1”。

 问题探究:

这个问题要是自己遇到肯定非常麻烦,好在这次有老师领路,而且看得出老师是故意引出这个问题的。老师的探究过程就是打断点调试,一步一步发现问题:在后台传递id给前端时id与数据库中id一致,但是前端在发送请求时带的参数与数据库中不一致,后三位进行四舍五入了,导致后台拿到的id在数据库中查不到数据。

数据库中的正确id:

 前端传给后端的id:

 最后老师给出了原因:js在处理long型数据时丢失精度,导致提交的id与数据库中的id不一致。

解决方法:

老师也给出了解决方法: 

1.提供对象转换器JacksonObjectMapper,基于Jackson进行 java 对象到 json 数据的转换。(在common包中添加这个类,这部分是给好的,不用自己写):

package com.bin.reggie_take_out.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
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 java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

2.在WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行 java 对象到 json 数据的转换。

注:converters.add(0,messageConverter);这一步是将自己的转换器放在索引为0的位置,也就是第一位,否则还是会调用默认的转换器。

package com.bin.reggie_take_out.config;

import com.bin.reggie_take_out.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 扩展MVC框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //创建一个消息转化器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将java对象转换成json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到MVC框架的消息转换器集合中
        converters.add(0,messageConverter);
    }
}

如果之前一步步跟着老师的步骤做的,做完这两步问题就已经解决了。

 

但是我呢在静态资源那块偷懒了,没有跟老师一起在WebMvcConfig配置类中定义静态资源映射规则,而是直接将静态资源放在了static目录下,导致做完这两步(准确来说是第二步后),网页(index.html)无法打开了。

新的问题:

问题描述:

在做完上面的两步后,我的网页(index.html)打不开了。

问题的探究: 

 这个问题的原因也挺容易推断的,因为我的步骤除了静态资源那块与老师不同外,其他都一样,所以问题大概率是那部分,刚好看见视频有弹幕说了这个问题,于是定位到了静态资源这块。

解决方法:

这个问题有两种解决方法:

方法一:像老师一样,在WebMvcConfig配置类中定义静态资源映射规则(这样做需要直接将静态资源放在resource下):

package com.itzq.reggie.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射。。。");
        //访问路径
        registry.addResourceHandler("/backend/**")
                //映射到真实的路径(映射的真实路径末尾必须添加斜杠`/`)
                .addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**")
                .addResourceLocations("classpath:/front/");
    }
}

 方法二:将继承WebMvcConfigurationSupport(extends WebMvcConfigurationSupport)修改为实现WebMvcConfigurer(implements WebMvcConfigurer):

注:除修改类的方式外,还需要将方法由"protected"修改为"public",Idea也会提示。

package com.bin.reggie_take_out.config;

import com.bin.reggie_take_out.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer{

    /**
     * 扩展MVC框架的消息转换器
     * @param converters
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //创建一个消息转化器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将java对象转换成json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到MVC框架的消息转换器集合中
        converters.add(0,messageConverter);
    }
}

方法二更简单,而且将静态资源放在static目录下也是Spring boot官方推荐的做法,至于老师为什么使用自定义静态资源映射配置,我想可能是为了复习这个知识点,至于后续有没有作用暂时还不知道。

总结:

针对瑞吉外卖(其他项目也可能遇到)禁用账号(修改用户信息)失效问题。

原因是:js在处理long型数据时丢失精度,导致提交的id与数据库中的id不一致,以至于后台在数据库中查不到用户信息自然无法成功修。

解决方法:

1.提供对象转换器JacksonObjectMapper,基于Jackson进行 java 对象到 json 数据的转换。

2.在WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行 java 对象到 json 数据的转换。

思考:

1.用UUID算法是否可以避免这个问题?

答案是可以。

在雪花算法生成 id 时,它是将毫秒级时间戳左移22位,并通过或运算加入机器id和自增序列来生成唯一id。这样生成的id是一个 64 位的整型数据,而JavaScript中的数字类型为双精度浮点数,只能精确处理53位以内的整数,超过这一位数就会出现精度丢失的问题。

如果采用 UUID 作为唯一标识符,就能避免此问题。UUID 是由 32 个十六进制数字表示的 128 位数字,它不依赖于时钟的精度和时钟回拨的问题,因此可以避免在前后端数据交互中出现 long 类型数据丢失精度的问题。同时,它也能更好地保证数据的安全性和唯一性。

2.为什么不直接设置id自增而要用算法生成id?

使用 UUID 或者雪花算法生成唯一标识符的目的之一就是为了避免在并发添加用户时发生主键冲突的情况。

在直接设置 id 自增的情况下,当多个客户端同时进行插入操作时,有可能会导致并发冲突,同时插入相同的 id 值,进而导致插入操作失败。此时,在应用程序中需要对此进行异常处理,并进行重试或回滚,以保证数据的一致性。

而通过使用 UUID 或雪花算法生成唯一标识符,则可以避免这种情况。UUID 和雪花算法在生成唯一标识符时,几乎不会出现冲突的情况,因此可以避免并发冲突,提高数据的插入效率。

同时,使用 UUID 或雪花算法生成唯一标识符还可以提高数据的安全性。因为使用自增长的方式生成主键时,可能会导致其他人通过直接猜测主键来访问到不该访问的数据。而使用 UUID 或雪花算法,生成的主键是不可预测的,从而增强了数据的安全性。

3.extends WebMvcConfigurationSupport与implements WebMvcConfigurer的区别?

WebMvcConfigurationSupport 和 WebMvcConfigurer 都是用于扩展 Spring MVC 配置的类,但是它们有一些区别:

首先,WebMvcConfigurationSupport 是一个基于 Java 配置的 Spring MVC 配置类,用于扩展 Spring MVC 默认配置。通过继承该类,可以定制化 Spring MVC 的配置,例如添加拦截器、重写消息转换器等。

而 WebMvcConfigurer 是一个接口,定义了若干个回调方法,用于扩展 Spring MVC 配置。通过实现该接口,可以定制化 Spring MVC 的配置,例如添加拦截器、静态资源映射等。

其次,WebMvcConfigurationSupport 的自定义配置会覆盖 Spring Boot 默认配置,因此在使用该类自定义配置时需要注意。而 WebMvcConfigurer 的自定义配置不会覆盖 Spring Boot 默认配置,而是在其基础上进行扩展。

因此,通常情况下,建议使用 WebMvcConfigurer 进行配置扩展,这样可以避免意外覆盖 Spring Boot 的默认配置,并且能够与其他第三方库或框架更好地协作。只有在某些情况下需要完全覆盖 Spring MVC 默认配置时,才需要使用 WebMvcConfigurationSupport

4.WebMvcConfigurationSupport与WebMvcConfigurer使用时机与方法:

WebMvcConfigurationSupport 是一个抽象类,它提供了Spring MVC的完整配置,并提供了多个方法用于扩展其默认配置。通过继承 WebMvcConfigurationSupport ,可以完全控制Spring MVC的配置,并实现高度的自定义。

而 WebMvcConfigurer 接口是一个接口,它定义了很多方法,可以用来扩展 Spring MVC 的默认配置。与 WebMvcConfigurationSupport 相比,它提供了更加灵活的方式来进行自定义,而不需要完全重写整个配置。实现 WebMvcConfigurer 接口的类可以被认为是与默认配置协作的组件,使其可以通过部分自定义来修改默认配置。

总而言之,当需要对全局性的 Spring MVC 配置进行大规模的修改时,建议使用 WebMvcConfigurationSupport 。在这种情况下,可以通过继承 WebMvcConfigurationSupport 来扩展默认配置,并实现全部或大部分的方法来控制整个MVC配置。

当需要做一些小的修改,例如添加拦截器、修改静态资源位置,建议实现 WebMvcConfigurer 。实现 WebMvcConfigurer 接口可以按需修改 Spring MVC 的默认配置,而不需要将整个配置重写一遍。同时,项目也可以继续使用默认配置中的一些组件,例如 ContentNegotiationMessageConverter 等。

需要注意的是,当实现了 WebMvcConfigurer 接口时,不要继承 WebMvcConfigurationSupport。 因为,这些接口的默认实现是在 WebMvcAutoConfiguration 配置类中注册的。如果同时实现这两个接口,可能会导致冲突或者不必要的麻烦。

猜你喜欢

转载自blog.csdn.net/m0_56680022/article/details/130161901