Springboot控制层不同类型参数接收

866 阅读11分钟

前言

和前端对接接口时,针对不同的请求方法和请求contentType,我们后端要编写不同的接收参数的控制层方法。所以大概的汇总下对接过程中常用的方法接参形式。

基本类型的接参

主要针对的是Get请求携带的参数获取。

@RequestParam获取请求参数值

如请求 http://localhost:8080/hello?param=100 ,使用@RequestParam注解声明接参数的名称,对于请求参数param对应的值自动封装到进方法参数param上

@GetMapping("/hello")
public String hello(@RequestParam("param") String param) {
    return param;
}

如请求 http://localhost:8080/hello?param=100,200 , 使用@RequestParam注解声明接参数的名称,申明数组类型来接收参数

    @GetMapping("/hello")
    public String hello(@RequestParam("param") List<String> param) {
        return param;
    }

@PathVariable获取路径变量的值

如请求 http://localhost:8080/{name}/hello, 使用 @PathVariable 注解声明路径上的参数名称,对应请求路径上的name值会封装进方法参数name上

@GetMapping("/{name}/hello")
public String testHello(@PathVariable("name") String name) {
    return name;
}

针对x-www-form-urlencoded 和 form-data的普通参数

实体类内的参数值

第一种:在使用post请求下,可以通过对象类型来接受对应的类型

@RequestMapping("/user")
public User tt(User user) {
    return user;
}

通过传递user对象里的字段名称的值,可以快速填充到对象中。

方法具体参数值

第二种: 在使用post请求下,可以通过一个个参数来填充

@RequestMapping("/tt")
public User tt(String name, Integer id) {
    User user = new User();
    user.setId(id);
    user.setName(name);
    return user;
}

通过传递id,name来填充字段值

复杂类型json的接参

针对Post请求,contentType对应的是application/json或者application/xml等类型

@RequestBody来获取body中的数据

使用@RequestBody注解将请求体中的数据绑定到一个 Java 对象 。User对象中包含id和name的参数,将从body请求数据中获取到id的值和name的值。

@PostMapping("/bodyTest")
public User bodyTest(@RequestBody User user) {
    return user;
}

构造对应的测试Post请求地址:http://localhost:8080/bodyTest , contentType为application/json ,body的内容为 {"id":1,"name":2}

image.png

获取文件上传信息

可以通过使用MultipartFile类型的参数或者@RequestPart注解,可以获取文件内容,名称等信息。

单文件上传

请求路径 http://localhost:8080/testUploadFile, contentType为multipart/form-data , 参数为file,类型为文件格式。

// 直接使用MultipartFile类型指定,名称为file
@PostMapping("/testUploadFile")
public String testUploadFile(MultipartFile file) {
    return file.getOriginalFileName();
}

// 使用RequestPart注解标记,名称为file
@PostMapping("/testUploadFile1")
public String testUploadFile1(@RequestPart("file") MultipartFile file) {
    return file.getOriginalFileName();
}

RequestPart接收参数名为file的文件,定义MultipartFile 为文件类型

image.png

多文件上传

如果要是遇到了多文件上传的场景,其实和上述单文件上传方法一致,只不过增加文件类型增加为数组格式 MultipartFile[]

// 此时接收参数file名称的参数为MultipartFile数组的格式
@PostMapping("/testUploadFile2")
public String testUploadFile2(@RequestPart("file") MultipartFile[] file) {
    return Arrays.stream(file).map(MultipartFile::getOriginalFilename)
                    .collect(Collectors.joining(","));
}

RequestPart接收参数名为file的文件数组 。

文件上传+参数传递

如果要是遇到了文件上传的场景,同时需要接收文件参数。这个时候需要怎么办呢?当然还是有办法的,就是通过 @ModelAttribute 配合 @RequestPart 组合接收

// user对象的属性直接写在form-data结构里的参数
@PostMapping("/testUploadFile3")
public String testUploadFile3(@ModelAttribute User user , 
                              @RequestPart("file") MultipartFile[] file) {
    String collect = Arrays.stream(file).map(MultipartFile::getOriginalFilename).collect(Collectors.joining(","));
    return user.getName() + " " + collect;
}

