Swagger使用过程中的一点问题

最近因为公司决定接入yapi,从swagger中导入接口信息,为了更方便的管理,设定了一些规则。然而,在进行改造的过程中却出现了一些问题。

  1. tags为中文时导致点击api无法展开详细信息

    Swagger最新版本已解决了

    点击api没反应,首先想到是不是前端的点击事件报错了,打开控制台查看,有一条错误日志

    image-20191104170224282.png

    这条日志看上去和点击事件没什么关系,是乱码的问题,但很有可能是因为乱码导致点击事件监听不到,用chrome查看元素看看。

image-20191104172013669.png

可以看出有两个地方出现了乱码,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包位置,重新打包替换,问题解决。

  1. 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。

image.png

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中包含所有可解析的类型,typeNameLookupbaseTypes类型映射,这里不包含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);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容