手写一个SpringBoot Starter,超简单

2,693 阅读11分钟

什么是SpringBoot Starter

SpringBoot是现下流行的开发框架,能够自动化的集成各个框架,没有恶心而繁琐的配置文件,使项目搭建非常之快。实现这一切“自动化”的功臣就是Starter,可以打个比方,SpringBoot空工程就像一个插座,上面有无数个插孔,当我们需要往项目中添加某些工具时,只需要把这个工具的插头(Starter)插进插座里,这个工具就可以完美的在我们的工程中工作了。比如:mybatis实现了mybatis-spring-boot-starter,让SpringBoot集成MyBatis变得非常轻松,几乎不需要写任何配置文件。

手写Starter一个小案例

1、背景

SpringBoot集成Swagger2后,并不能将Aip生成规范的接口文档(word版),由于这个问题我打算写以一个Starter,实现把Swagger2生成的接口信息的json转换成word版的接口文档。并且让他适用任何一个SpringBoot工程,即插即用。

2、详细步骤

1) 创建一个空的maven工程,并引入spring-boot-autoconfigure、spring-boot-configuration-processor依赖,这两个最关键。详细的pom文件如下:\

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.fbl</groupId>
    <artifactId>swagger2word-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.10</version>
        </dependency>
    </dependencies>
</project>

2)创建一个获取springBoot配置文件的配置类
配置类代码(Swagger2WordProp.java)

package com.fbl.configuration;

import org.springframework.boot.context.properties.ConfigurationProperties;
// ConfigurationProperties注解是读取swagger为前缀的配置。
@ConfigurationProperties(prefix = "swagger")
public class Swagger2WordProp {

    // 如果没有写swagger.url配置,就默认使用123456(此地址就是Swagger生成Json的接口地址)
    private String url = "123456";

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }
}

3)创建一个将Swagger2的Json转换成Word的服务、web接口,代码我是参考JMCuixy 的。(此文不是主要讲述Thymeleaf模板和Word生成技术的,感兴趣的可以去JMCuixy 查看)\

Servers的接口代码(WordService.java)

package com.fbl.services;

import java.util.Map;

public interface WordService {

    Map<String,Object> tableList(String jsonUrl);
}

Servers的实现代码(WordServiceImpl.java)

package com.fbl.services.impl;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fbl.configuration.Swagger2WordProp;
import com.fbl.services.WordService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;
import com.fbl.model.Request;
import com.fbl.model.Response;
import com.fbl.model.ResponseModelAttr;
import com.fbl.model.Table;
import com.fbl.utils.JsonUtils;

import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;

/**
 * @Author fangbl
 * @Date 2020/2/11
 **/
@SuppressWarnings({"unchecked", "rawtypes"})
public class WordServiceImpl implements WordService {

    private RestTemplate restTemplate;

    private Swagger2WordProp swagger2WordProp;

    public WordServiceImpl(RestTemplate restTemplate, Swagger2WordProp swagger2WordProp) {
        this.restTemplate = restTemplate;
        this.swagger2WordProp = swagger2WordProp;
    }

