免费的网站源码去哪下载,江苏网站备案,做网站的去哪找客户,服装设计网站大全作者简介#xff1a;大家好#xff0c;我是smart哥#xff0c;前中兴通讯、美团架构师#xff0c;现某互联网公司CTO 联系qq#xff1a;184480602#xff0c;加我进群#xff0c;大家一起学习#xff0c;一起进步#xff0c;一起对抗互联网寒冬 上一篇我们通过编写MyB…作者简介大家好我是smart哥前中兴通讯、美团架构师现某互联网公司CTO 联系qq184480602加我进群大家一起学习一起进步一起对抗互联网寒冬 上一篇我们通过编写MyBatis的转换器最终完成枚举在DAO层和数据库之间的转换 现在让我们把目光往前移思考一下如何编写SpringMVC的转换器完成前端与Controller层的枚举转换。 环境准备
目录结构 pom.xml小册使用的版本都是2.3.4但今天遇到坑了后面会提到
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.3.4.RELEASE/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.example/groupIdartifactIdspringboot_enum/artifactIdversion0.0.1-SNAPSHOT/versionnamespringboot_enum/namedescriptionDemo project for Spring Boot/descriptionpropertiesjava.version1.8/java.version/propertiesdependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scopeexclusionsexclusiongroupIdorg.junit.vintage/groupIdartifactIdjunit-vintage-engine/artifactId/exclusion/exclusions/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/projectPOJO
Data
public class UserDTO {/*** 姓名*/private String name;/*** 年龄*/private Integer age;/*** 用户类型枚举*/private UserTypeEnum userType;
}
UserTypeEnum
Getter
public enum UserTypeEnum {STUDENT(1, 学生),TEACHER(2, 老师),;private final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type type;this.desc desc;}
}
Controller
Slf4j
RestController
RequestMapping(/api/web/user)
public class UserController {GetMapping(/get)public void get(UserDTO userDTO) {log.info(userDTO.toString());}PostMapping(/postForm)public void postForm(UserDTO userDTO) {log.info(userDTO.toString());}PostMapping(/postJson)public void postJson(RequestBody UserDTO userDTO) {log.info(userDTO.toString());}} 关于GET与POST
首先要和大家交代一下常见的请求方式分两大类不算REST风格
GETPOST
GET和POST有个很大区别是GET请求的参数放在请求行而POST请求的参数放在请求体(Body)。 另外POST请求又细分很多种
form-datax-www-form-urlencodedjson 如果你足够细心平时使用Postman时就会注意到以上三种POST请求形式虽不同但参数都在Body 我们会在JavaWeb章节详细介绍它们的区别这里按下不表。 需要注意的是从后端接口参数的格式看POST请求中的表单提交方式和GET请求是很相似的 所以本文在测试时分为两个阵营
GET与POST表单POST JSON 测试的方向分为
请求入响应出 开始测试之前再来回顾一下我们写的枚举 枚举名称(name)分别叫STUDENTTEACHER之前分析过所有的枚举类默认继承Enum而Enum重写了toString() 所以当我们打印STUDENT或TEACHER对象时最终会打印: STUDENT、TEACHER。 UserTypeEnum有两个字段type和desc抽象父类Enum也有两个字段ordinal序号从0开始和name枚举名称 OK接下来让我们开始测试。 请求反序列化
测试请求时我们的关注点是前端传入userType:STUDENT后端是如何变成UserTypeEnum对象的。 测试GET与POST表单 传入Enum.name转换成功
GET请求 POST表单 很明显前端传STUDENT、TEACHER等枚举名称(name)时SpringMVC能自动帮我们转为对应的枚举对象而在实际打印时由于调用了toString()所以显示userTypeSTUDENT。 那么为什么枚举名称name为什么会自动转为枚举对象UserTypeEnum呢我们先不管SpringMVC怎么做到的通过断点很容易发现SpringMVC在解析STUDENT这个字符串时最终调用了Enum#valueOf()然后根据name获取枚举对象 传入Enum.ordinal转换失败 无论是GET还是POST表单传入0或1都失败了ordinal从0开始也就是说SpringMVC默认不支持根据ordinal转换
Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object userDTO on field userType: rejected value [1]; codes [typeMismatch.userDTO.userType,typeMismatch.userType,typeMismatch.com.bravo.demo.enums.UserTypeEnum,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.userType,userType]; arguments []; default message [userType]]; default message [Failed to convert property value of type java.lang.String to required type com.bravo.demo.enums.UserTypeEnum for property userType; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [com.bravo.demo.enums.UserTypeEnum] for value 1; nested exception is java.lang.IllegalArgumentException: No enum constant com.bravo.demo.enums.UserTypeEnum.1]] 特别注意最后的异常信息似乎在哪见到过 也就是说对于GET/POST表单请求SpringMVC都是根据valueOf()来匹配枚举对象的。 也即是说对于GET和POST表单请求而言如果想正确的反序列化String转为Enum对象前端只能传Enum.name。 传入UserTypeEnum.type转换失败
理由如上 传入UserTypeEnum.desc转换失败
理由如上 默认的ConverterFactory
对于前端来说他们可能更喜欢传递枚举内部的字段比如UserTypeEnum.type而不是Enum.name。有没有办法更改SpringMVC的默认行为当前端传递userType1时把1转为UserTypeEnum的“学生”对象呢 要解决这个问题可以分两步
了解GET/POST表单请求时SpringMVC默认的转换机制改写这个机制 由于我们已经知道整个请求链路的终点是调用Enum#valueOf()进行转换于是给valueOf()打上断点 省略中间的步骤根据调用链进行反推很快定位到AbstractPropertyAccessor#setPropertyValues() 这是个for循环它拿到了UserDTO的所有属性并逐个进行赋值。比如截图的代码显示SpringMVC正在给UserDTO.userType字段赋值。 再往下走几步会看到GenericConversionService#convert() 找到converter后调用converter的convert()方法进行值转换 最终把转换后的值设置给UserDTO.userType。 我们发现SpringMVC默认的枚举转换器是StringToEnumConverterFactory 它的convert()方法正好调用了Enum.valueOf()所以GET/POST表单请求时只能传Enum.name至此真相大白。 整个流程是
前端发起请求传递userTypeSTUDENT从Tomcat的Servlet到SpringMVC的Controller中间要经过很多类和方法SpringMVC会解析入参对象的每一个字段选取合适的ConverterFactory为其进行转换默认使用StringToEnumConverterFactory为枚举类型进行转换即调用Enum.valueOf(name) 有了上面的铺垫关于GET/POST表单请求时如何自定义枚举入参转换器已经很明确了。 自定义EnumConverterFactory
/*** 自定义枚举转换器直接抄StringToEnumConverterFactory** author mx*/
public final class MyEnumConverterFactory implements ConverterFactoryString, Enum {Overridepublic T extends Enum ConverterString, T getConverter(ClassT targetType) {return new StringToEnum(targetType);}private static class StringToEnumT extends Enum implements ConverterString, T {private final ClassT enumType;public StringToEnum(ClassT enumType) {this.enumType enumType;}/*** StringToEnumConverterFactory默认是调用Enum.valueOf()也就是根据Enum.name匹配* 我们改成根据Enum.ordinal匹配** param source* return*/Overridepublic T convert(String source) {if (source.isEmpty()) {// Its an empty enum identifier: reset the enum value to null.return null;}for (T enumObject : enumType.getEnumConstants()) {if (source.equals(String.valueOf(enumObject.ordinal()))) {return enumObject;}}return null;}}}
把它加到请求链路中
Configuration
public class MvcConfig implements WebMvcConfigurer {Overridepublic void addFormatters(FormatterRegistry registry) {// 把我们自定义的枚举转换器添加到Spring容器Spring容器会把它加入到SpringMVC的拦截链路中registry.addConverterFactory(new MyEnumConverterFactory());}
}
测试 特别特别注意把MyEnumConverterFactory加入调用链后jackson原本的StringToEnumConverterFactory就不起作用了此时前端传入STUDENT、TEACHER将无法成功解析。 改进自定义枚举转换器
上面这样还是无法满足我们的需求我们只是把原先默认支持Enum.name改为Enum.ordinal。 部分同学可能有疑问你刚才为什么不直接在上面的ConverterFactory中调用getType()或者getDesc()呢 不是我不想而是不好这样做。两点理由
getType()/getDesc不够通用项目中其他枚举可能叫getValue()/getDescription()最重要的是class MyEnumConverterFactory implements ConverterFactoryString, Enum使用Enum限定内部元素只能使用父类Enum的方法无法直接调用getType()等方法 解决办法有两个
抽取公共的IEnum接口强制指定按哪个字段反序列化使用注解反射 方案1抽取IEnum接口强制指定反序列化字段
IEum接口
/*** 统一的枚举接口** author mx*/
public interface IEnumT {/*** 强制指定按哪个字段进行反序列化** return*/T getValue();}
让UserTypeEnum实现IEnum
Getter
public enum UserTypeEnum implements IEnumString {STUDENT(1, 学生),TEACHER(2, 老师),;private final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type type;this.desc desc;}/*** 强制指定按哪个字段进行反序列化** return*/Overridepublic String getValue() {return this.desc;}
}
改写MyEnumConverterFactory
/*** 自定义枚举转换器配合统一枚举接口IEnum** author mx*/
public final class MyEnumConverterFactory implements ConverterFactoryString, IEnum {Overridepublic T extends IEnum ConverterString, T getConverter(ClassT targetType) {return new StringToEnum(targetType);}private static class StringToEnumT extends IEnum implements ConverterString, T {private final ClassT enumType;public StringToEnum(ClassT enumType) {this.enumType enumType;}Overridepublic T convert(String source) {if (source.isEmpty()) {// Its an empty enum identifier: reset the enum value to null.return null;}for (T enumObject : enumType.getEnumConstants()) {// 默认项目中所有Enum都实现了IEnum那么必然有getValue()if (source.equals(String.valueOf(enumObject.getValue()))) {return enumObject;}}return null;}}}
测试 方案2注解反射
/*** 指定反序列化字段** author mx*/
Target({ElementType.FIELD, ElementType.METHOD})
Retention(RetentionPolicy.RUNTIME)
public interface MyJsonCreator {
}
/*** 自定义枚举转换器还是用原生的Enum* 使用分三步* 1.自定义一个注解假设叫MyJsonCreator* 2.读取注解* 3.解析注解字段的值找到匹配的枚举对象* p* MyEnumConverterFactory主要负责第2、3步** author mx*/
public final class MyEnumConverterFactory implements ConverterFactoryString, Enum {Overridepublic T extends Enum ConverterString, T getConverter(ClassT targetType) {return new StringToEnum(targetType);}private static class StringToEnumT extends Enum implements ConverterString, T {private final ClassT enumType;public StringToEnum(ClassT enumType) {this.enumType enumType;}Overridepublic T convert(String source) {if (source.isEmpty()) {// Its an empty enum identifier: reset the enum value to null.return null;}try {for (T enumObject : enumType.getEnumConstants()) {Field[] declaredFields enumObject.getClass().getDeclaredFields();for (Field declaredField : declaredFields) {// 读取MyJsonCreator标注的字段if (declaredField.isAnnotationPresent(MyJsonCreator.class)) {declaredField.setAccessible(true);// 读取对应的字段valueObject fieldValue declaredField.get(enumObject);// 匹配并返回对于的Enumif (source.equals(String.valueOf(fieldValue))) {return enumObject;}}}}} catch (IllegalAccessException e) {e.printStackTrace();}return null;}}}
在UserTypeEnum中使用
Getter
public enum UserTypeEnum {STUDENT(1, 学生),TEACHER(2, 老师),;MyJsonCreatorprivate final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type type;this.desc desc;}} 测试POST JSON
为了不干扰后续的实验请大家先把自定义的枚举转换器注释掉 传入Enum.name、Enum.ordinal转换成功
测试POST JSON 我们惊奇的发现SpringMVC默认就支持了Enum.name和Enum.ordinal的转换但对于子类UserTypeEnum的特有字段type、desc是不识别的。 HttpMessageConverter与jackson
有部分同学可能有点晕了来捋一捋
GET/POST表单默认使用StringToEnumConverterFactory只支持Enum.namePOST JSON默认支持Enum.name、Enum.ordinal 很明显POST JSON和GET/POST表单使用的不是同一个转换器并且从上面的异常信息可以捕捉到一丝丝信息
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type com.bravo.demo.enums.UserTypeEnum from String 学生: not one of the values accepted for Enum class: [TEACHER, STUDENT]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type com.bravo.demo.enums.UserTypeEnum from String 学生: not one of the values accepted for Enum class: [TEACHER, STUDENT]
at [Source: (PushbackInputStream); line: 4, column: 17] (through reference chain: com.bravo.demo.pojo.UserDTO[userType])] 市面上常见的3种JSON转换工具
jacksonSpringBoot内置fastjson阿里gson谷歌 SpringBoot默认使用jackson作为JSON转换工具比如我们经常会用的ObjectMapper其实就是jackson的。 而JSON转换工具的作用点有两个
JSON请求RequestBody响应ResponseBody GET或POST表单请求由于参数并不是JSON形式所以用不到jackson只需要实现ConverterFactory 而POST JSON请求则需要实现HttpMessageConverterjackson已经提供 请注意ConverterFactory和HttpMessageConverter两个接口的包路径都不一样并没有什么关联。 SpringMVC如何处理JSON请求
由于JSON请求本质是字符串所以必须要有反序列化的过程。SpringMVC对外提供了HttpMessageConverter接口用于处理JSON而SpringBoot内置的jackson提供了该接口的实现类MappingJackson2HttpMessageConverter 当一个JSON请求达到SpringMVC容器会根据为当前请求参数挑选合适的Converter 此时就轮到jackson的MappingJackson2HttpMessageConverter出场了。如果你跟着debug就会发现实际上大部分工作都是AbstractJackson2HttpMessageConverter干的jackson的主要贡献是提供了ObjectMapper实例及各种Serializer、Deserializer用于序列化和反序列化 AbstractJackson2HttpMessageConverter内部的ObjectMapper被赋值后通过构造器如果有请求到达SpringMVC它会调用ObjectMapperSerializer、Deserializer对参数进行转换。 比如EnumDeserializer默认支持转换Enum.name、Enum.ordinal 具体的源码就不在这里带大家跟读了我们会在Spring章节分析RequestBody时解释目前大家可以像下面截图一样打上断点然后用Postman分别传递数字(Enum.ordinal)或字符串(Enum.name)体会一下 你会发现jackson的EnumDeserializer默认的解析策略是
如果是字符串默认作为Enum.name解析 如果是数字则按ordinal解析 如果你刚好是从上一篇文章过来的就会发现jackson的策略和MyBatis很像都支持了Enum.name和Enum.ordinal的转换。那么如果前端传递的是UserTypeEnum.type或者UserTypeEnum.desc呢 JsonCreator自定义反序列化字段
好在jackson还提供了JsonCreator注解让我们自己指定反序列化的字段
Slf4j
Getter
public enum UserTypeEnum {STUDENT(1, 学生),TEACHER(2, 老师),;/*** 用JsonValue指定序列化字段后面再介绍不用管*/JsonValueprivate final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type type;this.desc desc;}/*** 静态方法JsonCreator指定根据哪个字段反序列化** param desc* return*/JsonCreatorpublic static UserTypeEnum getEnum(String desc) {for (UserTypeEnum item : values()) {if (item.getDesc().equals(desc)) {log.info(进来了, desc:{}, item:{}, desc, item.toString());return item;}}return null;}public static void main(String[] args) throws IOException {// 模拟Postman发送JSON请求ObjectMapper objectMapper new ObjectMapper();String json {\n \name\: \bravoPostJson\,\n \age\: 18,\n \userType\: \老师\\n };System.out.println(json);// 请求反序列化UserDTO userDTO objectMapper.readValue(json, UserDTO.class);System.out.println(userDTO);// 响应序列化String returnJson objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(userDTO);System.out.println(returnJson);}
}
结果
请求
{ name: bravoPostJson, age: 18, userType: 老师
} 接收
进来了, desc:老师, item:TEACHER
UserDTO(namebravoPostJson, age18, userTypeTEACHER) 响应
{ name : bravoPostJson, age : 18, userType : 2
}
看起来很完美啊但是你用Postman去请求就会报错。我调试了一晚上坑爹结果发现是当前SpringBoot版本问题2.3.3SpringBoot2.0.x是可以的估计2.3.3修改了jackson的默认设置 响应序列化
介绍完前端如何传入枚举参数入最后讲讲枚举如何响应给前端出。其实答案已经呼之欲出JsonValue但方案不止一种。 在测试前请大家修改Controller让接口返回UserDto
Slf4j
RestController
RequestMapping(/api/web/user)
public class UserController {GetMapping(/get)public UserDTO get(UserDTO userDTO) {log.info(userDTO.toString());return userDTO;}PostMapping(/postForm)public UserDTO postForm(UserDTO userDTO) {log.info(userDTO.toString());return userDTO;}PostMapping(/postJson)public UserDTO postJson(RequestBody UserDTO userDTO) {log.info(userDTO.toString());return userDTO;}}
把之前请求相关的配置先注释掉并把SpringBoot版本改为2.0.5 OK我们自定义MyEnumConverterFacotry注释后对于GET/POST表单请求重新使用默认的StringToEnumConverterFactory仅支持Enum.name反序列化。而POST JSON请求默认支持Enum.name和Enum.ordinal。 现在你可以认为代码都回到了最初创建SpringBoot项目的状态。由于这回是测试响应形式我们不关心入参所以统一传递大家都支持的Enum.name。 需要注意的是无论GET/POST表单还是POST JSON请求它们只是请求方式不同而响应形式其实都是JSON因为我们使用了RestController Controller ResponseBody。 所以对于响应只需测试其中任意一组即可。 至于使用了ResponseBody后SpringMVC如何处理返回值由于篇幅已经太长留到Spring部分再聊。但有一点可以肯定正如JSON请求那样JSON响应也会经过jackson的处理而且必然调用HttpMessageConverter的write()。 中间复杂的调用就跳过了直接看AbstractJackson2HttpMessageConverter#writeInternal() 即最终会调用objectWriter.writeValue(generator, value)进行序列化写入response缓冲区。我们注意到在调用writeValue()之前userType字段还是个UserTypeEnum对象 而writeValue()本身已经没有什么好分析了 所以为什么UserTypeEnum最终会变成userType STUDENT这和SpringMVC本身没什么关系取决于JSON转换工具怎么设计的而jackson默认就是调用Enum.name()。 如何改变jackson对枚举类型的默认序列化规则呢 方案1JsonValue
在需要序列化的字段上加JsonValue即可。特别注意对于POST JSON请求使用JsonValue必须配合使用JsonCreator否则会报错很难受 方案2全局设置SerializationFeature 做了上面的设置相当于告诉jackson序列化响应时调用对象的toString()即可相应地我们要重写toString()
/*** 自定义JSON响应时枚举字段的序列化行为调用toString()** return*/
Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {return builder - builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
}
Getter
public enum UserTypeEnum {STUDENT(1, 学生),TEACHER(2, 老师),;private final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type type;this.desc desc;}Overridepublic String toString() {return 没想到吧UserTypeEnum序列化后竟然是完全无关的文字~;}
}
测试GET响应 测试POST JSON响应 推荐方案
SpringMVC对请求和响应的处理原本就复杂再加上枚举使得整篇文章难度加大不少。很多同学可能有点晕这里总结一下并尝试给出我推荐的方案。 个人推荐的方案 请求
POST JSONJsonCreatorGET/POSTMyJsonCreator
响应
全局设置toString()作为序列化的值
/*** 指定GET/POST表单请求反序列化字段* POST JSON请求反序列字段请用jackson原生注解JsonCreator** author mx*/
Target({ElementType.FIELD, ElementType.METHOD})
Retention(RetentionPolicy.RUNTIME)
public interface MyJsonCreator {
}
Slf4j
Getter
public enum UserTypeEnum {STUDENT(1, 学生),TEACHER(2, 老师),;private final Integer type;/*** MyEnumConvertFactoryMyJsonCreator指定GET/POST表单请求根据哪个字段反序列化*/MyJsonCreatorprivate final String desc;UserTypeEnum(Integer type, String desc) {this.type type;this.desc desc;}/*** 静态方法JsonCreator指定POST JSON请求根据哪个字段反序列化** param desc* return*/JsonCreatorpublic static UserTypeEnum getEnum(String desc) {for (UserTypeEnum item : values()) {if (item.getDesc().equals(desc)) {log.info(进来了, desc:{}, item:{}, desc, item.toString());return item;}}return null;}/*** 统一序列化字段调用toString()返回** return*/Overridepublic String toString() {return String.valueOf(this.type);}
}
Configuration
public class MvcConfig implements WebMvcConfigurer {/*** 自定义GET/POST表单提交方式的入参反序列化规则** param registry*/Overridepublic void addFormatters(FormatterRegistry registry) {// 把我们自定义的枚举转换器添加到Spring容器Spring容器会把它加入到SpringMVC的拦截链路中registry.addConverterFactory(new MyEnumConverterFactory());}/*** 自定义JSON响应时枚举字段的序列化行为调用toString()** return*/Beanpublic Jackson2ObjectMapperBuilderCustomizer customizer() {return builder - builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);}}
/*** 自定义枚举转换器还是用原生的Enum* 使用分三步* 1.自定义一个注解假设叫JsonCreator* 2.读取注解* 3.解析注解字段的值找到匹配的枚举对象* p* MyEnumConverterFactory主要负责第2、3步** author mx*/
public final class MyEnumConverterFactory implements ConverterFactoryString, Enum {Overridepublic T extends Enum ConverterString, T getConverter(ClassT targetType) {return new StringToEnum(targetType);}private static class StringToEnumT extends Enum implements ConverterString, T {private final ClassT enumType;public StringToEnum(ClassT enumType) {this.enumType enumType;}Overridepublic T convert(String source) {if (source.isEmpty()) {// Its an empty enum identifier: reset the enum value to null.return null;}try {for (T enumObject : enumType.getEnumConstants()) {Field[] declaredFields enumObject.getClass().getDeclaredFields();for (Field declaredField : declaredFields) {// 读取MyJsonCreator标注的字段if (declaredField.isAnnotationPresent(MyJsonCreator.class)) {declaredField.setAccessible(true);// 读取对应的字段valueObject fieldValue declaredField.get(enumObject);// 匹配并返回对于的Enumif (source.equals(String.valueOf(fieldValue))) {return enumObject;}}}}} catch (IllegalAccessException e) {e.printStackTrace();}return null;}}}
我个人实际开发时无论是Controller层还是DAO层都习惯手动转换枚举。
作者简介大家好我是smart哥前中兴通讯、美团架构师现某互联网公司CTO 进群大家一起学习一起进步一起对抗互联网寒冬