代码影响范围工具探索

作者:京东零售 田创新、耿蕾

一、背景

1.祖传代码不敢随意改动,影响范围无法评估。并且组内时常有因为修改了某块代码,导致其他业务受到影响,产生bug,影响生产。

2.研发提测完成后,测试进入测试后经常会向研发询问本次需求改动影响范围,以此来确定测试用例,以达到精准测试,提升整个需求的质量,缩短交付周期。

那么,如何才能规避这种隐患?有没有一种工具能够协助代码研发及review人员更加精确的判断当前代码改动影响范围,有没有一种方法能够提供除了业务逻辑条件验证,针对代码作用范围,给测试人员提供精确验证链路?

二、方案调研

技术方案调研

经过各方资料查找及比对,最终我们整理了两个满足我们需求的方案:

1.IDEA提供了显示调用指定Java方法向上的完整调用链的功能,可以通过“Navigate -> Call Hierarchy”菜单(快捷键:control+option+H)使用,缺点是并没有向下的调用链生成。

2.开源框架调研:wala/soot静态代码分析工具。

针对上述的调研,大致确认了两种方案,集中分析两种方案的优劣,来制定符合我们目前情况的方案:

工具名称 优势 劣势 是否符合
Call Hierarchy 支持方法向上调用链 功能比较单一,数据无操作性
wala/soot静态代码分析 能够完善的分析Java中任何逻辑包括方法调用链,且满足我们目前的需求 臃肿,复杂繁琐,功能过于庞大

经过前期的比较以及相关工具的资料调研、工具功能分析,并考虑到后期一些个性化功能定制开发,以上工具不太满足我们目前的需求,所以决定自己动手,丰衣足食,尝试重新开发一个能够满足我们需求的工具,来协助研发以及测试人员。

三、方案制定

预期:工具尽量满足全自动化,研发只需要接入即可,减少研发参与,提升整个调用链展示和测试的效率。并且调用链路应该在研发打包的过程中触发,然后将数据上传至服务端,生成调用链路图。

上述方案制定完成后,需要进一步确认实现步骤。前期我们确认了工具的大概的方向,并进行步骤分解,根据具体的功能将整个工具拆分成六个步骤

1.确认修改代码位置(行号)。与git代码管理关联,能够使用git命令,去提取研发最近一次提交代码的有变动的代码行数。

2.根据步骤1确认收集到影响的类+方法名+类变量。

3.根据2中确认的类+方法名称生成向上和向上的调用链。包括jar/aar包。

4.根据3中生成的调用链完成流程图的展示。

5.自定义注释标签Tag说明当前业务,并提取Tag内容。

6.本地数据生成并上传服务端生成调用流程图。

整体流程图如下:

四、方案实施

1.定位源代码修改位置行号。

​ 首先我们使用 git diff --unified=0 --diff-filter=d HEAD~1 HEAD命令 输出最近一次提交修改的内容,且已只git diff 会按照固定格式输出。

​ 通过提交增、删、改的修改,执行git diff命令,对输出内容进行观察。

​ 举例:某次提交修改了两个文件,如下

​ RecommendVideoManager.java

ScrollDispatchHelper.java

git diff命令执行后,输出以下内容:

技术方案:

a.按行读取输出内容,读取到到diff 行,则识别为一个新的文件,并用正则表达式提取文件名 :

String[] lines = out.toString().split("\r?\n");
Pattern pattern = Pattern.compile("^diff --git a/\S+ b/(\S+)");

b.用正则表达式提取 @@ -149 +148,0 @@ ,用来解析代码修改行数:

Pattern pattern = Pattern.compile("^@@ -[0-9]+(,[0-9]+)? \+([0-9]+)(,[0-9]+)? @@");

c.针对我们的需求,我们只关心本次修改影响的是那个方法,不关心具体影响了哪些行数,所以我们只需要

int changeLineStart = Integer.parseInt(m.group(2));

就拿到了本次修改,修改开始的代码行数, 在结合ASM就可以获取到本次改动影响的具体方法。

2.利用获取的行号定位具体的方法。

​ 根据上述1步骤中定位出研发每次提交的修改的Java源文件和改动的行号位置,我们需要定位修改代码行号所归属的方法名称,再由方法名称+类名+包名去定位本次修改的影响链路。

如何去定位?

首先确定的是,研发在工程中只能修改的是工程中的源文件,所以我们可以在遍历收集整个工程的源文件的过程中根据已知的修改行号来确定修改的方法名称,进而知道整个方法的调用链路。而对对于那些没有落到方法体范围之内的行号,基本上可以确认为类变量或常量,考虑到对于常量修改也可能影响到业务逻辑,所以我们也会对修改的Field进行上下调用的范围的查找,所以需要记录。所以整个过程分成两个部分:

​ a.遍历源码Class文件,获取整个类的Field;

​ b.遍历Class文件的过程中,通过visitMethod遍历整个方法体,记录方法的初始行号和结束行号,来定位方法;

首先是a部分,确认Field,ClassVisitor提供现成的方法:

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        Log.i("jingdong","Field name is :%s desc is %s: ",name,desc);
        return super.visitField(access, name, desc, signature, value);
    }

所以我们可以在文件中直接获得整个类的Field。然后去根据行数去判断是否有对Fields有修改。如果Fields有修改,那么我们可以根据上述方法去比对,那么就可以获得哪个Field被修改。

接下来是b部分,在遍历Class文件的过程中,通过visitMethod方法,重写AdviceAdapter类来提供MethodVisitor,在遍历过程中,完确定研发修改影响的类及方法,具体实现可分为以下步骤:

2.1 获取源文件编译好的Class文件;