    @Override
    public Map<String, Object> tableList(String jsonUrl) {
        jsonUrl = StringUtils.defaultIfBlank(jsonUrl, swagger2WordProp.getUrl());
        
        Map<String, Object> resultMap = new HashMap<>();
        List<Table> result = new ArrayList<>();
        try {
            String jsonStr = restTemplate.getForObject(jsonUrl, String.class);
            // convert JSON string to Map
            Map<String, Object> map = JsonUtils.readValue(jsonStr, HashMap.class);
            
            //解析model
            Map<String, Object> definitinMap = parseDefinitions(map);
            
            //解析paths
            Map<String, Map<String, Object>> paths = (Map<String, Map<String, Object>>) map.get("paths");
            if (paths != null) {
                Iterator<Entry<String, Map<String, Object>>> it = paths.entrySet().iterator();
                while (it.hasNext()) {
                    Entry<String, Map<String, Object>> path = it.next();

                    Iterator<Entry<String, Object>> it2 = path.getValue().entrySet().iterator();
                    // 1.请求路径
                    String url = path.getKey();

                    // 2.请求方式,类似为 get,post,delete,put 这样
                    String requestType = StringUtils.join(path.getValue().keySet(), ",");

                    // 3. 不管有几种请求方式,都只解析第一种
                    Entry<String, Object> firstRequest = it2.next();
                    Map<String, Object> content = (Map<String, Object>)firstRequest.getValue();

                    // 4. 大标题(类说明)
                    String title = String.valueOf(((List) content.get("tags")).get(0));

                    // 5.小标题 (方法说明)
                    String tag = String.valueOf(content.get("summary"));

                    // 6.接口描述
                    String description = String.valueOf(content.get("summary"));

                    // 7.请求参数格式,类似于 multipart/form-data
                    String requestForm = "";
                    List<String> consumes = (List) content.get("consumes");
                    if (consumes != null && consumes.size() > 0) {
                        requestForm = StringUtils.join(consumes, ",");
                    }

                    // 8.返回参数格式,类似于 application/json
                    String responseForm = "";
                    List<String> produces = (List) content.get("produces");
                    if (produces != null && produces.size() > 0) {
                        responseForm = StringUtils.join(produces, ",");
                    }

                    // 9. 请求体
                    List<LinkedHashMap> parameters = (ArrayList) content.get("parameters");

                    // 10.返回体
                    Map<String, Object> responses = (LinkedHashMap) content.get("responses");

                    //封装Table
                    Table table = new Table();

                    table.setTitle(title);
                    table.setUrl(url);
                    table.setTag(tag);
                    table.setDescription(description);
                    table.setRequestForm(requestForm);
                    table.setResponseForm(responseForm);
                    table.setRequestType(requestType);
                    table.setRequestList(processRequestList(parameters));
                    table.setResponseList(processResponseCodeList(responses));

                    // 取出来状态是200时的返回值
                    Map<String, Object> obj = (Map<String, Object>)responses.get("200");
                    if (obj != null && obj.get("schema")!=null) {
	                    table.setResponseModeAttrList(processResponseModelAttrs(obj, definitinMap));
                    }

                    //示例
                    table.setRequestParam(JsonUtils.writeJsonStr(buildParamMap(table.getRequestList(), map)));
                    table.setResponseParam(processResponseParam(obj, map));

                    result.add(table);
                }

                //排序,同类别的接口归并在一起
                Collections.sort(result, new Comparator<Table>() {
                	public int compare(Table o1, Table o2) {
                		return o1.getTitle().compareTo(o2.getTitle());
                	};
                });
            }

            resultMap.put("tables", result);
            resultMap.put("info", map.get("info"));

           System.out.println(JsonUtils.writeJsonStr(resultMap));
        } catch (Exception e) {
            System.out.println("parse error" + e);
        }
        return resultMap;
    }

