惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
爱范儿
爱范儿
人人都是产品经理
人人都是产品经理
博客园 - 司徒正美
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
量子位
罗磊的独立博客
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
小众软件
小众软件
C
Cybersecurity and Infrastructure Security Agency CISA
Cyberwarzone
Cyberwarzone
大猫的无限游戏
大猫的无限游戏
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
Simon Willison's Weblog
Simon Willison's Weblog
The Cloudflare Blog
博客园 - 三生石上(FineUI控件)
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园_首页
博客园 - 叶小钗
V
Vulnerabilities – Threatpost
T
The Exploit Database - CXSecurity.com
T
Tailwind CSS Blog
IT之家
IT之家
博客园 - 聂微东
Spread Privacy
Spread Privacy
V2EX - 技术
V2EX - 技术
S
Security Affairs
宝玉的分享
宝玉的分享
V
V2EX
C
Cisco Blogs
博客园 - Franky
美团技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
月光博客
月光博客
S
Securelist
J
Java Code Geeks
Webroot Blog
Webroot Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
P
Proofpoint News Feed
Last Week in AI
Last Week in AI
L
LINUX DO - 热门话题
NISL@THU
NISL@THU
WordPress大学
WordPress大学
W
WeLiveSecurity
T
Threatpost
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
腾讯CDC
阮一峰的网络日志
阮一峰的网络日志

青空之蓝

[青空之蓝-2023] - 色彩 | 青空之蓝 [青空之蓝-2022] - 平静 | 青空之蓝 [青空之蓝-2021] - 远望 | 青空之蓝 浅谈垃圾回收 | 青空之蓝 浅谈泛型擦除 | 青空之蓝 浅谈单点登录 | 青空之蓝 使用 Kotlin 编写 Spring 测试 | 青空之蓝 设计模式系列文章 | 青空之蓝 从零实现一个 Java 微框架 - IoC | 青空之蓝 从零实现一个 Java 微框架 - 前言 | 青空之蓝 浅谈 JVM:类加载 | 青空之蓝 浅谈 IO | 青空之蓝 浅谈并发:synchronized & ReentrantLock | 青空之蓝 浅谈并发:CAS & AQS | 青空之蓝 浅谈并发:ThreadLocal | 青空之蓝 浅谈并发:三大特性 | 青空之蓝 浅谈组合注解 & 注解别名 | 青空之蓝 [青空之蓝-2020]-迷茫 | 青空之蓝 Java 系列文章 | 青空之蓝 HTTP 系列文章 | 青空之蓝 浅谈 EatWhatYouKill | 青空之蓝 浅谈可扩展线程池 | 青空之蓝 聊聊写框架 | 青空之蓝 聊聊现状-[2020-09] | 青空之蓝 浅谈并发:锁 | 青空之蓝 浅谈并发:基础 | 青空之蓝 浅谈缓存 | 青空之蓝 浅谈 Proxy 和 Aop | 青空之蓝 从零实现一个 PHP 微框架 - 初始化请求 | 青空之蓝 为 Vue3 添加一个简单的 Store | 青空之蓝 从零实现一个 PHP 微框架 - 服务提供者 | 青空之蓝 WSL2 踩坑记录 | 青空之蓝 浅谈浏览器Event Loop [更新] | 青空之蓝 从零实现一个 PHP 微框架 - Bootstrap 启动加载 | 青空之蓝 从零实现一个 PHP 微框架 - IoC 容器 | 青空之蓝 从零实现一个 PHP 微框架 - PSR & Composer | 青空之蓝 从零实现一个 PHP 微框架 - 前言 | 青空之蓝 MVVM 简单实现 | 青空之蓝 浅谈 DI 和 IoC | 青空之蓝 中间件实现 [PHP] | 青空之蓝 告别 Windows 终端的难看难用,打造好用的 PowerShell | 青空之蓝 VSCode Java输出中文乱码问题解决[更新] | 青空之蓝 浅谈浏览器渲染 | 青空之蓝 Vue-Cli@2 项目迁移日志 | 青空之蓝 Laragon & Scoop 集成踩坑记录 | 青空之蓝 「一行代码」优雅管理 Windows 软件 | 青空之蓝 [青空之蓝-2019]-年度总结 | 青空之蓝 为Vue添加简单的Store | 青空之蓝 为React添加简单的Store | 青空之蓝 为Vuex添加同步Action | 青空之蓝 浅谈B+树 | 青空之蓝 浅谈跳表 | 青空之蓝 浅谈数据库索引 | 青空之蓝 MySQL事务隔离 | 青空之蓝 算法复杂度分析(1) | 青空之蓝 一年来的经验总结 | 青空之蓝 Acrylic - VSCode Extension | 青空之蓝 ace编辑器设置惯性滚动 | 青空之蓝 Java二叉树实现 | 青空之蓝 Java图实现 | 青空之蓝 为apt方式安装的nginx重新编译增加WebDAV | 青空之蓝 XK-Editor - 一个支持富文本和Markdown的编辑器 | 青空之蓝 JS生成列表树 | 青空之蓝 Laravel生成目录树 | 青空之蓝 XK-Note - 集各种神奇功能的云笔记 | 青空之蓝 PHP GD生成验证码 | 青空之蓝 PHP GD图片处理[转换格式-水印-缩略图] | 青空之蓝 Origami - 简洁轻快的WordPress主题 | 青空之蓝 为WordPress启用WorkBox | 青空之蓝 [青空之蓝-2018]-年度总结 | 青空之蓝 VSCode Java手动导入jar和源码包 | 青空之蓝 Windows IP变化自动发送邮件 | 青空之蓝 C链表实现重制版 | 青空之蓝 C 结构体的定义和使用 | 青空之蓝 图的搜索(遍历) - BFS & DFS | 青空之蓝 Java链表实现 | 青空之蓝 C 快速排序 | 青空之蓝 C 插入排序 | 青空之蓝 C 归并排序 | 青空之蓝 C语言链表实现 | 青空之蓝 VSCode配置Java调试环境[Windows] | 青空之蓝 C 选择排序 | 青空之蓝 C 冒泡排序 | 青空之蓝 VSCode配置PHP调试环境[Windows] | 青空之蓝 VSCode配置C/C++ GDB调试环境[Windows] | 青空之蓝 WordPress友情链接模板 | 青空之蓝 Intel Optane 傲腾内存体验 | 青空之蓝 Mysql双机热备实战 | 青空之蓝 博客一年记录 | 青空之蓝 为WordPress启用Service Worker | 青空之蓝 Bing每日一图API | 青空之蓝 iframe延迟加载 | 青空之蓝 写在2018年高考前 | 青空之蓝 The Fox主题汉化分享 | 青空之蓝 [青空之蓝-2017]-崭新 | 青空之蓝 本博客评论规则 | 青空之蓝 世界,您好! | 青空之蓝
无须定义类,Spring 快速注入 Json 参数 | 青空之蓝
Otstar Lin · 2020-08-04 · via 青空之蓝

