最近遇到Controller中需要多个@RequestBody的情况,但是发现并不支持这种写法,

这样导致

1、单个字符串等包装类型都要写一个对象才可以用@RequestBody接收;

2、多个对象需要封装到一个对象里才可以用@RequestBody接收。

查阅StackOverFlow,受到一个解决方案的启发,本人改进为以下版本,并给出了详尽的注释,希望对大家有帮助。

改进后的方案支持:

1、支持通过注解的value指定JSON的key来解析对象。

2、支持通过注解无value,直接根据参数名来解析对象

3、支持GET方式和其他请求方式

4、支持基本类型属性注入

5、支持通过注解无value且参数名不匹配JSON串key时,根据属性解析对象。

6、支持多余属性(不解析、不报错)、支持参数“共用”(不指定value时,参数名不为JSON串的key)

7、支持当value和属性名找不到匹配的key时,对象是否匹配所有属性。

重要更新记录:

2019年02月25日 新增xml方式参考配置

2019年02月07日 fix 当list参数为空时,parameterType.newInstance会导致异常。

2018年12月28日 新增测试用例,完善解析部分代码

2018年10月23日 完善项目格式

2018年08月28日 创建第一版

项目仅供参考,如因使用不当造成任何问题,请自行负责,有问题欢迎探讨改进。

项目地址(建议去拉最新代码):

https://github.com/chujianyun/Spring-MultiRequestBody

另外代码应该会尽量持续更新完善,欢迎大家贡献代码。

步骤如下:

0、除spring的Jar包外涉及的主要Maven依赖


  1. <dependency>
  2. <groupId>commons-lang</groupId>
  3. <artifactId>commons-lang</artifactId>
  4. <version>2.4</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.alibaba</groupId>
  8. <artifactId>fastjson</artifactId>
  9. <version>1.2.35</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>commons-io</groupId>
  13. <artifactId>commons-io</artifactId>
  14. <version>2.6</version>
  15. </dependency>

其中fastjson用来解析json对象,commons-lang用来字符串判空(也可以自己手写),commons-io用来读取请求封装为字符串类型(也可以自己封装)。