user对象属性接收表单内其他属性,RequestPart接收参数名为file的文件类型

image.png

针对SSE的流式输出

一种基于 HTTP 的单向通信协议,适用于需要从服务器向客户端推送数据的场景(如实时通知、进度更新等)

  1. 定义申明返回的内容类型为 text/event-stream
  2. 使用Spring 提供的一个类,用于管理 SSE 连接,SseEmitter
    • send():发送事件数据。
    • complete():完成发送,关闭连接。
    • completeWithError():发生错误时关闭连接。
    • onCompletion()onTimeout():注册回调函数,处理连接完成或超时的情况。
// 创建 SSE 连接
    @GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter handleSse() {
        // 创建一个 SseEmitter 实例
        SseEmitter emitter = new SseEmitter();
        // 异步发送数据
        executor.execute(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    // 发送事件数据
                    emitter.send(SseEmitter.event()
                            .id(String.valueOf(i))
                            .name("sse-event")
                            .data("Message " + i));
                    // 模拟延迟
                    Thread.sleep(1000);
                }
                // 完成发送
                emitter.complete();
            } catch (IOException | InterruptedException e) {
                emitter.completeWithError(e);
            }
        });
        return emitter;
    }

以非阻塞式WebFlux的形式的返回

通过Sping5中提供的框架,使用 WebFlux 可以实现响应式编程模型 ,可以通过Mono(0/1个)和Flux(多个)分别表示元素序列。

    // 返回一个简单的 Mono<String>
    @GetMapping("/hello")
    public Mono<String> hello() {
        return Mono.just("Hello, WebFlux!");
    }

    // 模拟异步延迟返回
    @GetMapping("/delayed")
    public Mono<String> delayedResponse() {
        return Mono.just("Delayed Response")
                   .delayElement(java.time.Duration.ofSeconds(2)); // 延迟 2 秒
    }

针对Enum枚举类型的简单配置

如果使用的枚举对象,它被定义的时候不包含其他属性,此时能被自动序列化和反序列化,无需额外的配置

对于有属性的枚举值

此时针对如下的枚举值在序列化和反序列化时就会遇到问题。

public enum Status {

    SUCCESS("成功",1),FAIL("失败",1)

    ;

    private String name;
    private int code;
    private Status(String name,int code){
        this.name = name;
        this.code = code;
    }

    // 反序列化和序列化方法的关键(指定了using-to-string)
    @Override
    public String toString() {
        return this.name;
    }
}

此时可以通过配置文件定义枚举的序列化和反序列化的方式,在将包含枚举类型的 Java 对象序列化为 JSON 时,会调用枚举类型的toString方法来获取要序列化的值

spring:
  jackson:
    serialization:
      write-enums-using-to-string: true
    deserialization:
      read-enums-using-to-string: true

针对数据的null的忽略

遇到有些参数存在null的场景,可以通过配置输出给前端的时候默认就忽略掉带有null值的字段。

方法一: 通过全局的配置

spring:
  jackson:
    default-property-inclusion: non_null

方法二: 通过实体类属性上的注解

@JsonInclude(Include.NON_NULL)
private String name;

针对Date类型的接发参的简单配置

问题产生的场景

  • 场景一: 通过Get请求,入参是Date类型
  • 场景二: 通过Post场景,入参是实体类,实体类包含Date类型参数

结论: 以上情况使用时间戳和字符串都无法正确赋值到参数上。那么如何才能使用参数正确的被反序列化上值呢?可以简单的通过springboot内置的注解

配置统一格式返回给前端

当我们返回给前端字段属性为Date类型,要保证前端接收到的Date类型数据统一为一种时间格式类型,可以通过配置文件直接配置,或者通过注解单独生效。

方法一:生效全局的配置 application.yml

spring:
  jackson:
    // 第一种:使用字符串时间格式接收和输出
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    // 第二种:使用时间戳格式接收和输出
    serialization:
      write-dates-as-timestamps: true
    deserialization:
      read-date-timestamps-as-nanoseconds: true

方法二: 单独实体类的属性上的配置

