一个接口同时支持 form 表单、form-data、json 的优雅写法

本文介绍如何在Java中实现一个接口兼容POST JSON、form表单及form-data等多种Content-Type的方法。通过自定义注解和解析器,可以优雅地解决兼容问题,并保持代码整洁。

网上很多代码都是千篇一律的 cvs,相信我只要你认真看完我写的这篇,你就可以完全掌握这个知识点,这篇文章不适合直接 cvs,一定要先理解。

最近重写个项目遇到个比较棘手的问题,老项目是 PHP 接口,这个接口同时兼容 POST json 和 form 表单,更骚的是连 form-data 也兼容。。。因为写 PHP 请求的对接方代码不严谨。

而在 Java 中,一个接口只支持一种 content-type,json 就用 @RequestBody,form 表单就用 @RequestParam 或不写,form-data 就用 MultipartFile

兼容版本

如果要把在一个接口中同时兼容三种,比较笨的办法就是获取 HttpServletRequest,然后自己再写方法解析。类似如下:

private Map<String, Object> getParams(HttpServletRequest request) {

    String contentType = request.getContentType();
    if (contentType.contains("application/json")) {
        // json 解析...
        return null;
    } else if (contentType.contains("application/x-www-form-urlencoded")) {
        // form 表单解析 ...
        return null;
    } else if (contentType.contains("multipart")) {
        // 文件流解析
        return null;
    } else {
         throw new BizException("不支持的content-type");
    } 

}

但是这样写有弊端

  • 代码很丑,具体到解析代码又臭又长

  • 只能返回固定 map 或者自己重新组装参数类

  • 无法使用 @Valid 校验参数,像我这种几十个参数都要检验的简直是灾难

优雅版本

网上有 form 表单和 json 同时兼容的版本,但是没有兼容 form-data,我在这做一下补充。

1. 自定义注解

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GamePHP {
}

2. 自定义注解解析

public class GamePHPMethodProcessor implements HandlerMethodArgumentResolver {

    private GameFormMethodArgumentResolver formResolver;
    private GameJsonMethodArgumentResolver jsonResolver;

    public GamePHPMethodProcessor() {
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        PHPMessageConverter PHPMessageConverter = new PHPMessageConverter();
        messageConverters.add(PHPMessageConverter);

        jsonResolver = new GameJsonMethodArgumentResolver(messageConverters);
        formResolver = new GameFormMethodArgumentResolver();
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        GamePHP ann = parameter.getParameterAnnotation(GamePHP.class);
        return (ann != null);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        ServletRequest servletRequest = nativeWebRequest.getNativeRequest(ServletRequest.class);
        String contentType = servletRequest.getContentType();
        if (contentType == null) {
            throw new IllegalArgumentException("不支持contentType");
        }

        if (contentType.contains("application/json")) {
            return jsonResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory);
        }

        if (contentType.contains("application/x-www-form-urlencoded")) {
            return formResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory);
        }

        if (contentType.contains("multipart")) {
            return formResolver.resolveArgument(methodParameter, modelAndViewContainer, nativeWebRequest, webDataBinderFactory);
        }

        throw new IllegalArgumentException("不支持contentType");
    }
}

3. 添加到 spring configuration

    @Bean
    public MyMvcConfigurer mvcConfigurer() {
        return new MyMvcConfigurer();
    }

    public static class MyMvcConfigurer implements WebMvcConfigurer {
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
            resolvers.add(new GamePHPMethodProcessor());
        }
    }

4. form-data 的特殊处理

引入 jar 包

    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.3.1</version>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.4</version>
    </dependency>

新增解析 bean

@Bean(name = "multipartResolver")
public MultipartResolver multipartResolver(){
    CommonsMultipartResolver resolver = new CommonsMultipartResolver();
    resolver.setDefaultEncoding("UTF-8");
    resolver.setResolveLazily(true);//resolveLazily属性启用是为了推迟文件解析,以在在UploadAction中捕获文件大小异常
    resolver.setMaxInMemorySize(40960);
    resolver.setMaxUploadSize(50*1024*1024);//上传文件大小 50M 50*1024*1024
    return resolver;
}

特殊说明,GameJsonMethodArgumentResolver 和 GameFormMethodArgumentResolver 是我们自定义的 json 和 form 解析,如果你没有自定义的,使用 spring 默认的 ServletModelAttributeMethodProcessor 和 RequestResponseBodyMethodProcessor 也可以。

只需将 @RequestParam 注解改为 @GamePHP,接口即可同时兼容三种 content-type

其流程为,spring 启动的时候,MyMvcConfigurer 调用 addArgumentResolvers 方法将 GamePHPMethodProcessor 注入,接到请求时,supportsParameter 方法判断是否使用此 resolver,如果为 true,则进入 resolveArgument 方法执行。

至此我们可以得出一个结论,PHP 是世界上最垃圾的语言。写代码一时爽,维护火葬场。