1、重写方法参数解析器


  1. package com.chujianyun.web.bean;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import io.github.chujianyun.annotation.MultiRequestBody;
  5. import org.apache.commons.io.IOUtils;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.springframework.core.MethodParameter;
  8. import org.springframework.web.bind.support.WebDataBinderFactory;
  9. import org.springframework.web.context.request.NativeWebRequest;
  10. import org.springframework.web.method.support.HandlerMethodArgumentResolver;
  11. import org.springframework.web.method.support.ModelAndViewContainer;
  12. import javax.servlet.http.HttpServletRequest;
  13. import java.io.IOException;
  14. import java.lang.reflect.Field;
  15. import java.util.HashSet;
  16. import java.util.Set;
  17. /**
  18. * 多RequestBody解析器
  19. *
  20. * @author 明明如月
  21. * @date 2018/08/27
  22. */
  23. public class MultiRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
  24. private static final String JSONBODY_ATTRIBUTE = "JSON_REQUEST_BODY";
  25. /**
  26. * 设置支持的方法参数类型
  27. *
  28. * @param parameter 方法参数
  29. * @return 支持的类型
  30. */
  31. @Override
  32. public boolean supportsParameter(MethodParameter parameter) {
  33. // 支持带@MultiRequestBody注解的参数
  34. return parameter.hasParameterAnnotation(MultiRequestBody.class);
  35. }
  36. /**
  37. * 参数解析,利用fastjson
  38. * 注意:非基本类型返回null会报空指针异常,要通过反射或者JSON工具类创建一个空对象
  39. */
  40. @Override
  41. public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
  42. String jsonBody = getRequestBody(webRequest);
  43. JSONObject jsonObject = JSON.parseObject(jsonBody);
  44. // 根据@MultiRequestBody注解value作为json解析的key
  45. MultiRequestBody parameterAnnotation = parameter.getParameterAnnotation(MultiRequestBody.class);
  46. //注解的value是JSON的key
  47. String key = parameterAnnotation.value();
  48. Object value;
  49. // 如果@MultiRequestBody注解没有设置value,则取参数名FrameworkServlet作为json解析的key
  50. if (StringUtils.isNotEmpty(key)) {
  51. value = jsonObject.get(key);
  52. // 如果设置了value但是解析不到,报错
  53. if (value == null && parameterAnnotation.required()) {
  54. throw new IllegalArgumentException(String.format("required param %s is not present", key));
  55. }
  56. } else {
  57. // 注解为设置value则用参数名当做json的key
  58. key = parameter.getParameterName();
  59. value = jsonObject.get(key);
  60. }
  61. // 获取的注解后的类型 Long
  62. Class<?> parameterType = parameter.getParameterType();
  63. // 通过注解的value或者参数名解析,能拿到value进行解析
  64. if (value != null) {
  65. //基本类型
  66. if (parameterType.isPrimitive()) {
  67. return parsePrimitive(parameterType.getName(), value);
  68. }
  69. // 基本类型包装类
  70. if (isBasicDataTypes(parameterType)) {
  71. return parseBasicTypeWrapper(parameterType, value);
  72. // 字符串类型
  73. } else if (parameterType == String.class) {
  74. return value.toString();
  75. }
  76. // 其他复杂对象
  77. return JSON.parseObject(value.toString(), parameterType);
  78. }
  79. // 解析不到则将整个json串解析为当前参数类型
  80. if (isBasicDataTypes(parameterType)) {
  81. if (parameterAnnotation.required()) {
  82. throw new IllegalArgumentException(String.format("required param %s is not present", key));
  83. } else {
  84. return null;
  85. }
  86. }
  87. // 非基本类型,不允许解析所有字段,必备参数则报错,非必备参数则返回null
  88. if (!parameterAnnotation.parseAllFields()) {
  89. // 如果是必传参数抛异常
  90. if (parameterAnnotation.required()) {
  91. throw new IllegalArgumentException(String.format("required param %s is not present", key));
  92. }
  93. // 否则返回null
  94. return null;
  95. }
  96. // 非基本类型,允许解析,将外层属性解析
  97. Object result;
  98. try {
  99. result = JSON.parseObject(jsonObject.toString(), parameterType);
  100. } catch (JSONException jsonException) {
  101. // TODO:: 异常处理返回null是否合理?
  102. result = null;
  103. }
  104. // 如果非必要参数直接返回,否则如果没有一个属性有值则报错
  105. if (!parameterAnnotation.required()) {
  106. return result;
  107. } else {
  108. boolean haveValue = false;
  109. Field[] declaredFields = parameterType.getDeclaredFields();
  110. for (Field field : declaredFields) {
  111. field.setAccessible(true);
  112. if (field.get(result) != null) {
  113. haveValue = true;
  114. break;
  115. }
  116. }
  117. if (!haveValue) {
  118. throw new IllegalArgumentException(String.format("required param %s is not present", key));
  119. }
  120. return result;
  121. }
  122. }
  123. /**
  124. * 基本类型解析
  125. */
  126. private Object parsePrimitive(String parameterTypeName, Object value) {
  127. final String booleanTypeName = "boolean";
  128. if (booleanTypeName.equals(parameterTypeName)) {
  129. return Boolean.valueOf(value.toString());
  130. }
  131. final String intTypeName = "int";
  132. if (intTypeName.equals(parameterTypeName)) {
  133. return Integer.valueOf(value.toString());
  134. }
  135. final String charTypeName = "char";
  136. if (charTypeName.equals(parameterTypeName)) {
  137. return value.toString().charAt(0);
  138. }
  139. final String shortTypeName = "short";
  140. if (shortTypeName.equals(parameterTypeName)) {
  141. return Short.valueOf(value.toString());
  142. }
  143. final String longTypeName = "long";
  144. if (longTypeName.equals(parameterTypeName)) {
  145. return Long.valueOf(value.toString());
  146. }
  147. final String floatTypeName = "float";
  148. if (floatTypeName.equals(parameterTypeName)) {
  149. return Float.valueOf(value.toString());
  150. }
  151. final String doubleTypeName = "double";
  152. if (doubleTypeName.equals(parameterTypeName)) {
  153. return Double.valueOf(value.toString());
  154. }
  155. final String byteTypeName = "byte";
  156. if (byteTypeName.equals(parameterTypeName)) {
  157. return Byte.valueOf(value.toString());
  158. }
  159. return null;
  160. }
  161. /**
  162. * 基本类型包装类解析
  163. */
  164. private Object parseBasicTypeWrapper(Class<?> parameterType, Object value) {
  165. if (Number.class.isAssignableFrom(parameterType)) {
  166. Number number = (Number) value;
  167. if (parameterType == Integer.class) {
  168. return number.intValue();
  169. } else if (parameterType == Short.class) {
  170. return number.shortValue();
  171. } else if (parameterType == Long.class) {
  172. return number.longValue();
  173. } else if (parameterType == Float.class) {
  174. return number.floatValue();
  175. } else if (parameterType == Double.class) {
  176. return number.doubleValue();
  177. } else if (parameterType == Byte.class) {
  178. return number.byteValue();
  179. }
  180. } else if (parameterType == Boolean.class) {
  181. return value.toString();
  182. } else if (parameterType == Character.class) {
  183. return value.toString().charAt(0);
  184. }
  185. return null;
  186. }
  187. /**
  188. * 判断是否为基本数据类型包装类
  189. */
  190. private boolean isBasicDataTypes(Class clazz) {
  191. Set<Class> classSet = new HashSet<>();
  192. classSet.add(Integer.class);
  193. classSet.add(Long.class);
  194. classSet.add(Short.class);
  195. classSet.add(Float.class);
  196. classSet.add(Double.class);
  197. classSet.add(Boolean.class);
  198. classSet.add(Byte.class);
  199. classSet.add(Character.class);
  200. return classSet.contains(clazz);
  201. }
  202. /**
  203. * 获取请求体JSON字符串
  204. */
  205. private String getRequestBody(NativeWebRequest webRequest) {
  206. HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
  207. // 有就直接获取
  208. String jsonBody = (String) webRequest.getAttribute(JSONBODY_ATTRIBUTE, NativeWebRequest.SCOPE_REQUEST);
  209. // 没有就从请求中读取
  210. if (jsonBody == null) {
  211. try {
  212. jsonBody = IOUtils.toString(servletRequest.getReader());
  213. webRequest.setAttribute(JSONBODY_ATTRIBUTE, jsonBody, NativeWebRequest.SCOPE_REQUEST);
  214. } catch (IOException e) {
  215. throw new RuntimeException(e);
  216. }
  217. }
  218. return jsonBody;
  219. }
  220. }