使用@JsonFormat 或者 @DateTimeFormat 注解

@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") // 第一种:使用字符串时间格式接收输出
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") // 第二种:使用字符串时间格式接受输出
private Date created;

注意: 和前端同学约定好统一一种就行,字符串或者时间戳。

1.先自定义实现序列化和反序列化的规则类

反序列化对LocalDateTime的配置实现,然后通过deserialize方法,获取到前端传递的jsonParser.getText()文本内容,然后再去通过工具类转换成LocalDateTime时间内容

// 对该类型的反序列化的转化类
public class CustomLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {

    @Override
    public LocalDateTime deserialize(JsonParser jsonParser, 
DeserializationContext deserializationContext) throws IOException, IOException {
        if (StringUtils.isEmpty(jsonParser.getText())) {
            return null;
        }
        return Instant
                .ofEpochMilli(Long.parseLong(jsonParser.getText()))
                .atZone(ZoneOffset.ofHours(8))
                .toLocalDateTime();
    }
}

序列化对LocalDateTime的配置实现 , 然后通过LocalDateTime的值通过工具类转换成对应前端想要的值,然后通过 jsonGenerator来write输出给前端。

public class CustomLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {


    @Override
    public void serialize(LocalDateTime localDateTime, JsonGenerator jsonGenerator,
                          SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeNumber(localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli());
    }


}

2.再使用消息转换器的配置

Springboot 支持个性化定制处理消息转换配置的接口,开发同学可以根据自己的需求来实现定制。比如针对一些LocalDateTime,LocalDate等时间类型使用自定义代码规则处理。

如下两种方式:

  • 对已经存在的对jackson处理的消息转换器修改
  • 也可以通过配置convert中添加进自己的定义的转换器

第一种直接修改ObjectMapper的Bean

修改 MappingJackson2HttpMessageConverterObjectMapper, 通过自定义这个对象来实现对指定类型的序列化和反序列化的结果。

@Bean
@Primary
public ObjectMapper defaultObjectMapper() {
    return new ObjectMapper();
}

@Bean(name = "customObjectMapper")
public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    SimpleModule module = new SimpleModule();
    // 注册自定义反序列化器和序列化器
    module.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer());
    module.addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer())
    objectMapper.registerModule(module);
    return objectMapper;
}

@Bean
public MappingJackson2HttpMessageConverter customMessageConverter(ObjectMapper customObjectMapper) {
    return new MappingJackson2HttpMessageConverter(customObjectMapper);
}

第二种自定义MappingJackson2HttpMessageConverter添加

使用 WebMvcConfigurer 提供的 configureMessageConverters 方法 , 用于自定义消息转换器(Message Converters)的配置,负责将请求体中的数据转换为 Java 对象(在请求处理阶段),以及将 Java 对象转换为响应体(在响应发送阶段)。

常用的消息转换器

  • FastJsonHttpMessageConverter : 阿里开发,可以替代springboot默认的jackson的对json序列化的操作
    • 使用场景: application/json媒体类型的请求和响应
  • StringHttpMessageConverter :处理字符串类型的消息转换
    • 使用场景: text/plain媒体类型的请求和响应
  • ByteArrayHttpMessageConverter : 处理字节数组和http信息的转换
    • 使用场景: 支持多种媒体类型,包括application/octet - stream(用于通用的二进制数据)等
  • MappingJackson2HttpMessageConverter: Springboot依赖的核心库jackson对json序列化的处理
    • 使用场景: application/json媒体类型的请求和响应

在converts消息转换器列表中添加自己的定义的消息转换器,添加到顶部,对于处理上application/json 的时候会走我们配置的消息转换器上

@Configuration
public class WebCustomConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        ObjectMapper objectMapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        // 注册自定义反序列化器和序列化器
        module.addDeserializer(LocalDateTime.class, new CustomDeserializer());
        module.addSerializer(LocalDateTime.class, new CustomSerializer())
        objectMapper.registerModule(module);
        MappingJackson2HttpMessageConverter jacksonConvert = new MappingJackson2HttpMessageConverter(objectMapper);
        // 添加到converts消息转换器列表中的顶部
        converters.add(0, jacksonConvert);
    }
}