apk的编译过程中有很多的task需要执行,各个任务环环相扣有序的执行,我们要获取编译好的Class文件,需要在特定的任务之间。我们知道在Java Compiler之后,不管是R.java抑或是aidl,再或者是Java interfaces都会编译成.class文件,在编译完成后会接着完成dex的编译,所以我们尽可能的在dex编译之前完成class文件的处理,这种仅仅是考虑到宿主或者单独的插件工程方案,但是对于主站业务来说,会有各种各样的组件aar,aar的编译编译不会走dex编译,所以针对这些组件工程,我们也需要考虑到,简单的方式就是我们去监听aar编译的task,然后再做一些处理,所以在Plugin的apply方法中需要进行区分处理,代码如下:

project.afterEvaluate {
            def android = project.extensions.android
            def config = project.method
  				if (config.enable) {
  					//应用级别
            if (project.plugins.hasPlugin('com.android.application')) {
                android.applicationVariants.all { variant ->
                    MethodTransform.inject(project, variant)
                }
            }else{
              	//aar编译处理--
                //这里我们是在compileReleaseJavaWithJavac之后运行自定义Task
                Task javaWithJavacTask = project.tasks.findByName("compileReleaseJavaWithJavac")
                if (javaWithJavacTask != null) {
                     def customTask = project.tasks.create("JDcustomTask", JdParseClassTask.class)
                     javaWithJavacTask.finalizedBy(customTask)
                    }else {
                        new GradleException("创建task失败~~")
                    }
            		}
						}
        }

两者的处理逻辑一致,也就是在Task的监听有些区别,所以下面我们不重复复述,以MethodTransform为主线进行讲解。

那有的同学就问了,为啥我们不直接对源文件.java文件进行处理呢?

因为,就目前京东主站项目而言,各个aar模块相互调用,如果我们仅仅使用源文件进行扫描,各个aar或者jar包的调用链会断掉不全面,影响代码review人员及测试人员的测试用例完整度。

接下来是代码实现,我们监听任务执行,并针对需要监听的任务开展我们的Class收集操作:

 //Project
project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
     @Override
     public void graphPopulated(TaskExecutionGraph taskGraph) {
         for (Task task : taskGraph.getAllTasks()) {
           //对满足我们需求的Task执行前,
           if(task.name.equalsIgnoreCase("transformClassesWithDexForDebug")){
             //执行我们的TrasnsformTask
             //省略。。。。
      }}}})

2.2 排除非class文件的干扰,对源文件路径进行递归遍历;

代码的编译长短对研发的影响很大,所以编译时长很宝贵,需要我们尽量的减少编译的时长,所以我们在执行我们自定义的Transform过程中,需要过滤并排除非Class文件,减少不必要的浪费。经过整理主要为:R文件以及R文件的内部类R$文件,包括R$string、R$styleable等等,所以,在遍历处理过程中我们需要对R文件及R$文件过滤。

public static final String[] UN_VISITOR_CLASS = {"R.class", "R$"};

2.3 提供ClassVisitor类和MethodClass去搜集Class及对应Method,并定位

这个步骤是最主要的一部分,这一部分主要获取两部分数据,第一部分是研发修改直接影响到的类和方法;第二部分是遍历整个源文件的所获得的类信息,主要包括类+各个方法以及各个方法体,也就是方法中的指令;

在拿到transformInvocation后我们进行源文件文件夹遍历和所有jar包的遍历,在外层我们定义好存储被影响的类列表(changedClassesList),和包含类信息的列表(classesInfoList),将两个列表作为参数,传递进去在遍历过程中赋值。这里值得注意的是,在进行jar解析过程中不需要进行changedClassesList,因为对于本工程来说研发人员不会直接对jar文件中文件操作。

//修改类列表
List<LinkedClassInfo> changedClassesList = new ArrayList<>()
//类信息列表
List<Map<String, List<Map<String, List<MethodInsInfo>>>>> classesInfoList = new ArrayList<Map<String, List<Map<String, List<MethodInsInfo>>>>>()
transformInvocation.inputs.each { TransformInput input ->
      //所有源文件生成的class
      input.directoryInputs.each { DirectoryInput dirInput ->
           collectDir(dirInput, isIncremental, classesInfoList, changedClassesList)
       }
       //所有jar包集合
       input.jarInputs.each { JarInput jarInput ->
           if (jarInput.getStatus() != Status.REMOVED) {
                //可以取到jar包集合
                collectJar(jarInput, isIncremental, classesInfoList,jarOutputFile)
                }
         }
   }

在对源文件遍历过程中,我们进行定位搜寻。

遍历源文件根节点并读取:

if (file != null) {
            //根布局目录进行循环遍历
            File[] files = file.listFiles()
            files.each { File f ->
                if (f.isDirectory()) {
                    collectJar(f, classList,changedClasss,changedLineInfoMap)
                } else {
                    boolean isNeed = true
                    //对文件类型进行校验,排除一些无意义的配置性文件
                    //省略。。。
                    if (isNeed) {
                        try {
                          //类集合(包含:类名+方法名+方法指令)
                            Map<String, List<Map<String, List<MethodInsInfo>>>> mClassMethodsList = new HashMap<String, List<Map<String, List<MethodInsInfo>>>>()
                            ClassReader cr = new ClassReader(new FileInputStream(f))
                            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES)
                          //重写ClassVisitorAdapter
                            ClassVisitorAdapter ca = new ClassVisitorAdapter(cw, mClassMethodsList,changedClasss ,changedLineInfoMap )
                            cr.accept(ca, ClassReader.EXPAND_FRAMES)
                            classList.add(mClassMethodsList) //将类的整个方法和指令加进去
                        } catch (RuntimeException re) {
                            re.printStackTrace()
                        } catch (IOException e) {
                            e.printStackTrace()
                        }
                    }
                }
            }
        }

重写ClassVisitor,ASM提供的visit方法可以很方便的去识别这个类的各种信息,而我们用到的信息为两种,一种是接口类型的判定,一种是当前类的类名。对于接口,我们没有必要去进行Method的访问,对获得的类名信息我们进行判定当前类是否是git最后提交有做过修改的的类:

@Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
        owner = name;//类名
      	//判定不为接口类型
        isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
        //这里判断是否修改是否包含此类
        if (this.mChangedLineInfoMap!=null&&mChangedLineInfoMap.size()>0){
            for (Map.Entry<String,ChangedLineInfo> changedLineInfoEntry:this.mChangedLineInfoMap.entrySet()){
                String filePath = changedLineInfoEntry.getKey();
                if(filePath.contains(owner)){
                    //包含此类
                    linkedClassInfo= new LinkedClassInfo();
                    linkedClassInfo.className = owner;
                    mChangedLineInfo= changedLineInfoEntry.getValue();
                    methodNameList = new ArrayList<>();
                    linkedClassInfo.methodNameList = methodNameList;
                }
            }
        }
    }

`在上述的visit中我们定位了当前类是否与上次git提交的是否有关,接下来我们需要MethodVisitor中进行有选择的拦截对应的Method的访问。

重写MethodVisitor在visitMethod中进行拦截处理,如果git修改相关在当前类中,则我们在访问Method时,进行方法体行数定位。

mv = new MethodVisterAdapter(mv,
                    owner,
                    access,
                    //省略。。。
                    mChangedLineInfo //更改行数位置
                    );

在MethodVisitor中,我们可以通过系统方法定位访问方法的每条方法指令及指令对应的行数,所以我们只要重写visitLineNumber方法即可实时的在visitMethodInsn方法中拿到方法体访问行数,这里有个小的注意点就是,我们在调用visitLineNumber返回的line不是我们理解意义上的方法名称部分开始,而是从方法体的第一行代码计算开始,所以我们在做判断的时候,需要注意,相对方法体的首行,我们更关心方法体的变更,所以我们只需要判定落在visitMethodInsn中的更改即可。有需要更加精细的判定,小伙伴可以进行更加精细的调研。

以下是visitLineNumber方法:

    @Override
    public void visitLineNumber(int line, Label start) {
        this.lineNumber = line;//置换lineNumber
        super.visitLineNumber(line, start);
    }

知道了方法体开始的地方,我们也需要知道结束的位置,获取到结束位置后,我们就能轻松的定位到我们需要的定位的方法体,从而获得方法名称,进一步获得类的名称。ASM在MethodVisitor中提供了visitEnd方法,表示方法体访问结束,那么我们就可以在visitEnd中进行定位:

@Override
public void visitEnd() {
    super.visitEnd();
    int startLine = this.startLineNumber;
    int endLine = this.lineNumber;
    boolean isContained = false;
    if (this.mChangedLineInfo!=null&&mChangedLineInfo.lineNumbers!=null&&mChangedLineInfo.lineNumbers.size()>0){
        for (String line : this.mChangedLineInfo.lineNumbers){
            if (line!=null){
               int lineNum = Integer.parseInt(line);
              //是否落在xx方法中
               if (lineNum>=startLine&&lineNum<=endLine){
                   isContained = true;
                    break;
                 }
              }
          }
      }
      if (isContained&&this.methodNameList!=null){
         //包含在此方法中
         MethodName nameContained = new MethodName();
         nameContained.methodName = name;
         this.methodNameList.add(nameContained);
        }
  			//保存
        methodInsMapList.put(name, mMethodInsInfoList);
    }

至此,我们通过自定义ClassVisitor和MethodVisitor完成了对源文件的搜集和定位。

总结一下思路:首先我们拉取了研发最后一次在Git上提交的代码,通过分析并找出规律,配合正则表达式匹配的方式,拿到修改的后缀为java的文件,又进一步的寻找规律筛选出对应java文件修改的行号;其次遍历工程源文件,利用自定义ClassVisitor和MethodVisitor进行类信息的收集包括类名、方法以及方法体指令,并在访问过程中提交后有修改痕迹的文件通过行号进行定位;最后完成收集集合的填充。这整个过程中用到很多比较重要方法,比如:CLassVisitor中的visit、visitMethod、visitEnd,以及MethodVisitor中的visitLineNumber、visitMethodInsn、visitEnd等。

3.遍历查找对应方法的上行链路和下行链路

在二步骤中完成了定位类与方法,并且完成了整个工程的源文件遍历收集,接下来就能逐步的整理出来,修改方法在整个工程中所带来的影响,

3.1 方法上行链路数据生成;

这一步骤相对来说比较简单,对于在上一步骤中,我们得到的上次的git提交定位数据,及整个工程的源文件类中方法信息的集合,我们只需要将改变的list集合在工程源文件信息集合递归循环,便能得到对应方法的上行调用链。而遍历的思路则是,递归向上扫描调用了变更集合中的类以及方法,以此递归循环遍历,只要调用到相关联的方法就被收集,对于Android应用来说,研发所写业务逻辑,基本上终止于Activity或者Applicantion中,所以向上的是有终点的。

如下是一个简图:

3.2方法下行链路数据生成;

方法的下行链路相比上行链路来说更为分散,需要我们去定位变更方法体中所有的指令,也就是扫描方法体,以及方法体各个指令的上行链路,并且在日常的开发过程中,我们的方法中有很大一部分调用的系统API,所以下行链路的扫描对比上行链路更为复杂。而对于研发或者测试,系统的API可能对我们的影响较小,所以在扫描下行链路的过程中,我们需要去识别当前方法体指令是否为系统API。

在识别去除系统API后,剩下的即是我们的业务逻辑方法,那么又回到了方法体中各个指令的上行链路扫描,方法跟上行链路一致。

对于系统的API以及一些三方库,我们大致总结了一下几种,供大家参考:

public static final String[] SYSTEM_PACKAGES = {"java/*", "javax/*", "android/*", "androidx/*","retrofit2/*","com/airbnb/*","org/apache/*"};

示意图如下:

至此,我们完成了方法上/下行链路的搜索。

4.注释及自定义Tag

上面三个步骤,我们们完成了对应方法上/下行链路功能开发,但是整条链路上只是包含了对应的类名+方法名,对于研发来讲,对应的类的作用以及方法的实现是什么逻辑比较清楚,但是仅仅局限于研发,对于测试人员可能没什么用,也只是一堆代码而已。针对这一问题,我们想到了注释,各个研发组在很早之前就开始接入京东自研的EOS来规范代码的注释,经过这么长时间的打磨也趋于完善。我们可以通过注释的方式来与对应的业务逻辑。我们设想能够通过某些手段去完成注释的获取,但是,注释可能也不能完全的去表达当前的业务逻辑,我们还需要提供具体的业务逻辑标注。

怎么解决呢?其实,总结起来就是,我们要说明上/下行链路涉及到的类和方法解释以及业务说明,并且可以利用一些特殊的标记去完成对应的一些特殊逻辑说明。

基于代码的注释,我们可以很容易的想到JavaDoc,包括Android的开发环境Android studio中也自带了可以生成源文件的javadoc(路径:Tools-->Generate JavaDoc),执行命令后几秒钟后,生成了一份完整的文档。

既然自带的工具可以完成Java文件注释的提取,那么我们也可以在代码中获取到对应的注释,经过相关资料,了解到,JDK中自带的tools.jar包可以完成JavaDoc的提取。

在将tools包上传Maven后在gradle中进行依赖,基本就完成了环境的配置。经过多方资料的查找及demo实验,tools包支持命令的形式生JavaDoc。这里需要注意的是,我们不需要html形式的javadoc文档形式,所以需要进行一些自定义的东西来达到我们自己的要求。

官方文档是这样说的:

If you run javadoc without the -doclet command-line option, it will default to the standard doclet to produce HTML-format API documentation.

也就说,我们需要在命令行中添加 -doclet来进行自定义文档。并且给出自定义的Doclet类:

	public static  class JDDoclet {
		public static boolean start(RootDoc root) {
			JDJavaDocReader.root = root;
			return true;
		}
	}

接下来简单的封装tools中的execute方法:

public synchronized static RootDoc readDocs(String source, String classpath,String sourcepath) {
		if (!Strings.isNullOrEmpty(source)){ //java源文件或者为包名
			List<String> args = Lists.newArrayList("-doclet",
					JDDoclet.class.getName(), "-quiet","-encoding","utf-8","-private");
			if(!Strings.isNullOrEmpty(classpath)){
				args.add("-classpath");//source的class位置,可以为null,如果不提供无法获取完整注释信息(比如无法识别androidx.annotation.NonNull)
				args.add(classpath);
			}
			if(!Strings.isNullOrEmpty(sourcepath)){
				args.add("-sourcepath");
				args.add(sourcepath);
			}
			args.add(source);
			int returnCode = com.sun.tools.javadoc.Main.execute(JDJavaDocReader.class.getClassLoader(),args.toArray(new String[args.size()]));
			if(0 != returnCode){
				Log.i(TAG,"javadoc ERROR CODE = %d\n", returnCode);
			}
		}
		return root;
	}

其中命令中参数,感兴趣的小伙伴可以查看官方文档,这里就不再赘述了。

基本封装完成后,就可以直接使用了,但是考虑到在遍历使用的过程中会出现多次调用解析ClassDoc的问题,这里还是建议将解析过的Java文件进行缓存处理,方便直接调用,也能减少整个编译的时间,并且在解析过程中我们也需要排除系统类的解析。

//....略
if(classDoc!=null){
     javaDocFile.append("\n\n")
      //获取类注释并写入文件
     javaDocFile.append(classDoc.getClassComment())
     javaDocFile.append(className+"\n")
     doc = classDoc.getClassDoc()
 }
//....略
if (doc!=null){
    for (MethodDoc methodDoc : doc.methods()) {
         //添加自定义Tag
         methodDoc.tags(MethodBuildConstants.CUSTOM_TAG)
         if (method.methodName.trim() == methodDoc.name().trim()){
             Tag[] tags = methodDoc.tags()
             if (tags!=null&&tags.length>0){
             		//取自定义Tag内容
                 for (int i = 0;i<tags.length;i++){
                      if (tags[i].name() == "@"+MethodBuildConstants.CUSTOM_TAG){
                          javaDocFile.append(tags[i].text()+"\n")
                          javaDocFile.append(method.methodName+"\n")
                        }
              }
           }else{//如果没有tag则输出对应的所有注释
                javaDocFile.append(methodDoc.commentText()+"\n")
                javaDocFile.append(method.methodName+"\n")
            }
         }
      }
  }

这里我们也给出自定义Tag,当然,在项目中可以根据自己的业务名称进行命名。

/**
 * 自定义tag标签
 */
public static final String CUSTOM_TAG = "LogicIntroduce";

完成了功能的开发,我们需要在代码中中进行验证,测试如下:

/**
 * 打印方法(谁调用就会被打印),会打印两次
 * @LogicIntroduce 这个是自定义Tag  getPrintMethod方法
 */
public static void getPrintMethod(){
    System.out.println("我被调用了");
    getPrintMethod2();
}

当我们的方法调用链涉及到getPrintMethod()时,就会提取@LogicIntroduce标签后面的内容,达到了获取业务逻辑说明注释的目的。这样对于那些不动代码的非研发人员,也能够非常清晰的看懂这部分代码涉及到的业务逻辑,测试也能够着重的进行测试了。

本地输出:

影响类:com/jd/fragment/test/utils/TestUtils.java (测试类)
影响方法:
  getPrintMethod(这个是自定义Tag  getPrintMethod方法)

5.推荐实际业务使用

方法的调用上下链路在上述步骤中已经生成,我们可以在MarkDown中简单的生成调用链,至于要遵循什么样的格式,大家可以自己查阅,相对比较简单不再展开。下面是推荐位最近一次修改涉及到的部分流程图:

代码修改位置输出为:

com/jingdong/xxx/RecommendItem.java //被修改的文件
252	 //修改的行

向上调用链展示:

向下调用链,我们只取本方法体:

相关Javadoc输出:

//上行调用链
影响类:com/jingdong/xxx/RecommendItem(推荐位基础数据bean对象)
影响方法:
  productExpoData(生成曝光数据给外部使用)
  generateExpoData(商卡构造曝光用数据)
  setData(服务端JSON数据解析)

影响类:com/jingdong/xxx/RecommendProductPageView (推荐UI组件)
影响方法:
	toRecomendList(网络接口返回数据)

影响类:com/jingdong/xxx/RecommendProductPageView$3(服务端数据处理(内部类))
影响方法:
	toList(接口数据处理)

//下行调用链
影响类:com/jingdong/xxx/RecommendItem(推荐位基础数据bean对象)
影响方法:
  productExpoData(生成曝光数据给外部使用)
      -com/jd/xxx/JDJSONObject

五、总结

经过上面的描述,我们整体上完成了再Android端的代码影响范围工具探索,过程中完成了Git定位,生成方法调用的上、下链路,以及通过JDK工具jar包完成注释以及自定义Tag的内容获取,也通过MarkDown生成了对应的流程图。下面是整个工程的流程说明图:

对于这个工具来说,我们仅仅是对Android客户端的探索开发,目前已在推荐组进行试用,使用过程中还有一些问题以及流程需要进一步改善和优化,比如,当一个方法被多处调用则生成的关系图就会过去庞大,不容易被阅读;无法突出调用链节点的一些关键节点;JavaDoc强依赖于研发,如果注释不规范或者不写,那整个链路的说明就会断掉等等,我们会持续性的去优化打磨这个工具,也会在使用过程中添加一些更贴近业务的功能,或者调整部分流程,比如说会在本地编译触发或者手动触发,或者添加一些JavaDoc的模板等等。这些功能会在业务使用过程中进行调整。后续,也会在服务端铺开,逐步的拓展业务面,为我们的业务开发交付降本增效。

参考文档:

http://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/doclet/overview.html

http://git-scm.com/docs/git-diff

本文转载于网络 如有侵权请联系删除

相关文章

  • ASP.NET中页面传值技巧

    在ASP.NET中有几种基于页面间的传值方法:常见的有QueryString.这种方法最简单不过了,在ASP.NET中有几种基于页面间的传值方法:常见的有QueryString.这种方法最简单不过了,但缺点就是它把要传送的值显示在地址栏中,如果对于在安全性的信息来说这并不是一种好的解决方案。还有一个缺点就是它不能传对象。这种方法适合于传送一个简单的值及安全性不大重要的信息。例:   有两个页面:WebForm1.aspx,WebForm2.aspx。  在WebForm1.aspx.cs的某些事件中放置如下代码:  stringurl="WebForm2.aspx?name="+this.txtname.Text;  Response.Redirect(url); 接着关键时刻出现了:在WebForm2.aspx.cs中某些事件中放置如下代码: lblname.Text=Request.QueryString["name"]; OK!整个传值过程完成!  还有一种是利用Session变量来传值,这种也比较常用。利用Session比较灵活,可以 在

  • 今天 1024,为了不 996,Lombok 用起来以及避坑指南

    Lombok简介、使用、工作原理、优缺点 Lombok项目是一个Java库,它会自动插入编辑器和构建工具中,Lombok提供了一组有用的注解,用来消除Java类中的大量样板代码。简介官方介绍ProjectLombokisajavalibrarythatautomaticallyplugsintoyoureditorandbuildtools,spicingupyourjava.Neverwriteanothergetterorequalsmethodagain,withoneannotationyourclasshasafullyfeaturedbuilder,automateyourloggingvariables,andmuchmore.翻译之后就是:Lombok项目是一个Java库,它会自动插入您的编辑器和构建工具中,简化您的Java。不需要再写另一个getter、setter、toString或equals方法,带有一个注释的您的类有一个功能全面的生成器,可以自动化您的日志记录变量,以及更多其他功能官网链接使用添加maven依赖<dependency> <gr

  • image-rs - Rust下的图像处理库

    本期带来的是Rust下的图像处理库,image-rs。Github:image-rsrepoDocsrs:image-rsdoc根据Github仓库页面的介绍,image-rs提供了基础的图像处理功能和图像格式转换功能。所有的图像处理函数都通过GenericImage和ImageBuffer完成。image-rs支持的图像格式如下:从上图我们可以看出image-rs基本支持了应用中常见的图像容器格式类型。关于ImageDecoder和ImageDecoderExt 所有的图像格式decoders都包含了ImageDecoder实现,其中主要过程是从图像文件中获取图像的metadata并解码图像。其中一些decoders的比较重要的参数包括:dimensions:返回包含图像的宽度和高度的元组数据color_type:返回由decoder返回的图像的色彩类型read_image:把图像解码成bytes关于像素,image提供了如下几种像素类型:Rgb:包含Rgb像素Rgba:包含Rgba像素(a为alpha,透明通道)Luma:灰度像素LumaA:包含alpha通道的灰度像素图像处理函数

  • 2019年社零行业数据统计与投资建议

    2019年12月CS商贸零售行业上涨5.42%,与上年同期相比提振9.44pct,但跑输大盘1.58pct。12月份中信一级30个行业均呈上涨状态,市场表现较为乐观。CS商贸零售行业12月市场表现强于去年同期,在中信29个行业排名第24位,处于中下游水平,较上月持平。从子板块来看,涨跌幅排行前三的分别是家电3C、百货和综合业态,涨跌幅分别为12.76%、9.92%、7.59%,表现均超过沪深300,除其他连锁以外,子版块在连续几月的超跌中实现反弹。2019年全年CS商贸零售估值水平整体呈现阶梯式上扬趋势,至9月中旬达到年内阶段性高位,随后10月底回落至20X左右,11月整体呈现“微笑曲线”,一直持续到12月中旬,整体看1-12月平均估值水平为21.8X。12月CS一般零售估值水平显著提升,至下旬涨幅趋于平稳,截止12月31日,CS一般零售估值水平提振至38.3X,略微高于年内平均水平,月均估值水平为37.6X。行业数据跟踪社会消费品零售数据消费持续发挥对经济增长的拉动效应,据测算2019年最终消费支出对经济增长的贡献率为57.8%,分别比资本形成总额、货物和服务净出口高26.6pct和

  • 编码方法论,赋能你我他

    导读DonRoberts提出的一条重构准则:第一次做某件事时只管去做;第二次做类似的事时会产生反感,但无论如何还是可以去做;第三次再做类似的事时,你就应该重构。编码也是如此,当多次编写类似的代码时,我们需要考虑是否有一种方法能够提高编码速度。作者多年来致力于敏捷开发,总结了一套编码的方法论,有助于程序员"快速、优质、高效"地进行编码。方法1:手工编写代码大多数刚学习Java的程序员,都会怀着一种崇敬的仪式感,一字一句地在开发工具上敲出以下代码:publicclassTest{ publicstaticvoidmain(String[]args){ System.out.println("Helloworld!"); } }复制没错,这就是经典的"Helloworld",这也是大多数人手工编写的第一个程序。手工编写代码,更能体现一个程序员的基本素质。有很多公司,都把上机编程考试作为面试的重要手段之一。面试者需要根据题目的要求,挑选一款熟悉的编程工具(比如Eclipse),手工编写代码并调试运行通过。在整个过程中,不能通过网络搜索

  • CXF - 拦截器

    服务器端自定义拦截器packageorg.shi.cxf; importjava.io.FileNotFoundException; importjava.io.PrintWriter; importjavax.xml.ws.Endpoint; importorg.apache.cxf.ext.logging.LoggingOutInterceptor; importorg.apache.cxf.interceptor.LoggingInInterceptor; importorg.apache.cxf.jaxws.EndpointImpl; importorg.shi.cxf.intecepter.AuthInteceptor; importorg.shi.cxf.ws.HelloWorld; importorg.shi.cxf.ws.QueryCatsByUser; importorg.shi.cxf.ws.impl.HelloWorldImpl; importorg.shi.cxf.ws.impl.QueryCatsByUserImpl; /** *WebService服

  • Java集合的subList方法分析

    本文研究List集合的subList方法,测试方式为:新建一个集合,然后截取原集合的部分元素,然后去操作新集合和原集合来观察结果。1.新集合中添加元素publicstaticvoidtestSubList(){ Listlist=newArrayList<>(); list.add(1); list.add(2); list.add(3); list.add(4); list.add(5); list.add(5); list.add(6); //截取集合 ListsubList=list.subList(0,3); System.out.println(subList); //截取的新集合添加元素 subList.add(33); System.out.println(subList); System.out.println(list); }复制结果为:[1,2,3] [1,2,3,33] [1,2,3,33,4,5,5,6]复制结论:我们会发现,虽然list和subList是两个不同的对象,但是我们在操作新集合时,发现原集合的数据也改变了,看一下源码:SubList

  • DDoS防御的8种方针详解

    对于遭受DDOS攻击的情况是让人很尴尬的,如果我们有良好的DDoS防御方法,那么很多问题就将迎刃而解,我们来看看我们有哪些常用的有效地方法来做好DDoS防御呢。对于DDoS防御的理解:对付DDOS是一个系统工程,想仅仅依靠某种系统或产品防住DDOS是不现实的,可以肯定的是,完全杜绝DDOS目前是不可能的,但通过适当的措施抵御90%的DDOS攻击是可以做到的,基于攻击和防御都有成本开销的缘故,若通过适当的办法增强了抵御DDOS的能力,也就意味着加大了攻击者的攻击成本,那么绝大多数攻击者将无法继续下去而放弃,也就相当于成功的抵御了DDOS攻击。DDoS防御的方法:1、采用高性能的网络设备首先要保证网络设备不能成为瓶颈,因此选择路由器、交换机、硬件防火墙等设备的时候要尽量选用知名度高、口碑好的产品。再就是假如和网络提供商有特殊关系或协议的话就更好了,当大量攻击发生的时候请他们在网络接点处做一下流量限制来对抗某些种类的DDOS攻击是非常有效的。2、尽量避免NAT的使用无论是路由器还是硬件防护墙设备要尽量避免采用网络地址转换NAT的使用,因为采用此技术会较大降低网络通信能力,其实原因很简单,因为

  • 迪士尼版“智能气动夹克”,可通过物理反馈模拟真实触感 | 黑科技

    迪士尼将其命名为ForceJacket(气动夹克),这种夹克内部装有压力传感器和气动气囊,为人的身体提供准确的力反馈和振动。随着科技的进步,很多科技公司都在进一步探索各种奇妙又有趣的技术,并且进行大举投资,迪斯尼也希望能够站在虚拟现实领域带给用户最逼真的感受。近日,据外媒报道,迪士尼研发了一款智能夹克,向全世界展示了一种关于触觉反馈机制最新的研究。据悉,迪士尼将其命名为ForceJacket(气动夹克),里面包含26个充气气囊,位于躯干和手臂之间,每个气囊配备一个压力传感器,以便精准地控制施压区域。气囊和传感器与控制系统连接,算法会控制它的充气、放气速度,力度,还有持续时间。压力和振动与视觉展示相对应,可以模拟十几种触感,比如心跳加速、雨滴降落、雪球砸到身体、手拍在肩膀上、汗水滴到身上、虫子在身上爬、蛇在身上爬、摩托车的振动。虽然很多人都穿着带有触觉反馈的紧身衣和背心玩游戏,但是实际上感受到的并不是真实压力,只是小小的晃动而已。对于迪斯尼来说,这绝对是一个很好的方向,因为它可以将虚拟现实体验与主题乐园结合起来。该公司已经在其迪士尼度假村推出了星球大战主题体验,很明显,虚拟现实与实际互动

  • 如何实现IIS 7.0对非HTTP协议的支持

    在《再谈IIS与ASP.NET管道》介绍各种版本的IIS的设计时,我们谈到IIS7.0因引入WAS提供了对非HTTP协议的支持。这个对于WCF的服务寄宿来说意义重大,它意味着我们通过IIS/WAS寄宿的服务终结点不仅仅可以采用BasicHttpBinding、WSHttpBinding/WS2007HttpBinding等基于HTTP协议的绑定,也可以采用NetTcpBinding、NetNamedPipeBinding和NetMsmqBinding。在默认的情况下,IIS7.0针对非HTTP支持的特性是关闭的。为了将一个IIS中的Web应用作为WCF服务的宿主,并采用非HTTP的通信协议,我们不但需要为Web应用所在的站点添加相应的站点绑定(net.tcp、net.pipe和net.msmq等),还需要为Web应用本身进行相应的设置使之允许相应的通信协议。我们可以通过三种方式实现相应的设置。一、命令行设置我们可以以命令行的方式执行Appcmd.exe来设置站点绑定和开启某个Web应用针对某种协议的支持,该命令所在的目录为%windir%\system32\inetsrv\。如下所示的

  • 从 PageRank Example 谈 Spark 应用程序调优

    最近做了关于SparkCache性能测试,开始是拿BigData-Benchmark中SparkKMeans来作为测试基准,分别测试各种Cache下应用程序的运行速度,最后使用SparkPageRankExample来验证。在做PageRank测试时,发现有很多有趣的调优点,想到这些调优点可能对用户来说是普遍有效的,现把它整理出来一一分析,以供大家参考。BigData-Benchmark中的SparkPageRank采用的是Spark开源代码里的PageRank样例代码,原理及代码实现都比较简单,下面我简单地介绍下。PageRank基本原理介绍PageRank的作用是评价网页的重要性,除了应用于搜索结果的排序之外,在其他领域也有广泛的应用,例如图算法中的节点重要度等。假设一个由4个页面组成的网络如下图所示,B链接到A、C,C链接到A,D链接到所有页面。那么A的PR(PageRank)值分别来自B、C、D的贡献之和,由于B除了链接到A还链接到C,D除了链接到A还链接B、C,所以它们对A的贡献需要平摊,计算公式为:简单来说,就是根据链出总数平分一个页面的PR值:对于上图中的A页面来说,它没

  • 利用garnett鉴定细胞

    背景网站:https://cole-trapnell-lab.github.io/garnett/文档:https://cole-trapnell-lab.github.io/garnett/docs/一、软件安装library(devtools) install.packages("devtools") devtools::install_github("cole-trapnell-lab/garnett")#缺依赖直接conda安装了复制二、下载数据库https://cole-trapnell-lab.github.io/garnett/classifiers/garnettmarkergene列表三、利用marker基因进行细胞鉴定#利用marker基因进行细胞鉴定 library(garnett) #读入10x数据,转换为cds类 pbmc_cds<-load_cellranger_data("run_count_1kpbmcs/") #marker基因文件 marker_file_path<-&quo

  • JDBC连接数据库增删改查实例

    查询所有 dao层代码: packageexample; importjava.sql.Connection; importjava.sql.DriverManager; importjava.sql.PreparedStatement; importjava.sql.ResultSet; importjava.sql.SQLException; importjava.util.ArrayList; importjava.util.List; importorg.junit.Test; importfengzhuang.Brand; publicclassBrandTest{ /** *1.sql语句:select*fromtb_brand *2.参数:不需要 *3.结果:List<Brand> * */ @Test publicvoidtestSelectAll()throwsException{ //获取连接 Stringurl="jdbc:mysql://localhost:3306/db1"; Stringname="

  • 闲着没事?你可以这样学学算法

    对于很多码农来说,算法总是显得有那么一点高深莫测,好像是一道难以跨越的坎。造成这种现象的原因,一是因为我们对算法的了解和对自己能力的了解不够,还没入门就被吓退了,另外一个原因是,大部分人实际工作中很少用到算法,很多算法都已经被封装到函数库或接口里面了,只需要调用就行,而为了能早点完成任务,我们一般都不会去想接口里面的算法如何实现,只管调用来完成任务,顺利交差就万事大吉了。 “不管用什么方法,能够完成任务就可以”,其实我觉得这种想法也是可以的,毕竟我们搞技术的都比较崇尚实用主义。但是如果我们懂得背后的原理,可能心里会舒坦一些,而且在使用别人接口达不到理想的性能要求时,可以分析是否是接口的实现有问题,从而懂得去选择其他实现版本或者自己动手写,而自己动手写,就必须懂算法了。此外,很多大公司在面试中,往往也会考你算法,很多很多人都倒在算法这个环节上,尸骨遍野。这就是作为码农,即使你的工作不是算法研究,也需要了解一些基础算法的原因。 基础算法,我觉得对于大部分程序员来说,通过一定时间的练习,是可以学会的。了解了这一点,我们才不至于很容易被算法吓退。如果你是个算法小白,现在要开始学习算法了,可是又

  • RSA5、RSA6

      RSA5:只能查看标准数据源。可用来激活传输标准数据源 RSA6:能查看激活了的标准数据源,以及自建数据源。在RSA6看得到的数据源都是可以使用的数据源。并可用来对数据源进行增强     原文出自江正军技术博客,博客链接:www.cnblogs.com/jiangzhengjun

  • AGC047简要题解

    AGC047AIntegerProduct 给定\(n\)个浮点数\(a_i\),问有多少组数对\((i,\j)\)满足\(a_i\cdota_j\)是整数 \(n\leq2\times10^5\) \(a_i\)小数点后只有不超过\(9\)位数 \(a_i\)的小数点前部分可以丢掉,将小数点后部分看成\(\frack{2^a\times5^b}\)的形式,其中\(k\)是整数,\(a,\b\)要尽量小 然后即求满足\(a_i+a_j\ge0,\b_i+b_j\ge0\)的数对个数,值域很小暴力即可 AGC047BFirstSecond 给定\(n\)个串\(S_i\),定义对串\(T\)的一次变换为删掉\(T\)的第一个或第二个字符,问有多少个二元组\((i,\j)\)满足能够通过对\(S_i\)进行若干次变换使得其等于\(S_j\) \(n\leq2\times10^5\) \(\displaystyle\sum|S_i|\leq10^6\) \(S_i\)能够进行若干次变换使得其等于\(S_j\),当且仅当\(S_j\)是\(S_i\)的后缀,或\(S_j\)去

  • Jenkins发布回滚方案

    Jenkins回滚可以通过每次发布从主干打tag,然后发布的时候发tag,比如tag,v1,v2,v3如果我发布了v3,想要回滚回v2,直接在Jenkins中选择v2的tag地址重新构建就可以回滚到v2发布时的代码构建。   但是如果发布时没有打tag的习惯或者觉得重构代码比较费时如何操作? 下面看下如何配置   参数化构建过程中添加,如图,按自己需要添加 接着是发布脚本   case$Status  in   Deploy)     echo"===========Generatedeployparameters=============="     path="${WORKSPACE}/bak/$BUILD_ID"     if[-d$path];     then        

  • Windows10文件重命名/复制/移动/删除时,导致文件资源管理器卡顿,解决方案

    Windows10文件重命名/复制/移动时,导致文件资源管理器卡顿,解决方案 转 https://blog.csdn.net/sinat_42483341/article/details/99677537Windows10文件重命名/复制/移动时,导致文件资源管理器卡顿的解决方案有时,重命名文件要卡顿好久,拖拽一个文件也要卡顿好久。解决方式很简单: (1)随便打开一个文件夹,点击左上角文件->更改文件夹和搜索选项 (2)点击“清除”即可 ———————————————— win10系统电脑复制文件延时卡顿怎么办? 转 http://www.ylmfwin100.com/ylmf/11185.html   win10系统电脑复制文件延时卡顿怎么办?现本站为大家讲解方法。希望可以帮助到大家!1、打开WinRAR或双击打开任意压缩文件,点击菜单栏中的【选项】-【设置】;2、点击【综合】选卡,将下面的【把WinRAR整合到资源管理器中】选项前的勾去掉,点击【确定】保存设置即可。设置完成后重启资源管理器就可以解决复制卡顿缓慢的问题了。以上就是win10系

  • scala recursive value x$5 needs type

    recursivevaluex$5needstype的原因是使用了一个类型不确定的变量,例如 val(id,name)=(id,getName(id)) 复制  其中id是个变量,其值还不确定,就在getName(id)方法上使用.。例子不合理,但是一般这个报错就是代码写错了。

  • debian11_nftables_限制ipv6公网访问特定端口_centos8 仅允许部分IPv4访问_WEB

    debian11_nftables_限制ipv6公网访问特定端口_centos8仅允许部分IPv4访问_WEB 转载注明来源:本文链接来自osnosn的博客,写于2022-05-28. 参考 【Nftables使用指南】 【wiki-nftables】 【Quickreference-nftablesin10minutes】 背景环境 现在有不少家用的路由器,比如TPLINK,华为路由,都已经支持ipv6了。 但它们缺省都不允许从外网,通过ipv6访问内部的机器。 这些路由器的防火墙配置。对于ipv4,防火墙有一些端口映射的功能可以设置。对于ipv6,防火墙就只有一个开关。 关闭这个ipv6防火墙开关之后,其实就把内网的机器完全暴露出去了。 这时候,就需要你自己去配置,内网机器自身的防火墙规则,从而保护内网服务器。 仅限制tcp端口的ipv6访问 这是个简单的例子。只是为了限制某几个端口的ipv6公网访问。 debian安装nftables包。激活nftables服务,aptinstallnftables;systemctlenablenftables; 如果系统有什么re

  • 他来了!袋鼠云大数据基础平台EasyMR正式上线

    7月28日,在袋鼠云2022产品发布会上,袋鼠云技术负责人思枢正式宣布旗下产品「大数据基础平台EasyMR」发布。 EasyMR是袋鼠云自研的大数据基础平台,提供Hadoop、Hive、Spark、Trino、HBase、Kafka等组件,完全兼容Apache开源生态;支持企业级安全管控,一键开启LDAP+Kerberos+Ranger认证权限体系;提供一站式运维管理平台,帮助企业快速构建大数据平台,降低运维成本。 结合袋鼠云在数字化领域多年的寸积铢累,此次全新发布的大数据基础平台EasyMR紧跟开源生态的先进技术,不仅可以帮助客户轻松应对海量数据的采集、存储、计算、分析挖掘和数据安全等应用场景,并且对于智能运维的部署、升级、扩缩容、监控等进行全方位支持,真正做到成为企业便捷、智能、高效的“数据底座”。 六大特性打造国产大数据基础平台 不同于十年前的新奇,现在大家已经完全习惯自己身处于“大数据时代”这件事情,所有人都能够深切地感受到大数据对于生活带来的各种改变和便利,数据爆发的时代推动着每个个人、企业、行业,甚至是国家往前走。 当前国际形势风云变幻,中美双边关系的割裂,国家对于信创国产

相关推荐

推荐阅读