2、编写解析的方法注解:


  1. package com.chujianyun.web.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. /**
  7. * Controller中方法接收多个JSON对象
  8. *
  9. * @author 明明如月
  10. * @date 2018/08/27
  11. */
  12. @Target(ElementType.PARAMETER)
  13. @Retention(RetentionPolicy.RUNTIME)
  14. public @interface MultiRequestBody {
  15. /**
  16. * 是否必须出现的参数
  17. */
  18. boolean required() default true;
  19. /**
  20. * 当value的值或者参数名不匹配时,是否允许解析最外层属性到该对象
  21. */
  22. boolean parseAllFields() default true;
  23. /**
  24. * 解析时用到的JSON的key
  25. */
  26. String value() default "";
  27. }

3、在配置Bean中注入

特别注意: 如果加入本配置导致页面访问404 可以去掉 @EnableWebMvc注解


  1. package com.chujianyun.web.config;
  2. import com.chujianyun.web.bean.MultiRequestBodyArgumentResolver;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.http.converter.HttpMessageConverter;
  6. import org.springframework.http.converter.StringHttpMessageConverter;
  7. import org.springframework.web.method.support.HandlerMethodArgumentResolver;
  8. import org.springframework.web.servlet.config.annotation.EnableWebMvc;
  9. import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
  10. import java.nio.charset.Charset;
  11. import java.util.List;
  12. /**
  13. * 添加多RequestBody解析器
  14. * @author 明明如月
  15. * @date 2018/08/27
  16. */
  17. @Configuration
  18. @EnableWebMvc
  19. public class WebConfig extends WebMvcConfigurerAdapter {
  20. @Override
  21. public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
  22. argumentResolvers.add(new MultiRequestBodyArgumentResolver());
  23. }
  24. @Bean
  25. public HttpMessageConverter<String> responseBodyConverter() {
  26. return new StringHttpMessageConverter(Charset.forName("UTF-8"));
  27. }
  28. @Override
  29. public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
  30. super.configureMessageConverters(converters);
  31. converters.add(responseBodyConverter());
  32. }
  33. }