!!!如果不想影响默认的MappingJackson2HttpMessage消息转换器配置,可以extendMessageConverters中获取再添加自己的内容

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    for (HttpMessageConverter<?> converter : converters) {
        if (converter instanceof MappingJackson2HttpMessageConverter) {
            MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converter;
            ObjectMapper objectMapper = jacksonConverter.getObjectMapper();
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            SimpleModule module = new SimpleModule();
            // 注册自定义反序列化器和序列化器
            module.addDeserializer(LocalDateTime.class, new CustomLocalDateTimeDeserializer());
            module.addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer());
            objectMapper.registerModule(module);
            jacksonConverter.setObjectMapper(objectMapper);
        }
    }
}

第三种直接添加序列化和反序列化器

除了前面提到的直接修改ObjectMapper的bean , springboot提供了通过实现Jackson2ObjectMapperBuilderCustomizer接口,可以对ObjectMapper进行高度定制化的配置。

/**
  * Jackson序列化和反序列化转换器,用于转换Post请求体中的json以及将对象序列化为返回响应的json
  */
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    return builder -> builder
            .serializerByType(LocalDateTime.class, new CustomSerializer())
            .deserializerByType(LocalDateTime.class,  new CustomDeserializer())
            ;
}

总得来说实现逻辑还是将自定义的序列化和反序列化的规则的类加进配置中,对不同类型的处理,应用到ObjectMapper,从而影响json的序列化和返序列化。

自定义注解来实现序列化和反序列化

通过注解上添加@JacksonAnnotationsInside和序列化的对应的规则类(前面定义过的)。

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonDeserialize(using = CustomDeserializer.class)
public @interface StampToLocalDateTime {

}

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = CustomSerializer.class)
public @interface LocalDateTimeToStamp {

}

然后可以对于你想使用对应字段应用的序列化和反序列化的地方,增加上述的注解使用即可。

@Data
public class TestDate {
    @StampToLocalDateTime
    @LocalDateTimeToStamp
    private LocalDateTime date;
}

Jackson替换为FastJson消息转换器

注意: 前面提到的常用消息转换器 FastJsonHttpMessageConverter ,也是可以针对application/json数据参数类型进行处理。那我们需要如何用 FastJsonHttpMessageConverter 去替换来解析生效呢?

增加fastjson的依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>版本号</version>
    </dependency>

编写 FastJsonHttpMessageConverter

public FastJsonHttpMessageConverter fastJsonHttpMessageConverter() {
    // 创建FastJsonConfig对象
    FastJsonConfig fastJsonConfig = new FastJsonConfig();
    // 设置自定义序列化类(!!)
    fastJsonConfig.setSerializer(CustomUserSerializer.class);
    // 创建FastJsonHttpMessageConverter对象
    FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
    // 设置配置信息
    converter.setFastJsonConfig(fastJsonConfig);
    // 设置支持的媒体类型
    List<MediaType> supportedMediaTypes = new ArrayList<>();
    supportedMediaTypes.add(MediaType.APPLICATION_JSON);
    converter.setSupportedMediaTypes(supportedMediaTypes);
    return converter;
}

然后再添加到我们的 List<HttpMessageConverter<?>> converts集合中。同时需要注意下生效顺序。

总结

  • 对于一般的Get请求
    • 使用@RequesParam 注解生效字段绑定属性
    • 使用@PathVariable 注解生效获取路径上属性
  • 对于一般的post请求
    • application/json ,使用@RequestBody注解反序列化到对象属性上
    • multipart/form-data ,使用@RequestPart 获取文件信息
  • 对于date类型/enum类型,null的忽略的统一序列化和反序列化值
    • 通过框架给的注解实现
    • 通过全局配置文件配置生效
  • 对于自定义参数序列化和反序列化
    • 通过ObjectMapper的绑定针对具体参数的序列化/反序列化方法
    • 通过springboot给定的接口实现Jackson2ObjectMapperBuilderCustomizer
    • 通过自定义注解封装实现

最后,还是和前端约定好对应类型的统一的出入参数,约定好不同请求对应的不同的相关的的请求方式和类型。