由文档自动化引发的技术思考

发表信息: by

由文档自动化引发的技术思考

为什么需要接口文档

在我们日常工作中,正常的公司工种一般都会有后端、h5端、ios客户端、android客户端这几大类研发工程师,身为一个后端研发,经常会跟其他领域(服务负责人)的后端研发,各种前端研发打交道,而这“交道”之一便是介绍自己领域的对外能力,让其他领域或者工种的人能够了解和使用我们自己所负责的这个领域,那这种介绍的手段之一便是接口文档。

那针对于接口文档来说,会发生如下几个action:

  • 写文档
  • 传递文档
  • 读文档

这里,写文档尤其重要,也是耗时最多的一个点

现状和问题

那我们现在是用什么来写文档呢?

Wiki

wiki

Yapi

Yapi

着两种文档综合起来,存在着几种问题:

  • 格式要手写 – 效率低,不美观, 不统一
  • 复制粘贴程度高 – 基本就是将代码里面的东西粘贴出来
  • 实时性低 – 比如我由于一些功能迭代,导致接口增加了一些参数,但是有些人可能忘记更新文档,有些人可能根本不知道存在文档,导致文档更新不及时

常用的解决方案

好,那有没有什么解决办法呢?

其实接口文档有着自己固有的一些解决方案,特别是依托于java生态的强大。

前端和后端的接口文档比较出名的工具叫Swagger,维基百科是这么介绍的:Swagger 是一款RESTFUL接口的、基于YAML、JSON语言的文档在线自动生成、代码自动生成的工具。 会java的同学看一眼下面的图,基本就知道怎么回事了。

swagger

swagger

后端和后端的接口文档可以用现成的Javadoc

swagger

swagger

无论是javadoc还是swagger生成的方式都是在写代码的时候以注解的方式或者注释的方式写在代码上,然后使用命令,将文档生成出来,接下来主要是介绍javadoc,swagger也是大同小异。

maven如何生成javadoc

  • 修改API目录下的pom文件,添加javadoc插件

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-javadoc-plugin</artifactId>
	<version>2.10.4</version>
	<configuration>
	    <charset>UTF-8</charset>
	    <additionalparam>-Xdoclint:none</additionalparam>
	</configuration>
</plugin>

  • 在API目录下执行maven命令
mvn clean javadoc:javadoc
  • 在target目录下查看文档
open ./target/site/apidocs/index.html

javadoc 生成就是各种html文件,串联起来之后便成了一个方便阅读的网站,可以把这个网站放在静态资源服务器上方便传阅

javadoc

还记不记得上面提到的三个问题:格式要手写,复制粘贴程度高,实时性低。 maven带javadoc已经解决了前两个问题了,那第三个问题“实时性”低怎么解决呢?我们接下来讨论下。

如何让文档自动化生成

我们先讨论下什么叫实时性?我所能理解的文档生成实时性无非就是每次代码变更都会重新生成文档,仅此。那为题是什么时候算代码变更呢? 如果用git的话,我人文,一次code git push,才算是一次代码变更,也就是说我们期望,每次git push 的时候都重新生成javadoc展示给用户, 这样应该是一个理想的状态。

我们要做的事情无非就是下面的图:

gitlab

看着这张图,整个流程就是 用户gitpush代码到gitlab服务器的时候,会有个东西能够告诉文档服务器,文档服务器获取最新的代码,然后生成javadoc,再部署到资源服务上,之后展示文档给用户。整个流程中,gitpush是能实现的,文档服务器展示静态资源是能实现的(nginx),那么其中一环就是gitlab如何同志文档服务器并且文档服务器如何获取gitlab的代码并下载执行javadoc生成

其实现有两个解决方案可以帮助我们解决问题:

  • gitlab-runner
  • jenkins

由于gitlab-runner是gitlab天然就支持的功能,所以,我们选择gitlab-runner来做这件事情

gitlab

我们这里简单说下gitlab-runner是什么? gitlab-runner其实就是一个进程程序,可以安装在任何linux服务器上,运行的时候可以将自己和gitlab服务区联合(前提必须是网络互通),每次gitlab更新代码的时候,gitlab都会根据runner注册的时候所带的tag来告诉指定的runner,runner会将代码从gitlab下载下来,并且执行代码文件里面的.gitlab-ci.yml 大体的流程如下:

gitlab

这是项目所写的gitlab-ci.yml 内容:

stages:
  - build


genJavaDoc:
  stage: build
  script:
    - "ls -al"
    - "export APPID=pay-trader" # appid_xx 唯一标示 可以修改成自己的
    - "export API_PATH=casher-trader-api" # api_modle api模块的路径 根据自己项目填写
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # 下面的代码不需要变更
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    - "export ROOT_FOLDER=/data/javadoc" #文档服务器放置javadoc的目录
    - "export PROJECT_FOLDER=$ROOT_FOLDER/$APPID" # 项目目录
    - "export CURRENT_GIT_BRANCH=$CI_BUILD_REF_NAME" # 当前分支
    - "export TARGET_DOC_FOLDER=$PROJECT_FOLDER/${CURRENT_GIT_BRANCH//\\//-}" # 文档目录 我把分支中的/替换成了-
    - "cd $API_PATH" # 进入api模块的路径
    - "mvn clean javadoc:javadoc" # maven 生成javadoc
    - "mkdir -p $TARGET_DOC_FOLDER" # 创建目录文件
    - "rm -rf $TARGET_DOC_FOLDER/*" # 如果存在目录文件,则将其内容删除
    - "mv ./target/site/apidocs/* $TARGET_DOC_FOLDER" # 把生成出来的文档移动到对应的文档目录
    - "cd $ROOT_FOLDER" # 进入项目根目录
    - "python replace_template.py" # 运行这个python脚本
  tags:
    - javadoc