xml配置方式(感谢网友 "熔 岩"提供了的xml参考配置方式)


  1. <mvc:annotation-driven>
  2. <mvc:message-converters>
  3. <bean class="org.springframework.http.converter.StringHttpMessageConverter">
  4. <constructor-arg value="UTF-8"/>
  5. </bean>
  6. <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
  7. <property name="supportedMediaTypes">
  8. <list>
  9. <value>application/json</value>
  10. <value>text/html</value>
  11. <value>text/plain</value>
  12. </list>
  13. </property>
  14. <property name="fastJsonConfig" ref="fastJsonConfig"/>
  15. </bean>
  16. </mvc:message-converters>
  17. <mvc:argument-resolvers>
  18. <bean class="io.github.chujianyun.bean.MultiRequestBodyArgumentResolver"/>
  19. </mvc:argument-resolvers>
  20. </mvc:annotation-driven>

使用方法:


  1. package com.chujianyun.web.controller;
  2. import com.chujianyun.web.annotation.MultiRequestBody;
  3. import com.chujianyun.web.domain.Dog;
  4. import com.chujianyun.web.domain.User;
  5. import org.springframework.stereotype.Controller;
  6. import org.springframework.web.bind.annotation.RequestMapping;
  7. import org.springframework.web.bind.annotation.ResponseBody;
  8. /**
  9. * 演示控制器
  10. * @author 明明如月
  11. * @date 2018/08/27
  12. */
  13. @Controller
  14. @RequestMapping("/xhr/test")
  15. public class DemoController {
  16. @RequestMapping("/demo")
  17. @ResponseBody
  18. public String multiRequestBodyDemo1(@MultiRequestBody Dog dog, @MultiRequestBody User user) {
  19. System.out.println(dog.toString()+user.toString());
  20. return dog.toString()+";"+user.toString();
  21. }
  22. @RequestMapping("/demo2")
  23. @ResponseBody
  24. public String multiRequestBodyDemo2(@MultiRequestBody("dog") Dog dog, @MultiRequestBody User user) {
  25. System.out.println(dog.toString()+user.toString());
  26. return dog.toString()+";"+user.toString();
  27. }
  28. @RequestMapping("/demo3")
  29. @ResponseBody
  30. public String multiRequestBodyDemo3(@MultiRequestBody("dog") Dog dog, @MultiRequestBody("user") User user) {
  31. System.out.println(dog.toString()+user.toString());
  32. return dog.toString()+";"+user.toString();
  33. }
  34. @RequestMapping("/demo4")
  35. @ResponseBody
  36. public String multiRequestBodyDemo4(@MultiRequestBody("dog") Dog dog, @MultiRequestBody Integer age) {
  37. System.out.println(dog.toString() + age.toString());
  38. return dog.toString() + ";age属性为:"+age.toString();
  39. }
  40. @RequestMapping("/demo5")
  41. @ResponseBody
  42. public String multiRequestBodyDemo5(@MultiRequestBody("color") String color, @MultiRequestBody("age") Integer age) {
  43. return "color="+color + "; age=" + age;
  44. }
  45. @RequestMapping("/demo6")
  46. @ResponseBody
  47. public String multiRequestBodyDemo6(@MultiRequestBody("dog") Dog dog, @MultiRequestBody Integer age) {
  48. System.out.println(dog.toString() + age.toString());
  49. return dog.toString() + ";age属性为:"+age.toString();
  50. }
  51. @RequestMapping("/demo7")
  52. @ResponseBody
  53. public String multiRequestBodyDemo7(@MultiRequestBody Dog color2, @MultiRequestBody("age") Integer age) {
  54. return "color="+color2 + "; age=" + age;
  55. }
  56. @RequestMapping("/demo9")
  57. @ResponseBody
  58. public String multiRequestBodyDemo9( @MultiRequestBody Dog dog) {
  59. return dog.toString();
  60. }
  61. @RequestMapping("/demo10")
  62. @ResponseBody
  63. public String multiRequestBodyDemo10( @MultiRequestBody(parseAllFields = false,required = false) Dog dog) {
  64. return dog.toString();
  65. }
  66. @RequestMapping("/testList")
  67. @ResponseBody
  68. public String multiRequestBodyDemo1(@MultiRequestBody List test, @MultiRequestBody String str) {
  69. return test.toString() + str;
  70. }
  71. }

