最近因为公司决定接入yapi,从swagger中导入接口信息,为了更方便的管理,设定了一些规则。然而,在进行改造的过程中却出现了一些问题。
-
tags为中文时导致点击api无法展开详细信息
Swagger最新版本已解决了
点击api没反应,首先想到是不是前端的点击事件报错了,打开控制台查看,有一条错误日志
image-20191104170224282.png这条日志看上去和点击事件没什么关系,是乱码的问题,但很有可能是因为乱码导致点击事件监听不到,用chrome查看元素看看。
可以看出有两个地方出现了乱码,a标签地址和content中的id,在点击时,url能正常解析,那么问题就出在content中了,在content中,可以看到display为none,这个模块其实是不生效的,将display属性去除,果然api信息展示出来了,现在基本可以确定是content模块拼接id时乱码导致的了。在chrome中将乱码修复为中文,问题解决。
但我们不可能每次都去chrome上修改,我们需要找到js出问题的地方将其修复。
找到jar包中的swagger-ui.min.js
,将代码美化后,搜索_content
,找到这样一段代码
})) ? a : "") + "</span></a>\n </li>\n </ul>\n </div>\n <div class='content' id='" + l((n.sanitize || t && t.sanitize || s).call(o, null != t ? t.encodedParentId : t, {
name: "sanitize",
hash: {},
data: i
})) + "_" + l((n.sanitize || t && t.sanitize || s).call(o, null != t ? t.nickname : t, {
name: "sanitize",
hash: {},
data: i
})) + "_content' style='display:none'>\n" + (null != (a = n["if"].call(o, null != t ? t.deprecated : t, {
这个地方的id使用的是t.encodedParentId,改为t.parentId即可
// t.encodedParentId因为做了次encodeURIComponent导致乱码
this.model.encodedParentId = encodeURIComponent(this.parentId)
找到本地仓库jar包位置,重新打包替换,问题解决。
- api参数为optional<Enum>导致的NPE问题
在另外一个项目,打开swagger页面就报错,查看日志出现NPE问题
java.lang.NullPointerException: null
at io.swagger.models.refs.GenericRef.computeRefFormat(GenericRef.java:93)
at io.swagger.models.refs.GenericRef.<init>(GenericRef.java:14)
at io.swagger.models.RefModel.set$ref(RefModel.java:72)
at io.swagger.models.RefModel.<init>(RefModel.java:23)
at springfox.documentation.swagger2.mappers.ParameterMapper.fromModelRef(ParameterMapper.java:83)
at springfox.documentation.swagger2.mappers.ParameterMapper.bodyParameter(ParameterMapper.java:46)
at springfox.documentation.swagger2.mappers.ParameterMapper.mapParameter(ParameterMapper.java:41)
at springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl.parameterListToParameterList(ServiceModelToSwagger2MapperImpl.java:199)
at springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl.mapOperation(ServiceModelToSwagger2MapperImpl.java:128)
at springfox.documentation.swagger2.mappers.ServiceModelToSwagger2Mapper.mapOperations(ServiceModelToSwagger2Mapper.java:174)
at springfox.documentation.swagger2.mappers.ServiceModelToSwagger2Mapper.mapApiListings(ServiceModelToSwagger2Mapper.java:156)
at springfox.documentation.swagger2.mappers.ServiceModelToSwagger2MapperImpl.mapDocumentation(ServiceModelToSwagger2MapperImpl.java:52)
at springfox.documentation.swagger2.web.Swagger2Controller.getDocumentation(Swagger2Controller.java:82)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at com.xingren.reaper.spring.rs.filters.HttpLogFilter.doFilterInternal(HttpLogFilter.java:33)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at com.xingren.reaper.spring.rs.filters.RequestReplaceFilter.doFilterInternal(RequestReplaceFilter.java:28)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:208)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:347)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:263)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:108)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:504)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:677)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
感觉莫名奇妙,日志也看不出啥,只能一步步打断点调试了。找到Swagger2Controller,一步步调试过去,最后定位到一个参数的modelRef的type为null导致的。
这个type记录的是入参的类型,枚举类应该是Integer类型,又由于这个参数套了一层Optional,导致Swagger无法解析该类型参数,导致NPE。
Swagger的参数解析是在项目启动时做的,在公司封装的类库中提供了OptionalExpandParameterBuilder和EnumExpandedParameterBuilder两个类去解析Optional和Enum类型的参数。看下OptionalExpandParameterBuilder源码。
@Component
@Order(SWAGGER_PLUGIN_ORDER + 1)
public class OptionalExpandParameterBuilder implements ExpandedParameterBuilderPlugin {
@Override
public void apply(ParameterExpansionContext context) {
Field field = context.getField().getRawMember();
Class<?> fieldType = field.getType();
if (!fieldType.isAssignableFrom(Optional.class)) {
return;
}
ParameterizedType type = (ParameterizedType) field.getGenericType();
// 在这个地方创建modelRef,关键就在于typeNameFor这个方法返回null
context.getParameterBuilder()
.modelRef(new ModelRef(typeNameFor(type.getActualTypeArguments()[0])));
}
@Override
public boolean supports(DocumentationType delimiter) {
return pluginDoesApply(delimiter);
}
}
关键就在于typeNameFor
这个方法返回null。
public class Types {
// 可解析的类型
private static final Set<String> baseTypes = Sets.newHashSet(new String[]{"int", "date", "string", "double", "float", "boolean", "byte", "object", "long", "date-time", "file", "biginteger", "bigdecimal"});
private static final Map<Type, String> typeNameLookup;
private Types() {
throw new UnsupportedOperationException();
}
public static String typeNameFor(Type type) {
return (String)typeNameLookup.get(type);
}
public static boolean isBaseType(String typeName) {
return baseTypes.contains(typeName);
}
public static boolean isBaseType(ResolvedType type) {
return baseTypes.contains(typeNameFor(type.getErasedType()));
}
public static boolean isVoid(ResolvedType returnType) {
return Void.class.equals(returnType.getErasedType()) || Void.TYPE.equals(returnType.getErasedType());
}
static {
// 对类型的映射
typeNameLookup = ImmutableMap.builder().put(Long.TYPE, "long").put(Short.TYPE, "int").put(Integer.TYPE, "int").put(Double.TYPE, "double").put(Float.TYPE, "float").put(Byte.TYPE, "byte").put(Boolean.TYPE, "boolean").put(Character.TYPE, "string").put(Date.class, "date-time").put(java.sql.Date.class, "date").put(String.class, "string").put(Object.class, "object").put(Long.class, "long").put(Integer.class, "int").put(Short.class, "int").put(Double.class, "double").put(Float.class, "float").put(Boolean.class, "boolean").put(Byte.class, "byte").put(BigDecimal.class, "bigdecimal").put(BigInteger.class, "biginteger").put(Currency.class, "string").put(UUID.class, "string").put(MultipartFile.class, "file").build();
}
}
从这里就可以看出来了,baseTypes
中包含所有可解析的类型,typeNameLookup
对baseTypes
类型映射,这里不包含Enum类型所以返回null。
解决方法,我们只需仿造OptionalExpandParameterBuilder写个解析器OK了
@Component
@Order(SWAGGER_PLUGIN_ORDER + 2)
public class OptionalEnumExpandParameterBuilder implements ExpandedParameterBuilderPlugin {
@Override
public void apply(ParameterExpansionContext context) {
Field field = context.getField().getRawMember();
Class<?> fieldType = field.getType();
if (!fieldType.isAssignableFrom(Optional.class)) {
return;
}
ParameterizedType type = (ParameterizedType) field.getGenericType();
Class<?> actualTypeArgument = (Class) type.getActualTypeArguments()[0];
if (!actualTypeArgument.isEnum()) {
return;
}
Map<Enum<?>, Object> enumCodeMap = getEnumAndValue(actualTypeArgument, "code");
Map<Enum<?>, Object> enumNameMap = getEnumAndValue(actualTypeArgument, "name");
List<String> codeValues = enumCodeMap.values().stream().map(Object::toString).collect(toList());
List<String> descValues = Lists.newArrayList();
enumCodeMap.forEach((anEnum, o) -> descValues.add(format("%s-%s", o.toString(), enumNameMap.get(anEnum))));
context.getParameterBuilder()
.modelRef(new ModelRef(typeNameFor(Integer.class)))
.allowableValues(new AllowableListValues(codeValues, "LIST"));
com.google.common.base.Optional<ApiModelProperty> apiModelPropertyOptional =
findApiModePropertyAnnotation(field);
if (apiModelPropertyOptional.isPresent()) {
context.getParameterBuilder()
.description(format("%s %s", apiModelPropertyOptional.get().value(), descValues));
}
}
@Override
public boolean supports(DocumentationType delimiter) {
return pluginDoesApply(delimiter);
}
}