什么是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