写文档是件大事情,怎么弄好它?
Posted sp42a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了写文档是件大事情,怎么弄好它?相关的知识,希望对你有一定的参考价值。
这里的文档指 API 接口文档。
写接口文档,首先原始的方式是手写,即文档脱离源码,使用 Word 或者 Markdown 写好发布给前端人员。好处是适合前期接口评审,缺点也是明显,源码和文档不同步,两边改。
Swagger 这样结合源码与文档的工具给我们带来了福音。Swagger 思路和问题我整理如下:
- 文档内容写在 Swagger 自定义的注解上,这样通过导出得到一份 json 文件然后渲染 html 文档;
- 注意是 Java **注解(Annotation)**而不是普通 Java 注释。同时本身就有 Java 方法的参数——如果你要既要写 Java 注释又要写 Swagger 注解,是不是重复?
- 其实很多时候这类信息是一致的(当然不排除接口的入参和 Java 参数是两回事的情况),完全可以“合并同类项”
- 于是有了另外一种思路:基于 Java 普通注释去导出文档,注解为辅,减少重复工作。这种思路很好,大家就这么干,很愉快地决定了
- 另外补句,有人说 Swagger 入侵性太强。我觉得这个不好说,想零入侵是不可能滴……看着办吧。
好~既然方向定了,就看看如何去实现吧,首当其冲的,怎么把 Java Comment 提炼出来?一开始我想到自然是正则表达式,话说俺最早搞 yui-ext(ExtJS 前身)文档就是用正则的,那时哪有什么 js 的文档工具,yui-ext 作者 Jack 也是自己写的文档工具。
写了一个晚上的正则,虽然感觉自己的正则功力宝刀未老,还略有进步,但最终结论这——还是个——坑~果断放弃吧
后来想起 JavaDoc 不就干这个的么?于是搜索一下,竟发现有 Doclet 这好东西,提供 API 调用 JavaDoc,十分强大。
要认识 Doclet,网上文档就不少,我推荐下面几个:
其他同类产品
基于注释的典型就是 smart-doc。世界很大,其他同类可参考这文章:《求你别再用swagger了,给你推荐几个在线文档生成神器》。还是 superJavaDoc 也不错 https://github.com/chenhaoxiang/super-java-doc。
为啥不用现有的呢?开始我也不知道,就一股脑地自己写轮子了……其实没想象中那么复杂,且听我娓娓道来……
使用 Doclet
其实 javadoc 在 jdk 目录下只是一个可执行程序,但是这个可执行程序是基于 jdk 的 tools.jar 的一个封装,也就是说 javadoc 实现在 tools.jar 中。
具体怎么使用 Doclet 就不赘述了。使用方法参考我给的源码也是答案。
使用 Doclet 过程中发现几个问题:
- Java Bean 的注释写在字段 field 上,field 又是 private 的,例如下面代码:
class A
/**
* 是否 xx
*/
private Boolean b;
读取 private 问题不大:classDoc.fields(false)
搞定,问题是 bean 有继承关系的,Doclet 就无能为力了。对此,我们通过反射也可以轻松搞定。
- 方法注释的,不能读取来自 interface 定义的注释。直接解析某个类上面的注释自然没问题,但是它继承于其他接口呢?这是正常操作呀?如下:
interface IController
/**
* 设置状态
*/
Boolean setStatus();
class Controller implements IController
// 这里就不写注释文档了
Docket 同样无能为力。对此,笔者想到的办法是返回获取该类的所有接口,看哪个是有指定注解的,比如 @getCommentHere
,表示从这里获取注释内容。前面说道,JavaDoc 为主,注解为辅,不是说完全排斥注解,注解还是挺好用的。当然我也知道自定义注解入侵性太强,所以还是优先使用 JavaDoc,以及它本来提供的标识。
- 解析内部类。内部类(Inner Class)又叫内嵌类(Nested Class),ClassName 标识可以是
com.abc.service$Pojo
这样的,我喜欢把代码行数比较少的 Bean 作为内部类放在一个外部类中——可 Doclet 并不会因你传$Pojo
而识别出来是内部类,咋搞?——我也头痛了很久,好在传入内部类外层的外部类,Doclet 是能够解析里面所有的内部类的,这样就行,我们解析好了,先存到一个 Map 之中,肯定包含你目标的那个内部类,返回即可。至于已经解析过了的,对 Map 判断下,找到返回就行。所以一开始识别了是内部类,其实接下来是对其外部类进行 Doclet 的解析。
实现
解析一个 Java 类,提取其字段注释和方法注释,当然也包括其类的信息,是为下列 Doclet Helper 也,循例也这里贴一下。
package com.ajaxjs.fast_doc;
import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import com.ajaxjs.framework.PageResult;
import com.ajaxjs.util.ReflectUtil;
import com.ajaxjs.util.logger.LogHelper;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Type;
import com.sun.tools.javadoc.Main;
public class Doclet implements Model
private static final LogHelper LOGGER = LogHelper.getLog(Doclet.class);
/**
* Docket 程序入口
*
* @param params
* @param clz
* @return
*/
public static BeanInfo parseFieldsOfOneBean(Params params, Class<?> clz)
init(params, clz);
BeanInfo bean = parseFieldsOfOneBean(clz); // 带注释的
bean.values.addAll(getSuperFields(clz, params));// 附加父类的 fields
return bean;
/**
* 解析某个 bean 的所有 fields(不包括父类的)
*
* @param clz
*
* @return
*/
private static BeanInfo parseFieldsOfOneBean(Class<?> clz)
BeanInfo beanInfo = new BeanInfo();
beanInfo.name = clz.getSimpleName();
beanInfo.type = clz.getName();
beanInfo.values = new ArrayList<>();
ClassDoc[] classes = root.classes();
if (classes.length == 1)
ClassDoc classDoc = classes[0];
beanInfo.description = classDoc.commentText();
beanInfo.values = Util.makeListByArray(classDoc.fields(false), fieldDoc ->
Value v = new Value();
v.name = fieldDoc.name();
v.type = fieldDoc.type().simpleTypeName();
v.description = fieldDoc.commentText();
return v;
);
Class<?>[] interfaces = clz.getInterfaces();
for (Class<?> _interface : interfaces)
LOGGER.info(_interface);
MethodDoc[] methods = classDoc.methods();
for (MethodDoc methodDoc : methods)
LOGGER.info(methodDoc.commentText());
else if (classes.length > 1) // maybe inner clz
for (ClassDoc clzDoc : classes)
/*
* qualifiedTypeName() 返回如 xxx.DetectDto.ResourcePlanResult 按照 clz$innerClz 风格转换
* xxx.DetectDto$ResourcePlanResult
*/
String fullType = clzDoc.qualifiedTypeName();
String clzName = clzDoc.simpleTypeName();
fullType = fullType.replace("." + clzName, "$" + clzName);
// LOGGER.info(fullType);
if (beanInfo != null && fullType.equals(beanInfo.type))
if (BeanParser.CACHE.containsKey(fullType))
continue; // 已经有
else
BeanInfo b = new BeanInfo();
b.name = clzName;
b.description = clzDoc.commentText();
b.type = fullType;
List<Value> simpleValues = new ArrayList<>();
List<BeanInfo> beans = new ArrayList<>();
Util.makeListByArray(clzDoc.fields(false), fieldDoc ->
Type type = fieldDoc.type();
Value v = new Value();
v.name = fieldDoc.name();
v.type = type.simpleTypeName();
v.description = fieldDoc.commentText();
simpleValues.add(v);
// TODO 递归 内嵌对象。没有 List<?> 真实 Class 引用
// if (ifSimpleValue(type))
// simpleValues.add(v);
// else
// BeanInfo bi = new BeanInfo();
// bi.name = v.type;
// bi.type = type.toString();
// bi.description = v.description;
// LOGGER.info(b.name + ">>>>>>>>>" + v.type);
//
// Class<?> clz = ReflectUtil.getClassByName(bi.type);
//
// if (!"Object".equals(v.type)) // Object 字段无法解析
// parseFieldsOfOneBean(tempParams, clz, bi);
//
// beans.add(bi);
//
return v;
);
b.values = simpleValues;
if (!CollectionUtils.isEmpty(beans))
b.beans = beans;
BeanParser.CACHE.put(fullType, b);
BeanInfo beanInCache = BeanParser.CACHE.get(beanInfo.type); // 还是要返回期待的那个
if (beanInCache != null)
beanInfo.description = beanInCache.description;
beanInfo.values = beanInCache.values;
beanInfo.beans = beanInCache.beans;
else
LOGGER.warning("Doclet 没解析任何类");
return beanInfo;
private final static String[] types = "int", "java.lang.String", "java.lang.Integer", "java.lang.Boolean", "java.lang.Long" ;
/**
*
* @param type
* @return
*/
public static boolean ifSimpleValue(Type type)
String t = type.toString();
// LOGGER.info(t);
for (String _t : types)
if (_t.equals(t))
return true;
return false;
/**
* Doclet 不能获取父类成员
*
* @param clazz
* @param params
* @return 只返回所有父类的 fields
*/
private static List<Value> getSuperFields(Class<?> clazz, Params params)
Class<?>[] allSuperClazz = ReflectUtil.getAllSuperClazz(clazz);
List<Value> list = new ArrayList<>();
if (!ObjectUtils.isEmpty(allSuperClazz))
for (Class<?> clz : allSuperClazz)
if (clz == PageResult.class || clz == List.class || clz == ArrayList.class || clz == AbstractList.class || clz == AbstractCollection.class)
continue;
Params p = new Params();
p.root = params.root;
p.classPath = params.classPath;
p.sourcePath = params.sourcePath;
init(p, clz);
BeanInfo superBeanInfo = parseFieldsOfOneBean(clz);// 父类的信息
if (!CollectionUtils.isEmpty(superBeanInfo.values))
list.addAll(superBeanInfo.values);
return list;
private static void init(Params params, Class<?> clz)
params.sources = new ArrayList<>();
String clzName = clz.getName();
boolean isInnerClz = clzName.contains("$");
if (isInnerClz)
clzName = clzName.replaceAll("\\\\$\\\\w+$", "");
params.sources.add(params.root + Util.className2JavaFileName(clzName));
else
params.sources.add(params.root + Util.className2JavaFileName(clz));
init(params);
/**
* 初始化 Doclet 参数,包括 classpath 和 sourcepath -classpath 参数指定 源码文件及依赖库的 class
* 位置,不提供也可以执行,但无法获取到完整的注释信息(比如 annotation)
*
* @param params Doclet 参数
*/
private static void init(Params params)
LOGGER.info("初始化 Doclet");
init(params.sources, params.sourcePath, params.classPath);
private static void init(List<String> sources, String sourcePath, String classPath)
List<String> params = new ArrayList<>();
params.add("-doclet");
params.add(Doclet.class.getName());
params.add("-docletpath");
params.add(Doclet.class.getResource("/").getPath());
params.add("-encoding");
params.add("utf-8");
if (sourcePath != null)
params.add("-sourcepath");
params.add(sourcePath);
if (classPath != null)
params.add("-classpath");
params.add(classPath);
params.addAll(sources);
Main.execute(params.toArray(new String[params.size()]));
/** 文档根节点 */
private static RootDoc root;
/**
* javadoc 调用入口
*
* @param root
* @return
*/
public static boolean start(RootDoc root)
Doclet.root = root;
return true;
当前只实现了字段的解析,方法的解析还要进一步实现,我会不断更新在源码库上:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-framework/src/main/java/com/ajaxjs/fast_doc。这工具名字姑且就叫 FastDoc,简单易记:)
以上是关于写文档是件大事情,怎么弄好它?的主要内容,如果未能解决你的问题,请参考以下文章