	/**
	 * 处理请求参数列表
	 * @param parameters
	 * @return
	 */
	private List<Request> processRequestList(List<LinkedHashMap> parameters){
		List<Request> requestList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(parameters)) {
            for (Map<String, Object> param : parameters) {
                Request request = new Request();
                request.setName(String.valueOf(param.get("name")));
                Object in = param.get("in");
                if (in != null && "body".equals(in)) {
                    request.setType(String.valueOf(in));
                    Map<String, Object> schema = (Map) param.get("schema");
                    Object ref = schema.get("$ref");
                    // 数组情况另外处理
                    if (schema.get("type") != null && "array".equals(schema.get("type"))) {
                        ref = ((Map) schema.get("items")).get("$ref");
                    }
                    request.setParamType(ref == null ? "{}" : ref.toString());
                } else {
                    request.setType(param.get("type") == null ? "Object" : param.get("type").toString());
                    request.setParamType(String.valueOf(in));
                }
                if (param.get("required") != null) {
                    request.setRequire((Boolean) param.get("required"));
                } else {
                    request.setRequire(false);
                }
                request.setRemark(String.valueOf(param.get("description")));
                request.setParamType(request.getParamType().replaceAll("#/definitions/", ""));
                requestList.add(request);
            }
        }
        return requestList;
	}


	/**
	 * 处理返回码列表
	 * @param responses
	 * @return
	 */
	private List<Response> processResponseCodeList(Map<String, Object> responses){
		List<Response> responseList = new ArrayList<>();
        Iterator<Entry<String, Object>> it3 = responses.entrySet().iterator();
        while (it3.hasNext()) {
            Response response = new Response();
            Entry<String, Object> entry = it3.next();
            // 状态码 200 201 401 403 404 这样
            response.setName(entry.getKey());
            LinkedHashMap<String, Object> statusCodeInfo = (LinkedHashMap) entry.getValue();
            response.setDescription(String.valueOf(statusCodeInfo.get("description")));
//            response.setRemark(String.valueOf(statusCodeInfo.get("description")));
            responseList.add(response);
        }
		return responseList;
	}
	
	/**
	 * 处理返回属性列表
	 * @param responseObj
	 * @param definitinMap
	 * @return
	 */
    private List<ResponseModelAttr> processResponseModelAttrs(Map<String, Object> responseObj, Map<String, Object> definitinMap){
    	List<ResponseModelAttr> attrList=new ArrayList<>();
        Map<String, Object> schema = (Map<String, Object>)responseObj.get("schema");
        String type=(String)schema.get("type");
        String ref = null;
        if("array".equals(type)) {//数组
        	Map<String, Object> items = (Map<String, Object>)schema.get("items");
            if (items != null && items.get("$ref") != null) {
                ref = (String) items.get("$ref");
            }
        }else {
        	if (schema.get("$ref") != null) {//对象
                ref = (String)schema.get("$ref");
            }else {//其他类型
            	ResponseModelAttr attr=new ResponseModelAttr();
            	attr.setType(type);
            	attrList.add(attr);
            }
        }
        
        if(StringUtils.isNotBlank(ref)) {
        	Map<String, Object> mode = (Map<String,Object>)definitinMap.get(ref);
        	
        	ResponseModelAttr attr=new ResponseModelAttr();
        	attr.setClassName((String)mode.get("title"));
        	attr.setName((String)mode.get("description"));
        	attr.setType(StringUtils.defaultIfBlank(type, StringUtils.EMPTY));
        	attrList.add(attr);
        	
        	attrList.addAll((List<ResponseModelAttr>)mode.get("properties"));
        }
    	return attrList;
    }
    
    /**
     * 解析Definition
     * @param map
     * @return
     */
    private Map<String, Object> parseDefinitions(Map<String, Object> map){
    	Map<String, Map<String, Object>> definitions = (Map<String, Map<String, Object>>) map.get("definitions");
        Map<String, Object> definitinMap = new HashMap<String, Object>();
        if(definitions!=null) {
        	Iterator<String> modelNameIt=definitions.keySet().iterator();
        	
        	String modeName = null;
        	Entry<String, Object> pEntry=null;
        	ResponseModelAttr modeAttr=null;
        	Map<String, Object> attrInfoMap=null;
        	while (modelNameIt.hasNext()) {
				modeName = modelNameIt.next();
				Map<String, Object> modeProperties=(Map<String, Object>)definitions.get(modeName).get("properties");
				Iterator<Entry<String, Object>> pIt= modeProperties.entrySet().iterator();
				
				List<ResponseModelAttr> attrList=new ArrayList<>();
				
				//解析属性
				while (pIt.hasNext()) {
					pEntry=pIt.next();
					modeAttr=new ResponseModelAttr();
					modeAttr.setValue(pEntry.getKey());
					attrInfoMap=(Map<String, Object>)pEntry.getValue();
					modeAttr.setName((String)attrInfoMap.get("description"));
					modeAttr.setType((String)attrInfoMap.get("type"));
					if(attrInfoMap.get("format")!=null) {
						modeAttr.setType(modeAttr.getType()+"("+(String)attrInfoMap.get("format")+")");
					}
					attrList.add(modeAttr);
				}
				
				Map<String, Object> mode=new HashMap<>();
				mode.put("title", definitions.get(modeName).get("title"));
				mode.put("description", definitions.get(modeName).get("description"));
				mode.put("properties", attrList);
				
				definitinMap.put("#/definitions/"+modeName, mode);
			}
        }
        return definitinMap;
    }

    /**
     * 处理返回值
     * @param responseObj
     * @param map
     * @return
     */
    private String processResponseParam(Map<String, Object> responseObj, Map<String, Object> map){
    	if (responseObj != null && responseObj.get("schema")!=null) {
	        Map<String, Object> schema = (Map<String, Object>)responseObj.get("schema");
	        String type=(String)schema.get("type");
	        String ref = null;
	        if("array".equals(type)) {//数组
	        	Map<String, Object> items = (Map<String, Object>)schema.get("items");
	            if (items != null && items.get("$ref") != null) {
	                ref = (String) items.get("$ref");
	                
	                ObjectNode objectNode = parseRef(ref, map);
	                ArrayNode arrayNode = JsonUtils.createArrayNode();
	                arrayNode.add(objectNode);
	                return arrayNode.toString();
	            }
	        }else if (schema.get("$ref") != null) {
	            ref = (String)schema.get("$ref");
	            
	            ObjectNode objectNode = parseRef(ref, map);
	            return objectNode.toString();
	        }
        }
        
    	return StringUtils.EMPTY;
    }
    

    /**
     * 从map中解析出指定的ref
     *
     * @param ref ref链接 例如:"#/definitions/PageInfoBT«Customer»"
     * @param map 是整个swagger json转成map对象
     * @return
     * @author fpzhan
     */
    private ObjectNode parseRef(String ref, Map<String, Object> map) {
        ObjectNode objectNode = JsonUtils.createObjectNode();
        if (StringUtils.isNotEmpty(ref) && ref.startsWith("#")) {
            String[] refs = ref.split("/");
            Map<String, Object> tmpMap = map;
            //取出ref最后一个参数 start
            for (String tmp : refs) {
                if (!"#".equals(tmp)) {
                    tmpMap = (Map<String, Object>) tmpMap.get(tmp);
                }
            }
            //取出ref最后一个参数 end
            //取出参数
            if (tmpMap == null) {
                return objectNode;
            }
            Object properties = tmpMap.get("properties");
            if (properties == null) {
                return objectNode;
            }
            Map<String, Object> propertiesMap = (Map<String, Object>) properties;
            Set<String> keys = propertiesMap.keySet();
            //遍历key
            for (String key : keys) {
                Map<String, Object> keyMap = (Map) propertiesMap.get(key);
                if ("array".equals(keyMap.get("type"))) {
                    //数组的处理方式
                    String sonRef = (String) ((Map) keyMap.get("items")).get("$ref");
                    //对象自包含,跳过解析
                    if (ref.equals(sonRef)) {
                        continue;
                    }
                    JsonNode jsonNode = parseRef(sonRef, map);
                    ArrayNode arrayNode = JsonUtils.createArrayNode();
                    arrayNode.add(jsonNode);
                    objectNode.set(key, arrayNode);
                } else if (keyMap.get("$ref") != null) {
                    //对象的处理方式
                    String sonRef = (String) keyMap.get("$ref");
                    //对象自包含,跳过解析
                    if (ref.equals(sonRef)) {
                        continue;
                    }
                    ObjectNode object = parseRef(sonRef, map);
                    objectNode.set(key, object);
                } else {
                    //其他参数的处理方式,string、int
                    String str = "";
                    if (keyMap.get("description") != null) {
                        str = str + keyMap.get("description");
                    }
                    if (keyMap.get("format") != null) {
                        str = str + String.format("格式为(%s)", keyMap.get("format"));
                    }
                    objectNode.put(key, str);
                }
            }
        }
        return objectNode;
    }

    /**
     * 封装post请求体
     *
     * @param list
     * @param map
     * @return
     */
    private Map<String, Object> buildParamMap(List<Request> list, Map<String, Object> map) throws IOException {
        Map<String, Object> paramMap = new HashMap<>(8);
        if (list != null && list.size() > 0) {
            for (Request request : list) {
                String name = request.getName();
                String type = request.getType();
                switch (type) {
                    case "string":
                        paramMap.put(name, "string");
                        break;
                    case "integer":
                        paramMap.put(name, 0);
                        break;
                    case "number":
                        paramMap.put(name, 0.0);
                        break;
                    case "boolean":
                        paramMap.put(name, true);
                        break;
                    case "body":
                        String paramType = request.getParamType();
                        ObjectNode objectNode = parseRef(paramType, map);
                        paramMap = JsonUtils.readValue(objectNode.toString(), Map.class);
                        break;
                    default:
                        paramMap.put(name, null);
                        break;
                }
            }
        }
        return paramMap;
    }
}