前言

不知各位在开发的时候有没有遇到这种情况,当前后端分离的时候,前端时常会把很简单的参数使用 JSON 格式传入,当 Spring 要获取这些参数的时候每次都需要定义一个类,在使用的时候也需要使用对象的 Getter 方法,这样极其不方便。而如果要改前端使用 FormData 的方式传输,那么又会遇到另外一个问题:前端常用的请求库是 axios,而 axios 传输的数据默认是采用 JSON 格式传输的,如果需要使用 FromData 的方式传输,那么需要再每个请求方法上增加 FormData 的 Content-Type,或者添加到默认的配置中。

那么有什么办法可以在 Spring 中使 JSON 可以像 FormData 那样方便的注入呢?

思路

在 Spring 入参 Controller 的时候会经过一系列的 HandlerMethodArgumentResolver,我们可以写一个 Resolver 实现该接口,并在 Spring 中增加这个 Resolver,那么只要符合 JSON 格式参数,那么就可以通过该 Resolver 实现注入。

实现

首先我们需要准备一些注解,用于标注是通过 JSON 格式的数据获取参数的:

首先是和 @RequestParam 一样的 @JsonParam,用于标注该参数注入的名称和是否必须注入等信息:

@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JsonParam {
  @AliasFor("name")
  String value() default "";

  @AliasFor("value")
  String name() default "";

  boolean required() default true;

  String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JsonParam {
  @AliasFor("name")
  String value() default "";

  @AliasFor("value")
  String name() default "";

  boolean required() default true;

  String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

然后是用于标注在方法上的 @RequestJson,通过标注这个注解,我们就可以不需要使用 @JsonParam 注解来描述参数,而是使 Spring 通过参数的名称注入该参数:

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJson {
}
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJson {
}

还有一些工具类这里就不写了,这些工具类主要是用于一些特性的实现,比如点语法等。具体可以到 这里 查看。

接着就是用于处理 JSON 格式参数的 HandlerMethodArgumentResolver 了:

package me.ixk.json_inject.injector;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import me.ixk.json_inject.annotation.JsonParam;
import me.ixk.json_inject.annotation.RequestJson;
import me.ixk.json_inject.utils.Helper;
import me.ixk.json_inject.utils.JSON;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class JsonArgumentResolver implements HandlerMethodArgumentResolver {
  private static final String JSON_REQUEST_ATTRIBUTE_NAME = "JSON_REQUEST_BODY";

  @Override
  public boolean supportsParameter(final MethodParameter methodParameter) {
    // 当参数有 JsonParam 或者 RequestJson 注解的时候,就视为传入的是 JSON 格式的参数
    return (
      methodParameter.hasParameterAnnotation(JsonParam.class) ||
      methodParameter.hasMethodAnnotation(RequestJson.class)
    );
  }

  @Override
  public Object resolveArgument(
    final MethodParameter methodParameter,
    final ModelAndViewContainer modelAndViewContainer,
    final NativeWebRequest nativeWebRequest,
    final WebDataBinderFactory webDataBinderFactory
  )
    throws Exception {
    // 读取并解析 JSON,记得把 JSON 存起来,一是可以避免多次解析,二是 Body 如果没有进行处理的话默认只能读取一次,当第二次读取的时候则会为空。
    final JsonNode body = this.getJsonBody(nativeWebRequest);
    // 判断是否是读取整个 JSON (因为有的时候传入的可能是一个数组或者其他类型的数据,而不是 JSON 对象)
    if ("$body".equals(methodParameter.getParameterName())) {
      return JSON.convertToObject(body, methodParameter.getParameterType());
    }
    final JsonParam jsonParam = methodParameter.getParameterAnnotation(
      JsonParam.class
    );
    final RequestJson requestJson = methodParameter.getMethodAnnotation(
      RequestJson.class
    );
    JsonNode node = NullNode.getInstance();
    if (jsonParam != null) {
      // 通过 JsonParam 中设置的名称读取
      node = Helper.dataGet(body, jsonParam.name(), NullNode.getInstance());
    } else if (requestJson != null) {
      // 通过参数名称读取
      node = body.get(methodParameter.getParameterName());
    }
    if (node.isNull()) {
      if (jsonParam != null) {
        // 若为空并且必须传入,则抛出异常
        if (jsonParam.required()) {
          throw new MissingServletRequestParameterException(
            jsonParam.name(),
            methodParameter.getParameterType().getTypeName()
          );
        } else {
          // 否则传入默认参数
          node = TextNode.valueOf(jsonParam.defaultValue());
        }
      } else {
        return null;
      }
    }
    // 转换数据类型
    return JSON.convertToObject(node, methodParameter.getParameterType());
  }

  private JsonNode getJsonBody(final NativeWebRequest nativeWebRequest) {
    final HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(
      HttpServletRequest.class
    );

    JsonNode body = (JsonNode) nativeWebRequest.getAttribute(
      JSON_REQUEST_ATTRIBUTE_NAME,
      NativeWebRequest.SCOPE_REQUEST
    );

    if (body == null) {
      try {
        if (servletRequest != null) {
          body =
            JSON.parse(
              servletRequest
                .getReader()
                .lines()
                .collect(Collectors.joining("\n"))
            );
        }
      } catch (final IOException e) {
        //
      }
      if (body == null) {
        body = NullNode.getInstance();
      }
      nativeWebRequest.setAttribute(
        JSON_REQUEST_ATTRIBUTE_NAME,
        body,
        NativeWebRequest.SCOPE_REQUEST
      );
    }

    return body;
  }
}
package me.ixk.json_inject.injector;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import me.ixk.json_inject.annotation.JsonParam;
import me.ixk.json_inject.annotation.RequestJson;
import me.ixk.json_inject.utils.Helper;
import me.ixk.json_inject.utils.JSON;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class JsonArgumentResolver implements HandlerMethodArgumentResolver {
  private static final String JSON_REQUEST_ATTRIBUTE_NAME = "JSON_REQUEST_BODY";

  @Override
  public boolean supportsParameter(final MethodParameter methodParameter) {
    // 当参数有 JsonParam 或者 RequestJson 注解的时候,就视为传入的是 JSON 格式的参数
    return (
      methodParameter.hasParameterAnnotation(JsonParam.class) ||
      methodParameter.hasMethodAnnotation(RequestJson.class)
    );
  }

  @Override
  public Object resolveArgument(
    final MethodParameter methodParameter,
    final ModelAndViewContainer modelAndViewContainer,
    final NativeWebRequest nativeWebRequest,
    final WebDataBinderFactory webDataBinderFactory
  )
    throws Exception {
    // 读取并解析 JSON,记得把 JSON 存起来,一是可以避免多次解析,二是 Body 如果没有进行处理的话默认只能读取一次,当第二次读取的时候则会为空。
    final JsonNode body = this.getJsonBody(nativeWebRequest);
    // 判断是否是读取整个 JSON (因为有的时候传入的可能是一个数组或者其他类型的数据,而不是 JSON 对象)
    if ("$body".equals(methodParameter.getParameterName())) {
      return JSON.convertToObject(body, methodParameter.getParameterType());
    }
    final JsonParam jsonParam = methodParameter.getParameterAnnotation(
      JsonParam.class
    );
    final RequestJson requestJson = methodParameter.getMethodAnnotation(
      RequestJson.class
    );
    JsonNode node = NullNode.getInstance();
    if (jsonParam != null) {
      // 通过 JsonParam 中设置的名称读取
      node = Helper.dataGet(body, jsonParam.name(), NullNode.getInstance());
    } else if (requestJson != null) {
      // 通过参数名称读取
      node = body.get(methodParameter.getParameterName());
    }
    if (node.isNull()) {
      if (jsonParam != null) {
        // 若为空并且必须传入,则抛出异常
        if (jsonParam.required()) {
          throw new MissingServletRequestParameterException(
            jsonParam.name(),
            methodParameter.getParameterType().getTypeName()
          );
        } else {
          // 否则传入默认参数
          node = TextNode.valueOf(jsonParam.defaultValue());
        }
      } else {
        return null;
      }
    }
    // 转换数据类型
    return JSON.convertToObject(node, methodParameter.getParameterType());
  }

  private JsonNode getJsonBody(final NativeWebRequest nativeWebRequest) {
    final HttpServletRequest servletRequest = nativeWebRequest.getNativeRequest(
      HttpServletRequest.class
    );

    JsonNode body = (JsonNode) nativeWebRequest.getAttribute(
      JSON_REQUEST_ATTRIBUTE_NAME,
      NativeWebRequest.SCOPE_REQUEST
    );

    if (body == null) {
      try {
        if (servletRequest != null) {
          body =
            JSON.parse(
              servletRequest
                .getReader()
                .lines()
                .collect(Collectors.joining("\n"))
            );
        }
      } catch (final IOException e) {
        //
      }
      if (body == null) {
        body = NullNode.getInstance();
      }
      nativeWebRequest.setAttribute(
        JSON_REQUEST_ATTRIBUTE_NAME,
        body,
        NativeWebRequest.SCOPE_REQUEST
      );
    }

    return body;
  }
}

最后还需要将该 Resolver 添加到 Spring 中:

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addArgumentResolvers(
    List<HandlerMethodArgumentResolver> resolvers
  ) {
    resolvers.add(new JsonArgumentResolver());
  }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void addArgumentResolvers(
    List<HandlerMethodArgumentResolver> resolvers
  ) {
    resolvers.add(new JsonArgumentResolver());
  }
}

至此实现的部分就完成了,让我们看看如何使用吧。

使用

@SpringBootApplication
@RestController
public class JsonInjectApplication {

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

  @PostMapping("/param")
  public String param(@JsonParam(name = "key") final String key) {
    return key;
  }

  @PostMapping("/method")
  @RequestJson
  public String method(final String key) {
    return key;
  }

  @PostMapping("/data-get")
  public String dataGet(@JsonParam(name = "key.sub") final String key) {
    return key;
  }

  @PostMapping("/default-value")
  public String defaultValue(
    @JsonParam(name = "value", required = false) final String value
  ) {
    return value;
  }

  @PostMapping("/convert")
  public Integer convert(@JsonParam(name = "key") final Integer key) {
    return key;
  }

  @GetMapping("/get")
  public String get(String key) {
    return key;
  }

  @PostMapping("/post")
  public String post(String key) {
    return key;
  }
}
@SpringBootApplication
@RestController
public class JsonInjectApplication {

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

  @PostMapping("/param")
  public String param(@JsonParam(name = "key") final String key) {
    return key;
  }

  @PostMapping("/method")
  @RequestJson
  public String method(final String key) {
    return key;
  }

  @PostMapping("/data-get")
  public String dataGet(@JsonParam(name = "key.sub") final String key) {
    return key;
  }

  @PostMapping("/default-value")
  public String defaultValue(
    @JsonParam(name = "value", required = false) final String value
  ) {
    return value;
  }

  @PostMapping("/convert")
  public Integer convert(@JsonParam(name = "key") final Integer key) {
    return key;
  }

  @GetMapping("/get")
  public String get(String key) {
    return key;
  }

  @PostMapping("/post")
  public String post(String key) {
    return key;
  }
}

可以看到使用的方式和 @RequestParam 无太大差别。

结语

最近打算开始接着折腾 Spring 和后端了,感觉我还是更喜欢后端,前端太折腾了 ?。

无须定义类,Spring 快速注入 Json 参数

https://blog.ixk.me/post/spring-quickly-inject-json-parameters
  • 许可协议

    BY-NC-SA

  • 本文作者

    Otstar Lin

  • 发布于

    2020/08/04

转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

浅谈缓存浅谈 Proxy 和 Aop