gitlab

nginx配置如下:

gitlab

打开localhost:8091看到的页面如下:

gitlab

这个页面有点丑,我们可以在/data/javadoc下增加index.html来美化界面,但是有个小问题,如何将本地目录文件内容放在自己写的index.html里面展示出来呢? 我这里用了一个简单的办法,就是手写了个python脚本,负责获取本地目录并且根据本地目录生成index.html

replace_template.py

#! -*- coding:utf8 -*-
import sys
import os
import json

reload(sys)
sys.setdefaultencoding("utf-8")

def replace(file_dir):
    index_template_file = open("index.html.template", "r")
    index_file = open("index.html", "w+")
    print 'relace:', file_dir
    while True:
        line = index_template_file.readline()
        if not line or len(line) < 1:
            break

        line = line.replace("GC_JAVA_DOC_FILE_DIR", json.dumps(file_dir))
        index_file.write(line)

    index_file.flush()

def list_dir(root_path):
    '''
        build dir struct
    '''
    result = []
    file_names = os.listdir(root_path)
    for file_name in file_names:
        file_path = root_path + '/' + file_name
        if os.path.isdir(file_path):
            one_info = {}
            one_info['appid'] = file_name
            one_info['branch'] = []
            child_file_names = os.listdir(file_path)
            for child_file_name in child_file_names:
                one_info['branch'].append({"name": child_file_name})

            result.append(one_info)

    return result

if __name__ == "__main__":
    file_dir = list_dir('.')
    replace(file_dir)

index.html.template

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <!-- import CSS -->
        <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    </head>
    <body>
        <div id="app">
            <el-container>
                <el-header class="doc-header" height="50px">
                    <div class="header-desc">
            JavaDoc自动化部署,有问题找GC,接入姿势请看wiki:<a href="xxxxx">xxxxx</a><br/><br/>
                    </div>
                </el-header>
                <div class="spilt-line"></div>
                <el-container>
                    <div class="appid">
                        <el-aside width="300px">
                            <el-menu :default-active="activityIndex" class="el-menu-vertical-demo" v-for="(item, index) in file_dir" @select="handleSelect">
                                <el-menu-item :index="index+''">
                                    <i class="el-icon-menu"></i>
                                    <span slot="title"></span>
                                </el-menu-item>
                            </el-menu>
                        </el-aside>
                    </div>
                    <div class="appid_branch">
                        <el-main>
                            <el-table :data="activity_appid_branch_info" style="width: 100%">
                                <el-table-column label="分支" >
                                    <template class="branc_name" slot-scope="scope">
                                        <a :href="activity_appid_info+'/'+scope.row.name"></a>
                                    </template>
                                </el-table-column>
                            </el-table>
                        </el-main>
                    </div>
                </el-container>
            </el-container>
        </div>
    </body>
    <!-- import Vue before Element -->
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <!-- import JavaScript -->
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: function() {
                return {
                    file_dir: GC_JAVA_DOC_FILE_DIR,
                    activityIndex: '0'
                }
            },
            computed: {
                activity_appid_branch_info: function () {
                    console.log(this.activityIndex)
                    return this.file_dir[parseInt(this.activityIndex)]['branch']
                },
                activity_appid_info: function() {
                    console.log(this.activityIndex)
                    return this.file_dir[parseInt(this.activityIndex)]['appid']
                }
            },
            methods: {
                handleSelect(key, keyPath) {
                    this.activityIndex = key+''
                }
            }
        })
    </script>
    <style scop>
        #app {
            width: 100%;
        }
        .spilt-line {
            height: 1px;
            width: 100%;
            background-color: #e6e6e6;
            margin: 0px 0px 30px 0px;
        }
        .doc-header {
            margin: 10px;
            position: relative;
        }
        .header-title {
            text-align: center;
            font-size: 30px;
        }
        .header-desc {
            font-size: 15px;
            position: absolute;
            bottom: 0px;
        }
        .appid_branch {
            width: 100%;
            text-align: center;
        }
        .branc_name {
            width: 100%;
            text-align: center;
        }
        el-table-column {
            font-size: 100px;
            width: 100%;
            text-align: center;
        }
    </style>
</html>

美化之后的样子是:

gitlab gitlab

这就完事了么?

我们已经实现了一个自动生成javadoc的流程,这样,上面所有的问题包括实时性已经完美的解决了,这种方式即提高了写文档的效率,又方便传输和观看,所以,针对于自动生成文档这个主题,已经介绍完了,但借助gitlab-runner or jenkins,我们可以做很多事情,而这些事情比文档自动化更有价值,比如发布系统,比如整个系统的自动化测试,等还有好多看起来更复杂的东西。

gitlab

上面这张图是 敏捷开发,持续集成,持续部署,Devops 各自所涵盖的点,而针对上面这个简单的文档自动化生成原理来说,其实我们使用其原理,可以完成整个devops的技术核心搭建。

gitlab

这张图,我简单的画了下,cicd整套开发模式,流程大致是:

  • rd根据prd开发需求并完成自测
  • rd将代码提mr到release分支
  • mr会自动触发ci流程,分别进行 自动化测试,和 镜像打包
  • mr会将打包的镜像提测给qa
  • qa将mr提测的镜像部署到测试服务器并进行测试
  • qa测试通过,将mr merge到release分支
  • gitlab自动触发cd流程,将代码部署到线上

这样,整套持续集成,持续部署便完成了,但这套流程里面毕竟自动化程度过高,特别是部署的流程风险太大不应该被自动化,所以一套更灵活的解决方案如下:

gitlab gitlab gitlab

整个在研发过程中,其实是感知不到运维的存在,这也就是devops的真谛