两个实体:


  1. package com.chujianyun.web.domain;
  2. /**
  3. * @author 明明如月
  4. * @date 2018/08/27
  5. */
  6. public class Dog {
  7. private String name;
  8. private String color;
  9. public String getName() {
  10. return name;
  11. }
  12. public void setName(String name) {
  13. this.name = name;
  14. }
  15. public String getColor() {
  16. return color;
  17. }
  18. public void setColor(String color) {
  19. this.color = color;
  20. }
  21. @Override
  22. public String toString() {
  23. return "Dog{" +
  24. "name='" + name + '\'' +
  25. ", color='" + color + '\'' +
  26. '}';
  27. }
  28. }

  1. package com.chujianyun.web.domain;
  2. /**
  3. * @author 明明如月
  4. * @date 2018/08/27
  5. */
  6. public class User {
  7. private String name;
  8. private Integer age;
  9. public String getName() {
  10. return name;
  11. }
  12. public void setName(String name) {
  13. this.name = name;
  14. }
  15. public Integer getAge() {
  16. return age;
  17. }
  18. public void setAge(Integer age) {
  19. this.age = age;
  20. }
  21. @Override
  22. public String toString() {
  23. return "User{" +
  24. "name='" + name + '\'' +
  25. ", age=" + age +
  26. '}';
  27. }
  28. }

效果:

demo

demo2

demo3

参考文章:https://stackoverflow.com/questions/12893566/passing-multiple-variables-in-requestbody-to-a-spring-mvc-controller-using-ajax

如果觉得本文对你有帮助,欢迎点赞评论,欢迎关注我,我将努力创作更多更好的文章。

                     原文地址:https://blog.csdn.net/w605283073/article/details/82119284           </div>