Controller实现代码(WordController.java)

package com.fbl.web;
import com.fbl.services.WordService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Map;

@Controller
public class WordController {

    private WordService tableService;

    private RestTemplate restTemplate;


    public WordController(WordService tableService, RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
        this.tableService = tableService;
    }

    @Value("${server.port}")
    private Integer port;

    /**
     * 将 swagger 文档转换成 html 文档,可通过在网页上右键另存为 xxx.doc 的方式转换为 word 文档
     *
     * @param model
     * @param url   需要转换成 word 文档的资源地址
     * @return
     */
    @Deprecated
    @RequestMapping("/toWord")
    public String getWord(Model model, @RequestParam(value = "url", required = false) String url, Integer type) {
        Map<String, Object> result = tableService.tableList(url);
        model.addAttribute("url", StringUtils.defaultIfBlank(url, StringUtils.EMPTY));
        model.addAttribute("type", type==null ? 0 : type);
        model.addAttribute("info", result.get("info"));
        model.addAttribute("tables", result.get("tables"));
        return "word";
    }

    /**
     * 将 swagger 文档一键下载为 doc 文档
     *
     * @param url      需要转换成 word 文档的资源地址
     * @param response
     */
    @RequestMapping("/downloadWord")
    public void word(@RequestParam(required = false) String url, HttpServletResponse response) {
        ResponseEntity<String> forEntity = restTemplate.getForEntity("http://localhost:" + port + "/toWord?type=1&url=" + StringUtils.defaultIfBlank(url, StringUtils.EMPTY), String.class);
        response.setContentType("application/octet-stream;charset=utf-8");
        response.setCharacterEncoding("utf-8");
        try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())) {
            response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode("toWord.doc", "utf-8"));
            byte[] bytes = forEntity.getBody().getBytes();
            bos.write(bytes, 0, bytes.length);
            bos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4)创建自动配置类,将我们写的服务、web接口发布到Spring容器中,加入此Starter就可以自动读取配置,并让自己的服务成功加入到新的工程。