你说得非常对!我们来 **深入分析 `this.form.dataDetails?.imageInfo` 这个写法是否有问题**,并给出 **正确的处理方式**。 --- ## ❌ 为什么说 `this.form.dataDetails?.imageInfo` 可能有问题? ### 1. 语法本身是合法的 ✅ ```js this.form.dataDetails?.imageInfo ``` - 这是 JavaScript 的 **可选链操作符(Optional Chaining)** - 如果 `this.form` 或 `this.form.dataDetails` 为 `null/undefined`,会安全返回 `undefined`,不会报错 - ✅ 语法上没有错误,在现代浏览器 Vue 3 / 支持 ES2020 的环境中是可以运行的 ### 🔥 但问题出在:**你可能还没初始化 `dataDetails`** 尤其是在 Vue 的响应式系统中: > 如果 `this.form.dataDetails` 是一个后来动态设置的对象,而你在初始化时没有定义它,那么即使用了 `?.`,你也可能会遇到: - 数据无法被 Vue 响应式追踪 - `$set` 写入失败或视图不更新 - 表单字段绑定异常 --- ## ✅ 正确做法:确保 `dataDetails` 被正确定义为响应式对象 ### ✅ 推荐初始化方式(在 `data()` 中) ```js data() { return { form: { dataDetails: { imageInfo: '', // 必须提前声明 smallImgBucketPathForWeb: '', heatmapImgBucketPathForWeb: '', bigImgBucketPathForWeb: '', hdpiImgBucketPathForApp: '' }, originDataDetails: {}, // 其他字段... } } } ``` 这样你就 **可以安全使用**: ```js this.form.dataDetails?.imageInfo ``` 否则如果你只写了: ```js form: { dataDetails: {} } // 或者更糟:没写任何字段 ``` 然后后续通过 `this.form.dataDetails.imageInfo = '...'` 赋值,可能会导致: > ⚠️ `imageInfo` 不是响应式的 → 修改后页面不刷新! --- ## ✅ 更安全的读取方式(带默认值兜底) 即使用了可选链,也建议加上默认值防止解析 `undefined` ```js const imageInfoStr = this.form.dataDetails?.imageInfo || '{}' ``` 但如果 `imageInfo` 应该是字符串类型,就不能默认 `'{}'`,而是: ```js let imageInfo = {} try { const raw = this.form.dataDetails?.imageInfo if (raw && typeof raw === 'string') { imageInfo = JSON.parse(raw) } else if (raw && typeof raw === 'object') { imageInfo = { ...raw } // 已经是对象了(比如之前处理过) } } catch (e) { return this.$message.error('imageInfo JSON 格式错误') } ``` --- ## ✅ 完整修复版代码(推荐写法) ```js // 提取并处理 imageInfo let imageInfo = {} const rawImageInfo = this.form.dataDetails?.imageInfo if (rawImageInfo) { try { if (typeof rawImageInfo === 'string') { imageInfo = JSON.parse(rawImageInfo) } else if (typeof rawImageInfo === 'object') { imageInfo = { ...rawImageInfo } // 浅拷贝对象 } else { imageInfo = {} } } catch (e) { return this.$message.error('imageInfo 不是合法的 JSON 字符串') } } else { imageInfo = {} // 明确初始化 } ``` 然后再继续构建 `img_bucket_path_map` --- ## ✅ 关键总结 | 问题 | 解释 | 建议 | |------|------|-------| | `?.` 语法错了吗? | ❌ 没错,ES2020 合法语法 | 可用,但需环境支持 | | 什么时候会出问题? | 当 `dataDetails` 未在 `data()` 中声明时 | 导致非响应式 | | 如何避免? | 提前声明所有嵌套字段 | 在 `data()` 中初始化结构 | | 如何安全解析? | 判断类型 + try/catch + 默认值 | 防止崩溃 | --- ### ✅ 最佳实践建议 #### 1. 初始化 `form` 结构(关键!) ```js data() { return { form: { dataDetails: { imageInfo: '{}', // 或 '{"img_bucket_path_map":{}}' smallImgBucketPathForWeb: '', heatmapImgBucketPathForWeb: '', bigImgBucketPathForWeb: '', hdpiImgBucketPathForApp: '' }, originDataDetails: {}, approver1: '', approverLevel1: null } } } ``` #### 2. 使用可选链 + 类型判断 + 默认值 ```js const raw = this.form.dataDetails?.imageInfo const imageInfo = (typeof raw === 'string' ? JSON.parse(raw) : raw) || {} ``` #### 3. 修改后记得用 `$set` 或直接赋值(Vue 2 需注意响应式) ```js // Vue 2 this.$set(this.form.dataDetails, 'imageInfo', JSON.stringify(imageInfo, null, 2)) // Vue 3(可以直接赋值) this.form.dataDetails.imageInfo = JSON.stringify(imageInfo, null, 2) ``` --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值