SpringBoot Controller 中使用多个@RequestBody的正确姿势的更多相关文章

  1. SpringBoot Controller 中 HttpServletRequest ServletInputStream 读取不到数据该怎么处理

    在Springboot程序启动后,会默认添加OrderedCharacterEncodingFilter和HiddenHttpMethodFilter过滤器.在HiddenHttpMethodFilt ...

  2. springboot使用百度富文本UEditor遇到的问题一览(springboot controller中request.getInputStream无法读取)

    先吐槽一下UEditor作为一个前端的js类库,非要把4种后端的代码给出来,而实际生产中用的框架不同,其代码并不具有适应性.(通常类似其它项目仅仅是给出数据交互的规范.格式,后端实现就可以自由定制) ...

  3. 在日志中记录Java异常信息的正确姿势

    遇到的问题 今天遇到一个线上的BUG,在执行表单提交时失败,但是从程序日志中看不到任何异常信息. 在Review源代码时发现,当catch到异常时只是输出了e.getMessage(),如下所示: l ...

  4. angular4.0中form表单双向数据绑定正确姿势

    issue:用[(ngModel)]="property"指令双向数据绑定,报错. reason1:使用ngModel绑定数据需要注入FormsModule模块,在app.modu ...

  5. SpringBoot 中 @RequestBody的正确使用方法

    SpringBoot 中 @RequestBody的正确使用方法 最近在接收一个要离职同事的工作,接手的项目是用SpringBoot搭建的,其中看到了这样的写法: @RequestMapping(&q ...

  6. SpringBoot12 QueryDSL01之QueryDSL介绍、springBoot项目中集成QueryDSL

    1 QueryDSL介绍 1.1 背景 QueryDSL的诞生解决了HQL查询类型安全方面的缺陷:HQL查询的扩展需要用字符串拼接的方式进行,这往往会导致代码的阅读困难:通过字符串对域类型和属性的不安 ...

  7. springboot项目中使用maven resources

    maven resource 组件可以把pom的变量替换到相关的resouces目录中的资源文件变量 示例项目:内容中心 (文章管理)  生成jar包,生成docker ,生成k8s文件 1.项目结构 ...

  8. SpringBoot Controller接收参数的几种方式盘点

    本文不再更新,可能存在内容过时的情况,实时更新请移步我的新博客:SpringBoot Controller接收参数的几种方式盘点: SpringBoot Controller接收参数的几种常用方式盘点 ...

  9. SpringBoot:SpringBoot项目中 HttpServletRequest ServletInputStream 读取不到文件数据流

    在Springboot程序启动后,会默认添加OrderedCharacterEncodingFilter和HiddenHttpMethodFilter过滤器.在HiddenHttpMethodFilt ...

随机推荐

  1. 05、python的基础-->字典的增、删、改、查

    1.字典的增 dict = {'age':19,'name':'老王','hobby':'girl'} dict['sex'] = 'boy' #没有键值对,直接添加 dict[' #有键值对,覆盖值 ...

  2. nodeType介绍及应用示例

    一,DOM中的节点类型介绍 DOM将一份文档抽象为一棵树,而树又由众多不同类型的节点构成. 元素节点是DOM中的最小单位节点,它包括了各种标签,比如表示段落的p,表示无序列表的ul等. 文本节点总是被 ...

  3. CentOS 7 配置SFTP

    目前越来越多的FTP客户端软件开始支持SSH协议上传和下载文件,这种协议方式就是SFTP. SFTP的优势主要有两点,一是不需要再配置个FTP服务端:二是SSH协议是安全传输,上传和下载是经过加密的. ...

  4. 微信小程序app.json文件常用全局配置

    小程序根目录下的 app.json 文件用来对微信小程序进行全局配置,决定页面文件的路径.窗口表现.设置网络超时时间.设置多 tab 等. JOSN文件不允许注释,下面为了学习加上注释,粘贴需要的片段 ...

  5. hbuilder模拟器端口

    模拟器 | 端口 夜神安卓模拟器夜神安卓模拟器     62001 逍遥安卓模拟器逍遥安卓模拟器     21503 BlueStacks(蓝叠安卓模拟器)BlueStacks(蓝叠安卓模拟器)    ...

  6. GitHub-Hexo-Blog 集成Gitalk评论插件

    在本文)末尾可查看先查看效果: 1. 新建New OAuth App 在github中,Settings / Develpoer settings OAuth Apps / New OAuth App ...

  7. xshell安装错解决方案

    之前安装过XShell后来因为各种原因不能使用了,卸载和再次安装的时候安装一直失败.研究了好久终于找到解决方案. 只需要删除在C:\Program Files (x86)\InstallShield ...

  8. 【Luogu】【关卡2-7】深度优先搜索(2017年10月)【AK】【题解没写完】

    任务说明:搜索可以穷举各种情况.很多题目都可以用搜索完成.就算不能,搜索也是骗分神器. P1219 八皇后 直接dfs.对角线怎么判断:同一条对角线的横纵坐标的和或者差相同. #include < ...

  9. python_异常

    异常的概念 程序在运行时,如果 Python 解释器 遇到 到一个错误,会停止程序的执行,并且提示一些错误信息,这就是 异常 程序停止执行并且提示错误信息 这个动作,我们通常称之为:抛出(raise) ...

  10. Python中Class中的object是什么意思?

    https://stackoverflow.com/a/2588667/8189120 In short, it sets free magical ponies. In long, Python 2 ...