package com.fbl;

import com.fbl.configuration.Swagger2WordProp;
import com.fbl.services.WordService;
import com.fbl.services.impl.WordServiceImpl;
import com.fbl.web.TestApi;
import com.fbl.web.WordController;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fbl.services.TestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.SSLContext;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;

@Configuration
// EnableConfigurationProperties注解必须加,否则Swagger2WordProp里就会报错
@EnableConfigurationProperties(Swagger2WordProp.class)
public class Swagger2WordAutoConfiguration {

    @Autowired
    Swagger2WordProp swagger2WordProp;
    
    // 如果容器中不存在WordController类型的Bean就添加WordController到容器
    @Bean
    @ConditionalOnMissingBean(WordController.class)
    public WordController wordController(WordService wordService, RestTemplate restTemplate) {
        return new WordController(wordService, restTemplate);
    }

    // 如果容器中不存在WordService类型的Bean就添加WordServiceImpl到容器
    @Bean
    @ConditionalOnMissingBean(WordService.class)
    public WordService wordService(RestTemplate restTemplate) {
        return new WordServiceImpl(restTemplate, swagger2WordProp);
    }

    @Bean
    @ConditionalOnMissingBean(RestTemplate.class)
    public RestTemplate restTemplate() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
        TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
        SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom()
                .loadTrustMaterial(null, acceptingTrustStrategy)
                .build();
        SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
        CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(csf)
                .build();
        HttpComponentsClientHttpRequestFactory requestFactory =
                new HttpComponentsClientHttpRequestFactory();
        requestFactory.setHttpClient(httpClient);

