写文档是件大事情,怎么弄好它?

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,简单易记:)

以上是关于写文档是件大事情,怎么弄好它?的主要内容,如果未能解决你的问题,请参考以下文章

实用工具:FastDoc 文档提取工具

实用工具:FastDoc 文档提取工具

实用工具:FastDoc 文档提取工具

java web的项目需求怎么写?

使用cgic库搭配ctemplate编写cgi程序

.Net Core入门学习 1 swagger的使用