一、加入springfox依賴html
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.6.1</version> </dependency>
二、配置org.springframework.web.accept.ContentNegotiationManagerFactoryBean,不然會報錯。確保applicationContext.xml 中 xsi:schemaLocation 的 http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd 爲3.2以上版本java
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" /> <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean"> <property name="favorPathExtension" value="false" /> <property name="favorParameter" value="true" /> <property name="parameterName" value="mediaType" /> <property name="ignoreAcceptHeader" value="true"/> <property name="useJaf" value="false"/> <property name="defaultContentType" value="application/json" /> <property name="mediaTypes"> <map> <entry key="json" value="application/json" /> <entry key="xml" value="application/xml" /> </map> </property> </bean>
三、配置DispatcherServlet,DispatcherServlet的攔截鏈接必須爲 / ,或者沒法訪問springfox-swagger的資源web
<servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
四、在applicationContext.xml中配置 <mvc:default-servlet-handler/>,或者沒法訪問項目的靜態資源spring
<mvc:default-servlet-handler/>
五、在 applicationContext.xml 配置訪問springfox-swagger-ui 的靜態資源json
<mvc:resources mapping="swagger-ui.html" location="classpath:/META-INF/resources/" /> <mvc:resources mapping="/webjars/**" location="classpath:/META-INF/resources/webjars/" />
六、編寫swagger配置類,確保<context:component-scan/> 掃描獲得api
import java.util.ArrayList; import java.util.List; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.type.AnnotationMetadata; import org.springframework.stereotype.Controller; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import com.eduboss.utils.PropertiesUtils; import com.google.common.base.Predicate; import springfox.documentation.RequestHandler; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.ParameterBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.schema.ModelRef; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Parameter; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 @EnableWebMvc @ComponentScan(SwaggerConfig.controllerPackages) public class SwaggerConfig implements BeanFactoryPostProcessor { static final Logger LOG = LoggerFactory.getLogger(SwaggerConfig.class); //掃描哪些包的controller static final String controllerPackages = "com.*.controller"; static final boolean IS_ENABLED_SWAGGER = true; static final String SWAGGER_AUTH_KEY = "Swagger-Auth-User"; @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) beanFactory; String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames(); for (String name : beanDefinitionNames) { BeanDefinition beanDefinition = beanFactory.getBeanDefinition(name); if ("springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterExpander".equals(beanDefinition.getBeanClassName())) { //去掉原來的ModelAttributeParameterExpander,會致使嵌套死循環 listableBeanFactory.removeBeanDefinition(name); } //爲每一個controller添加一個分組 if (beanDefinition instanceof AnnotatedBeanDefinition) { AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition; AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata(); if (annotationMetadata.hasAnnotation(Controller.class.getName()) && isFromControllerPackages(annotationMetadata.getClassName())) { DocketFactory docketFactory = new DocketFactory(annotationMetadata.getClassName()); try { listableBeanFactory.registerSingleton(DocketFactory.class.getName() + "." + docketFactory.groupName(), docketFactory.getObject()); } catch (Exception e) { LOG.error("註冊DocketFactory失敗", e); } } } } } private boolean isFromControllerPackages(String className) { for (String pk : controllerPackages.split(",")) { if (className.startsWith(pk)) { return true; } } return false; } static class DocketFactory implements FactoryBean<Docket> { private String groupName; private String controllerClassName; public DocketFactory(String controllerClassName) { Objects.requireNonNull(controllerClassName, "controllerClassName can not be null"); this.groupName = controllerClassName.substring(controllerClassName.lastIndexOf(".") + 1); this.controllerClassName = controllerClassName; } public String groupName() { return this.groupName; } @Override public Docket getObject() throws Exception { return new Docket(DocumentationType.SWAGGER_2) .enable(IS_ENABLED_SWAGGER) .groupName(groupName) .forCodeGeneration(true).select() .apis(controller(controllerClassName)) .paths(PathSelectors.any()) .build() .globalOperationParameters(setHeaderToken()) .apiInfo(apiInfo()); } /**設置全局變量**/ private List<Parameter> setHeaderToken() { ParameterBuilder tokenPar = new ParameterBuilder(); List<Parameter> pars = new ArrayList<>(); tokenPar.name(SWAGGER_AUTH_KEY) .description("當前測試用戶帳號") .modelRef(new ModelRef("string")) .parameterType("header") .required(false) .build(); pars.add(tokenPar.build()); return pars; } private Predicate<RequestHandler> controller(String controllerClassName) { return new Predicate<RequestHandler>() { @Override public boolean apply(RequestHandler input) { return input.declaringClass().getName().startsWith(controllerClassName); } }; } @Override public Class<?> getObjectType() { return Docket.class; } @Override public boolean isSingleton() { return true; } private ApiInfo apiInfo() { ApiInfo apiInfo = new ApiInfoBuilder() .title("API 集成測試") .description(controllerClassName + " Swagger API Test") .version("2.0") .build(); return apiInfo; } } }
七、因爲咱們刪除掉原來的springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterExpander,因此咱們必須本身實現一個,不然會報錯。該類都是抄過來的,只是把會致使死循環的代碼去掉,有須要能夠從新實現expand方法。spring-mvc
import static com.google.common.base.Objects.equal; import static com.google.common.base.Predicates.and; import static com.google.common.base.Predicates.not; import static com.google.common.base.Predicates.or; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Sets.newHashSet; import static springfox.documentation.schema.Collections.collectionElementType; import static springfox.documentation.schema.Collections.isContainerType; import static springfox.documentation.schema.Types.typeNameFor; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.util.HashSet; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.fasterxml.classmate.ResolvedType; import com.fasterxml.classmate.members.ResolvedField; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; import com.google.common.collect.Lists; import springfox.documentation.builders.ParameterBuilder; import springfox.documentation.schema.Maps; import springfox.documentation.schema.Types; import springfox.documentation.schema.property.field.FieldProvider; import springfox.documentation.service.Parameter; import springfox.documentation.spi.schema.AlternateTypeProvider; import springfox.documentation.spi.service.contexts.DocumentationContext; import springfox.documentation.spi.service.contexts.ParameterExpansionContext; import springfox.documentation.spring.web.readers.parameter.ModelAttributeField; import springfox.documentation.spring.web.readers.parameter.ModelAttributeParameterExpander; @Component public class SwaggerModelAttributeParameterExpander extends ModelAttributeParameterExpander { private static final Logger LOG = LoggerFactory.getLogger(SwaggerModelAttributeParameterExpander.class); private final FieldProvider fieldProvider; @Autowired public SwaggerModelAttributeParameterExpander(FieldProvider fields) { super(fields); this.fieldProvider = fields; } @Override public List<Parameter> expand(final String parentName, final ResolvedType paramType, DocumentationContext documentationContext) { List<Parameter> parameters = Lists.newArrayList(); Set<String> beanPropNames = getBeanPropertyNames(paramType.getErasedType()); Iterable<ResolvedField> fields = FluentIterable.from(fieldProvider.in(paramType)) .filter(onlyBeanProperties(beanPropNames)); LOG.debug("Expanding parameter type: {}", paramType); AlternateTypeProvider alternateTypeProvider = documentationContext.getAlternateTypeProvider(); FluentIterable<ModelAttributeField> modelAttributes = FluentIterable.from(fields) .transform(toModelAttributeField(alternateTypeProvider)); FluentIterable<ModelAttributeField> expendables = modelAttributes.filter(not(simpleType())) .filter(not(recursiveType(paramType))); for (ModelAttributeField each : expendables) { LOG.debug("Attempting to expand expandable field: {}", each.getField()); //parameters.addAll( // expand(nestedParentName(parentName, each.getField()), each.getFieldType(), documentationContext)); } FluentIterable<ModelAttributeField> collectionTypes = modelAttributes .filter(and(isCollection(), not(recursiveCollectionItemType(paramType)))); for (ModelAttributeField each : collectionTypes) { LOG.debug("Attempting to expand collection/array field: {}", each.getField()); ResolvedType itemType = collectionElementType(each.getFieldType()); if (Types.isBaseType(itemType)) { parameters.add(simpleFields(parentName, documentationContext, each)); } else { //parameters // .addAll(expand(nestedParentName(parentName, each.getField()), itemType, documentationContext)); } } FluentIterable<ModelAttributeField> simpleFields = modelAttributes.filter(simpleType()); for (ModelAttributeField each : simpleFields) { parameters.add(simpleFields(parentName, documentationContext, each)); } return FluentIterable.from(parameters).filter(not(hiddenParameters())).toList(); } private Predicate<ModelAttributeField> recursiveCollectionItemType(final ResolvedType paramType) { return new Predicate<ModelAttributeField>() { @Override public boolean apply(ModelAttributeField input) { return equal(collectionElementType(input.getFieldType()), paramType); } }; } private Predicate<Parameter> hiddenParameters() { return new Predicate<Parameter>() { @Override public boolean apply(Parameter input) { return input.isHidden(); } }; } private Parameter simpleFields(String parentName, DocumentationContext documentationContext, ModelAttributeField each) { LOG.debug("Attempting to expand field: {}", each); String dataTypeName = Optional.fromNullable(typeNameFor(each.getFieldType().getErasedType())) .or(each.getFieldType().getErasedType().getSimpleName()); LOG.debug("Building parameter for field: {}, with type: ", each, each.getFieldType()); ParameterExpansionContext parameterExpansionContext = new ParameterExpansionContext(dataTypeName, parentName, each.getField(), documentationContext.getDocumentationType(), new ParameterBuilder()); return pluginsManager.expandParameter(parameterExpansionContext); } private Predicate<ModelAttributeField> recursiveType(final ResolvedType paramType) { return new Predicate<ModelAttributeField>() { @Override public boolean apply(ModelAttributeField input) { return equal(input.getFieldType(), paramType); } }; } private Predicate<ModelAttributeField> simpleType() { return and(not(isCollection()), not(isMap()), or(belongsToJavaPackage(), isBaseType(), isEnum())); } private Predicate<ModelAttributeField> isCollection() { return new Predicate<ModelAttributeField>() { @Override public boolean apply(ModelAttributeField input) { return isContainerType(input.getFieldType()); } }; } private Predicate<ModelAttributeField> isMap() { return new Predicate<ModelAttributeField>() { @Override public boolean apply(ModelAttributeField input) { return Maps.isMapType(input.getFieldType()); } }; } private Predicate<ModelAttributeField> isEnum() { return new Predicate<ModelAttributeField>() { @Override public boolean apply(ModelAttributeField input) { return input.getFieldType().getErasedType().isEnum(); } }; } private Predicate<ModelAttributeField> belongsToJavaPackage() { return new Predicate<ModelAttributeField>() { @Override public boolean apply(ModelAttributeField input) { return packageName(input.getFieldType().getErasedType()).startsWith("java.lang"); } }; } private Predicate<ModelAttributeField> isBaseType() { return new Predicate<ModelAttributeField>() { @Override public boolean apply(ModelAttributeField input) { return Types.isBaseType(input.getFieldType()) || input.getField().getType().isPrimitive(); } }; } private Function<ResolvedField, ModelAttributeField> toModelAttributeField( final AlternateTypeProvider alternateTypeProvider) { return new Function<ResolvedField, ModelAttributeField>() { @Override public ModelAttributeField apply(ResolvedField input) { return new ModelAttributeField(fieldType(alternateTypeProvider, input), input); } }; } private Predicate<ResolvedField> onlyBeanProperties(final Set<String> beanPropNames) { return new Predicate<ResolvedField>() { @Override public boolean apply(ResolvedField input) { return beanPropNames.contains(input.getName()); } }; } private String nestedParentName(String parentName, ResolvedField field) { String name = field.getName(); ResolvedType fieldType = field.getType(); if (isContainerType(fieldType) && !Types.isBaseType(collectionElementType(fieldType))) { name += "[0]"; } if (isNullOrEmpty(parentName)) { return name; } return String.format("%s.%s", parentName, name); } private ResolvedType fieldType(AlternateTypeProvider alternateTypeProvider, ResolvedField field) { return alternateTypeProvider.alternateFor(field.getType()); } private String packageName(Class<?> type) { return Optional.fromNullable(type.getPackage()).transform(toPackageName()).or("java"); } private Function<Package, String> toPackageName() { return new Function<Package, String>() { @Override public String apply(Package input) { return input.getName(); } }; } private Set<String> getBeanPropertyNames(final Class<?> clazz) { try { Set<String> beanProps = new HashSet<String>(); PropertyDescriptor[] propDescriptors = getBeanInfo(clazz).getPropertyDescriptors(); for (PropertyDescriptor propDescriptor : propDescriptors) { if (propDescriptor.getReadMethod() != null && propDescriptor.getWriteMethod() != null) { beanProps.add(propDescriptor.getName()); } } return beanProps; } catch (IntrospectionException e) { LOG.warn(String.format("Failed to get bean properties on (%s)", clazz), e); } return newHashSet(); } @VisibleForTesting BeanInfo getBeanInfo(Class<?> clazz) throws IntrospectionException { return Introspector.getBeanInfo(clazz); } }
八、總結,這個東西折騰了好久才弄出來,特別是ModelAttributeParameterExpander會死循環還不能拓展,裏面的實現方法基本都是private,因此只能抄一份出來改,並且還沒辦法覆蓋springfox自定義的,只能暴力刪除。springfox2.9.2是有解決死循環的問題的,若是你們的spring版本在4.0以上,直接用springfox2.9.2,那麼多坑要填。還有就是controller比較多的時候要跟我同樣分組,或者自定義分組,否則所有加載出來頁面就崩了,加載時間也會很感人。mvc