        //60s
        requestFactory.setConnectTimeout(60 * 1000);
        requestFactory.setReadTimeout(60 * 1000);
        RestTemplate restTemplate = new RestTemplate(requestFactory);
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }

}

5)在resources/META-INF/下创建Spring.factories文件,指定Starter自动配置的类。

org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.fbl.Swagger2WordAutoConfiguration

6)编码过程基本结束了,现在就是将工程打包安装到本地的maven仓库。
执行下面命令:

mvn clean install

在项目中使用这个Starter

1) 在新的项目中添加swagger2word-starter依赖

<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
</dependency>

2) 由于生成word需要支持thymeleaf,所以也要加入相关的依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

3) 在application.properties中加入Starter的配置

# swagger2word-starter需要的配置(获取Swagger文档json数据的接口)
swagger.url=http://127.0.0.1:8088/v2/api-docs

# thymeleaf需要的配置
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML

4) 在resources/templates/添加模板,用户生成word。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="application/msword"/>
    <title>toWord</title>
    <style type="text/css">
        .bg {
            color: #fff;
            background-color: #559e68;
        }

        table {
            border: 1px solid #dbe3e4;
            table-layout: fixed;
        }

        tr {
            height: 32px;
            font-size: 12px;
        }

        td {
            padding: 0px 5px 0px 5px;
            border: 1px solid #dbe3e4;
            height: 32px;
            overflow: hidden;
            word-break: break-all;
            word-wrap: break-word;
            font-size: 14px;
        }
        .specialHeight {
            height: 40px;
        }
        
        .first_title{
            height: 60px;
            line-height: 60px;
            margin: 0;
            font-weight: bold;
            font-size:20px;
        }
        .second_title{
            height: 40px;
            line-height: 40px;
            margin: 0;
            font-weight: bold;
            font-size:16px;
        }
        .doc_title{
            font-size:24px;
            text-align:center;
        }
        .download_btn{
            float:right;
        }
    </style>
</head>

<body>
<div style="width:800px; margin: 0 auto" th:with="count=1">
    <div>
        <p class="doc_title" th:text="${info.title+'('+info.version+')'}"></p>
        <a class="download_btn" th:href="${'/downloadWord?url='+url}" th:if="${type!=1}">下载文档</a>
        <br>
    </div>
    <div th:each="table,iterStat:${tables}" style="margin-bottom:20px;">
        <div th:with="preTitle=${iterStat.index>0 ? tables[iterStat.index-1].title : ''}, isSame=${table.title == preTitle}, count=${ table.title == preTitle ? count+1 : 1}">
            <!--这个是类的说明-->
            <h4 class="first_title" th:if="${!isSame}" th:text="${isSame ? '' : #strings.concat(iterStat.count,'. ', table.title)}"></h4>

            <!--这个是每个请求的说明,方便生成文档后进行整理-->
            <h5 class="second_title" th:text="${count+')'+table.tag}"></h5>

            <table border="1" cellspacing="0" cellpadding="0" width="100%">
                <tr class="bg">
                    <td colspan="5" th:text="${table.tag}"></td>
                </tr>
                <tr>
                    <td width="25%">接口描述</td>
                    <td colspan="4" th:text="${table.description}"></td>
                </tr>
                <tr>
                    <td>URL</td>
                    <td colspan="4" th:text="${table.url}"></td>
                </tr>
                <tr>
                    <td>请求方式</td>
                    <td colspan="4" th:text="${table.requestType}"></td>
                </tr>
                <tr>
                    <td>请求类型</td>
                    <td colspan="4" th:text="${table.requestForm}"></td>
                </tr>
                <tr>
                    <td>返回类型</td>
                    <td colspan="4" th:text="${table.responseForm}"></td>
                </tr>
    
                <tr class="bg" align="center">
                    <td>参数名</td>
                    <td>数据类型</td>
                    <td>参数类型</td>
                    <td>是否必填</td>
                    <td>说明</td>
                </tr>
    
                <tr align="center" th:each="request:${table.requestList}">
                    <td th:text="${request.name}"></td>
                    <td th:text="${request.type}"></td>
                    <td th:text="${request.paramType}"></td>
                    <td th:if="${request.require}" th:text="Y"></td>
                    <td th:if="${!request.require}" th:text="N"></td>
                    <td th:text="${request.remark}"></td>
                </tr>
    
                <tr class="bg" align="center">
                    <td>状态码</td>
                    <td colspan="2">描述</td>
                    <td colspan="2">说明</td>
                </tr>
    
                <tr align="center" th:each="response:${table.responseList}">
                    <td th:text="${response.name}"></td>
                    <td colspan="2" th:text="${response.description}"></td>
                    <td colspan="2" th:text="${response.remark}"></td>
                </tr>
                
                <tr class="bg" align="center">
                    <td>返回类名</td>
                    <td>返回属性名</td>
                    <td>类型</td>
                    <td colspan="2">说明</td>
                </tr>
    
                <tr align="center" th:each="response:${table.responseModeAttrList}">
                    <td th:text="${response.className}"></td>
                    <td th:text="${response.value}"></td>
                    <td th:text="${response.type}"></td>
                    <td colspan="2" th:text="${response.name}"></td>
                </tr>
                
                <tr class="bg">
                    <td colspan="5">示例</td>
                </tr>
                <tr class="specialHeight">
                    <td class="bg">请求参数</td>
                    <td colspan="4" th:text="${table.requestParam}"></td>
                </tr>
                <tr class="specialHeight">
                    <td class="bg">返回值</td>
                    <td colspan="4" th:text="${table.responseParam}"></td>
                </tr>
                
            </table>
        </div>
    </div>
</div>
</body>
</html>

5) 启动项目,访问接口http://127.0.0.1:8088/toWord

word文件效果:

总结

上述的代码都在我的git仓库:github.com/1315977663/…

实现一个Starter的步骤大致就是这样,当然我的这还有很多做的不好的地方,比如:引入Starter后还需要加入模板才能应用。没有做到完全的即插即用,这里有很大的优化空间,喜欢的朋友可以往github.com/1315977663/…commit你的代码,让以后编写接口文档工作量大大减少。

其实实现一个Starter的步骤不多,但是想要做一个优秀的Starter需要一个很好的设计,这样才能与客户端的工程完全解耦。想要设计的好还是要掌握扎实的基础知识,不能太依赖上层的框架,所以基础是非常重要的,只懂得使用上层的框架很难成为一个优秀的程序猿。

这里知道了Starter的基本工作流程,可以去看看mybatis-spring-boot-starter的源码,看看它是怎么将SqlSessionFactory、SqlSessionTemplate等关键的类自动注册到Spring容器中。\

首先看看mybatis-spring-boot-starter的spring.factories文件

在看看MybatisAutoConfiguration.class