Java进阶--编译时注解处理器(APT)详解

2023-11-02

本文同步发布在掘金,未经本人允许不得转载

上篇文章《Java进阶–Java注解及其实例应用》我们使用注解+反射实现了一个仿ButterKnife功能的示例。考虑到反射是在运行时完成的,多少会影响程序性能。因此,ButterKnife本身并非基于注解+反射来实现的,而是用APT技术在编译时处理的。APT什么呢?接下来一起来看。

一、APT简介

1.什么是APT?

APT即为Annotation Processing Tool,它是javac的一个工具,中文意思为编译时注解处理器。APT可以用来在编译时扫描和处理注解。通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。注意,获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能。APT的核心是AbstractProcessor类,关于AbstractProcessor类后面会做详细说明。

2.哪里用到了APT?

APT技术被广泛的运用在Java框架中,包括Android项以及Java后台项目,除了上面我们提到的ButterKnife之外,像EventBus 、Dagger2以及阿里的ARouter路由框架等都运用到APT技术,因此要想了解以、探究这些第三方框架的实现原理,APT就是我们必须要掌握的。

3.如何在Android Studio中构建一个APT项目?

APT项目需要由至少两个Java Library模块组成,不知道什么是Java Library?没关系,手把手来叫你如何创建一个Java Library。
在这里插入图片描述
首先,新建一个Android项目,然后File–>New–>New Module,打开如上图所示的面板,选择Java Library即可。刚才说到一个APT项目至少应该由两个Java Library模块。那么这两个模块分别是什么作用呢?

1.首先需要一个Annotation模块,这个用来存放自定义的注解。

  1. 另外需要一个Compiler模块,这个模块依赖Annotation模块。

3.项目的App模块和其它的业务模块都需要依赖Annotation模块,同时需要通过annotationProcessor依赖Compiler模块。

app模块的gradle中依赖关系如下:

implementation project(':annotation')
annotationProcessor project(':factory-compiler')

APT项目的模块的结构图如下所示:

在这里插入图片描述

为什么要强调上述两个模块一定要是Java Library?如果创建Android Library模块你会发现不能找到AbstractProcessor这个类,这是因为Android平台是基于OpenJDK的,而OpenJDK中不包含APT的相关代码。因此,在使用APT时,必须在Java Library中进行。

二、从一个例子开始认识APT

在学习Java基础的时候想必大家都写过简单工厂模式的例子,回想一下什么是简单工厂模式。接下来引入一个工厂模式的例子,首先定义一个形状的接口IShape,并为其添加 draw()方法:

public interface IShape {
	void draw();
}

接下来定义几个形状实现IShape接口,并重写draw()方法:

public class Rectangle implements IShape {
	@Override
	public void draw() {
		System.out.println("Draw a Rectangle");
	}
}

public class Triangle implements IShape {
	@Override
	public void draw() {
		System.out.println("Draw a Triangle");
	}
}

public class Circle implements IShape {  
    @Override
    public void draw() {   
        System.out.println("Draw a circle");
    }
}

接下来我们需要一个工厂类,这个类接收一个参数,根据我们传入的参数创建出对应的形状,代码如下:

public class ShapeFactory {
  public Shape create(String id) {
    if (id == null) {
      throw new IllegalArgumentException("id is null!");
    }
    if ("Circle".equals(id)) {
      return new Circle();
    }
    if ("Rectangle".equals(id)) {
      return new Rectangle();
    }
    if ("Triangle".equals(id)) {
      return new Triangle();
    }
    throw new IllegalArgumentException("Unknown id = " + id);
  }
}

以上就是一个简单工厂模式的示例代码,想必大家都能够理解。

那么,现在问题来了,在项目开发过程中,我们随时可能会添加一个新的形状。此时就不得不修改工厂类来适配新添加的形状了。试想一下,每添加一个形状类都需要我们手动去更新Factory类,是不是影响了我们的开发效率?如果这个Factory类能够根据我们添加新的形状来同步更新Factory代码,岂不是就省了我们很多时间了吗?

应该怎么做才能满足上述需求呢?在第一节中已经提到了使用APT可以帮助我们自动生成代码。那么这个工厂类是不是可以使用APT技术来自动生成呢?我们唯一要做的事情就是新添加的形状类上加上一个注解,注解处理器就会在编译时根据注解信息自动生成ShapeFactory类的代码了,美哉,美哉!理想很丰满,但是,现实很骨感。虽然已经明确了要做什么,但是想要注解处理器帮我们生成代码,却还有很长的路要走。不过,不当紧,接下来我们将一步步实现注解处理器并让其自动生成Factory类。

三、使用APT处理注解

1.定义Factory注解

首先在annotation模块下添加一个Factory的注解,Factory注解的Target为ElementType,表示它可以注解类、接口或者枚举。Retention指定为RetentionPolicy.CLASS,表示该在字节码中有效。Factory注解添加两个成员,一个Class类型的type,用来表示注解的类的类型,相同的类型表示属于同一个工厂。令需一个String类型的id,用来表示注解的类的名称。Factory注解代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Factory {

    Class type();

    String id();
}

接下来我们用@Factory去注解形状类,如下:

@Factory(id = "Rectangle", type = IShape.class)
public class Rectangle implements IShape {
	@Override
	public void draw() {
		System.out.println("Draw a Rectangle");
	}
}
... 其他形状类代码类似不再贴出

2.认识AbstractProcessor

接下来,就到了我们本篇文章所要讲的核心了。没错,就是AbstractProcessor!我们先在factory-compiler模块下创建一个FactoryProcessor类继承AbstractProcessor ,并重写相应的方法,代码如下:

@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
 	 @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}

可以看到,在这个类上添加了@AutoService注解,它的作用是用来生成META-INF/services/javax.annotation.processing.Processor文件的,也就是我们在使用注解处理器的时候需要手动添加META-INF/services/javax.annotation.processing.Processor,而有了@AutoService后它会自动帮我们生成。AutoService是Google开发的一个库,使用时需要在factory-compiler中添加依赖,如下:

implementation 'com.google.auto.service:auto-service:1.0-rc4'

接下来我们将目光移到FactoryProcessor类内部,可以看到在这个类中重写了四个方法,我们由易到难依次来看:

(1) public SourceVersion getSupportedSourceVersion()

这个方法非常简单,只有一个返回值,用来指定当前正在使用的Java版本,通常return SourceVersion.latestSupported()即可。

(2) public Set<String> getSupportedAnnotationTypes()

这个方法的返回值是一个Set集合,集合中指要处理的注解类型的名称(这里必须是完整的包名+类名,例如com.example.annotation.Factory)。由于在本例中只需要处理@Factory注解,因此Set集合中只需要添加@Factory的名称即可。

(3) public synchronized void init(ProcessingEnvironment processingEnvironment)

这个方法用于初始化处理器,方法中有一个ProcessingEnvironment类型的参数,ProcessingEnvironment是一个注解处理工具的集合。它包含了众多工具类。例如:
Filer可以用来编写新文件;
Messager可以用来打印错误信息;
Elements是一个可以处理Element的工具类。

在这里我们有必要认识一下什么是Element。 在Java语言中,Element是一个接口,表示一个程序元素,它可以指代包、类、方法或者一个变量。Element已知的子接口有如下几种:

  • PackageElement 表示一个包程序元素。提供对有关包及其成员的信息的访问。
  • ExecutableElement 表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
  • TypeElement 表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
  • VariableElement 表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。

接下来,我希望大家先来理解一个新的概念,即抛弃我们现有对Java类的理解,把Java类看作是一个结构化的文件。什么意思?就是把Java类看作一个类似XML或者JSON一样的东西。有了这个概念之后我们就可以很容易的理解什么是Element了。带着这个概念来看下面的代码:

package com.zhpan.mannotation.factory;  //    PackageElement

public class Circle {  //  TypeElement

    private int i; //   VariableElement
    private Triangle triangle;  //  VariableElement

    public Circle() {} //    ExecuteableElement

    public void draw(   //  ExecuteableElement
                        String s)   //  VariableElement
    {
        System.out.println(s);
    }

    @Override
    public void draw() {    //  ExecuteableElement
        System.out.println("Draw a circle");
    }
}

现在明白了吗?不同类型Element其实就是映射了Java中不同的类元素!知晓这个概念后将对理解后边的代码有很大的帮助。

(4) public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

终于,到了FactoryProcessor类中最后一个也是最重要的一个方法了。先看这个方法的返回值,是一个boolean类型,返回值表示注解是否由当前Processor 处理。如果返回 true,则这些注解由此注解来处理,后续其它的 Processor 无需再处理它们;如果返回 false,则这些注解未在此Processor中处理并,那么后续 Processor 可以继续处理它们。
在这个方法的方法体中,我们可以校验被注解的对象是否合法、可以编写处理注解的代码,以及自动生成需要的java文件等。因此说这个方法是AbstractProcessor 中的最重要的一个方法。我们要处理的大部分逻辑都是在这个方法中完成。

了解上述四个方法之后我们便可以初步的来编写FactoryProcessor类的代码了,如下:

@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
        private Types mTypeUtils;
	    private Messager mMessager;
	    private Filer mFiler;
	    private Elements mElementUtils;
	    private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<>();

 	 @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
 
		mTypeUtils = processingEnvironment.getTypeUtils();
        mMessager = processingEnvironment.getMessager();
        mFiler = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(Factory.class.getCanonicalName());
        return annotations;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

		   //	扫描所有被@Factory注解的元素
	   for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {

		}

        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

上述FactoryProcessor 代码中在process方法中通过roundEnv.getElementsAnnotatedWith(Factory.class)方法已经拿到了被注解的元素的集合。正常情况下,这个集合中应该包含的是所有被Factory注解的Shape类的元素,也就是一个TypeElement。但在编写程序代码时可能有新来的同事不太了解@Factory的用途而误把@Factory用在接口或者抽象类上,这是不符合我们的标准的。因此,需要在process方法中判断被@Factory注解的元素是否是一个类,如果不是一个类元素,那么就抛出异常,终止编译。代码如下:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
	 //  通过RoundEnvironment获取到所有被@Factory注解的对象
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
		if (annotatedElement.getKind() != ElementKind.CLASS) {
			 throw new ProcessingException(annotatedElement, "Only classes can be annotated with @%s",
                    Factory.class.getSimpleName());
	         }
	         TypeElement typeElement = (TypeElement) annotatedElement;
	         FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(typeElement);
				...
        }
        return true;
}

基于面向对象的思想,我们可以将annotatedElement中包含的信息封装成一个对象,方便后续使用,因此,另外可以另外声明一个FactoryAnnotatedClass来解析并存放annotatedElement的相关信息。FactoryAnnotatedClass代码如下:

public class FactoryAnnotatedClass {
    private TypeElement mAnnotatedClassElement;
    private String mQualifiedSuperClassName;
    private String mSimpleTypeName;
    private String mId;

    public FactoryAnnotatedClass(TypeElement classElement) {
        this.mAnnotatedClassElement = classElement;
        Factory annotation = classElement.getAnnotation(Factory.class);
        mId = annotation.id();
        if (mId.length() == 0) {
            throw new IllegalArgumentException(
                    String.format("id() in @%s for class %s is null or empty! that's not allowed",
                            Factory.class.getSimpleName(), classElement.getQualifiedName().toString()));
        }
        
        // Get the full QualifiedTypeName
        try {  // 该类已经被编译
            Class<?> clazz = annotation.type();
            mQualifiedSuperClassName = clazz.getCanonicalName();
            mSimpleTypeName = clazz.getSimpleName();
        } catch (MirroredTypeException mte) {// 该类未被编译
            DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
            TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
            mQualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
            mSimpleTypeName = classTypeElement.getSimpleName().toString();
        }
    }

    // ...省去getter
}

为了生成合乎要求的ShapeFactory类,在生成ShapeFactory代码前需要对被Factory注解的元素进行一系列的校验,只有通过校验,符合要求了才可以生成ShapeFactory代码。根据需求,我们列出如下规则:

  • 只有类才能被@Factory注解。因为在ShapeFactory中我们需要实例化Shape对象,虽然@Factory注解声明了Target为ElementType.TYPE,但接口和枚举并不符合我们的要求。
  • 被@Factory注解的类中需要有public的构造方法,这样才能实例化对象。
  • 被注解的类必须是type指定的类的子类
  • id需要为String类型,并且需要在相同type组中唯一
  • 具有相同type的注解类会被生成在同一个工厂类中

根据上面的规则,我们来一步步完成校验,如下代码:


private void checkValidClass(FactoryAnnotatedClass item) throws ProcessingException {

        TypeElement classElement = item.getTypeElement();

        if (!classElement.getModifiers().contains(Modifier.PUBLIC)) {
            throw new ProcessingException(classElement, "The class %s is not public.",
                    classElement.getQualifiedName().toString());
        }

        // 如果是抽象方法则抛出异常终止编译
        if (classElement.getModifiers().contains(Modifier.ABSTRACT)) {
            throw new ProcessingException(classElement,
                    "The class %s is abstract. You can't annotate abstract classes with @%",
                    classElement.getQualifiedName().toString(), Factory.class.getSimpleName());
        }

        // 这个类必须是在@Factory.type()中指定的类的子类,否则抛出异常终止编译
        TypeElement superClassElement = mElementUtils.getTypeElement(item.getQualifiedFactoryGroupName());
        if (superClassElement.getKind() == ElementKind.INTERFACE) {
            // 检查被注解类是否实现或继承了@Factory.type()所指定的类型,此处均为IShape
            if (!classElement.getInterfaces().contains(superClassElement.asType())) {
                throw new ProcessingException(classElement,
                        "The class %s annotated with @%s must implement the interface %s",
                        classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),
                        item.getQualifiedFactoryGroupName());
            }
        } else {
            TypeElement currentClass = classElement;
            while (true) {
                TypeMirror superClassType = currentClass.getSuperclass();

                if (superClassType.getKind() == TypeKind.NONE) {
                    // 向上遍历父类,直到Object也没获取到所需父类,终止编译抛出异常
                    throw new ProcessingException(classElement,
                            "The class %s annotated with @%s must inherit from %s",
                            classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),
                            item.getQualifiedFactoryGroupName());
                }

                if (superClassType.toString().equals(item.getQualifiedFactoryGroupName())) {
                    // 校验通过,终止遍历
                    break;
                }
                currentClass = (TypeElement) mTypeUtils.asElement(superClassType);
            }
        }

        // 检查是否由public的无参构造方法
        for (Element enclosed : classElement.getEnclosedElements()) {
            if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
                ExecutableElement constructorElement = (ExecutableElement) enclosed;
                if (constructorElement.getParameters().size() == 0 &&
                        constructorElement.getModifiers().contains(Modifier.PUBLIC)) {
                    // 存在public的无参构造方法,检查结束
                    return;
                }
            }
        }

        // 为检测到public的无参构造方法,抛出异常,终止编译
        throw new ProcessingException(classElement,
                "The class %s must provide an public empty default constructor",
                classElement.getQualifiedName().toString());
    }

如果通过上述校验,那么说明被@Factory注解的类是符合我们的要求的,接下来就可以处理注解信息来生成所需代码了。但是本着面向对象的思想,我们还需声明FactoryGroupedClasses来存放FactoryAnnotatedClass,并且在这个类中完成了ShapeFactory类的代码生成。FactoryGroupedClasses 代码如下:

public class FactoryGroupedClasses {

    private static final String SUFFIX = "Factory";
    private String qualifiedClassName;

    private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<>();

    public FactoryGroupedClasses(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
    }

    public void add(FactoryAnnotatedClass toInsert) {
        FactoryAnnotatedClass factoryAnnotatedClass = itemsMap.get(toInsert.getId());
        if (factoryAnnotatedClass != null) {
            throw new IdAlreadyUsedException(factoryAnnotatedClass);
        }
        itemsMap.put(toInsert.getId(), toInsert);
    }

  public void generateCode(Elements elementUtils, Filer filer) throws IOException {
        //  Generate java file
        ...
    }
}

接下来将所有的FactoryGroupedClasses都添加到集合中去

   private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<>();

	// 	...
	FactoryGroupedClasses factoryClass = factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
    if (factoryClass == null) {
            String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
            factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
            factoryClasses.put(qualifiedGroupName, factoryClass);
      }
	factoryClass.add(annotatedClass);
	// ...

OK!到目前为止,所有的准备工作都已经完成了。接下来就是根据注解信息来生成ShapeFactory类了,有没有很兴奋?遍历factoryClasses集合,并调用FactoryGroupedClasses类的generateCode()方法来生成代码了:

for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
          factoryClass.generateCode(mElementUtils, mFiler);
     }

可是,当我们去掉用generateCode(mElementUtils, mFiler)方法的时候…纳尼?还是一个空方法,我们还没由实现呢!笑哭?..

四、认识JavaPoet并用其生成ShapeFactory类

到此为止,我们唯一剩余的需求就是生成ShapeFactory类了。上一节中我们在FactoryProcessor类的init(ProcessingEnvironment processingEnvironment)方法中通过processingEnvironment拿到了Filer,并且我们也提到通过Filer可以用来编写文件,即可以通过Filer来生成我们所需要的ShapeFactory类。但是,直接使用Filer需要我们手动拼接类的代码,很可能一不小心写错了一个字母就致使所生成的类是无效的。因此,我们需要来认识一下JavaPoet这个库。 JavaPoet是square公司的一个开源框架JavaPoet,由Jake Wharton大神所编写。JavaPoet可以用对象的方式来帮助我们生成类代码,也就是我们能只要把要生成的类文件包装成一个对象,JavaPoet便可以自动帮我们生成类文件了。关于这个库的使用就不详细在这里讲解了,有需要了解的可以到github查看,使用起来很简单。

好了,步入正题,使用JavaPoet构建并自动生成ShapeFactory类的代码如下:

public void generateCode(Elements elementUtils, Filer filer) throws IOException {
        TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);
        String factoryClassName = superClassName.getSimpleName() + SUFFIX;
        String qualifiedFactoryClassName = qualifiedClassName + SUFFIX;
        PackageElement pkg = elementUtils.getPackageOf(superClassName);
        String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString();

        MethodSpec.Builder method = MethodSpec.methodBuilder("create")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(String.class, "id")
                .returns(TypeName.get(superClassName.asType()));
        method.beginControlFlow("if (id == null)")
                .addStatement("throw new IllegalArgumentException($S)", "id is null!")
                .endControlFlow();

        for (FactoryAnnotatedClass item : itemsMap.values()) {
            method.beginControlFlow("if ($S.equals(id))", item.getId())
                    .addStatement("return new $L()", item.getTypeElement().getQualifiedName().toString())
                    .endControlFlow();
        }

        method.addStatement("throw new IllegalArgumentException($S + id)", "Unknown id = ");

        TypeSpec typeSpec = TypeSpec
                .classBuilder(factoryClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(method.build())
                .build();

        JavaFile.builder(packageName, typeSpec).build().writeTo(filer);
    }

好了,现在项目已经可以帮我们自动来生成需要的Java文件啦。接下来验证一下,Build一下项目,切换到project模式下,在app–>build–>generated–>source–>apt–>debug–>(package)–>factory下面就可以看到ShapeFactory类,如下图:

这里写图片描述

这个类并非是我们自己编写的,而是通过使用APT的一系列骚操作自动生成的。现在可以再添加一个形状类实现IShape并附加@Factory注解,再次编译后都自动会生成到ShapeFactory中!

到此为止,本篇文章就告一段落了。相信看完本篇文章一定大有所获,因为掌握了APT技术之后,再去研究使用APT的第三方框架源码,一定会游刃有余,事半功倍。

由于本篇文章结构比较复杂且代码也较多,项目的源码已经放在文章末尾,可作参考。

源码下载

参考&推荐阅读

Java注解处理器

JDK文档AbstractProcessor

开源库推荐

BannerViewPager

一个基于ViewPager2实现的具有强大功能的无限轮播库。支持多种页面切换效果和指示器样式。

ViewPagerIndicator

一个适用于ViewPager和ViewPager2的指示器,支持多种滑块样式及滑动模式

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java进阶--编译时注解处理器(APT)详解 的相关文章

  • java TreeSet 和 TreeMap 源码解读

    目录 一 前言 二 TreeSet详解 1 TreeSet简介 2 TreeSet的底层实现 0 准备工作 1 TreeSet构造器 2 匿名内部类实现接口的多态 3 TreeMap构造器 4 add方法 5 put方法和put方法 6 继
  • Java高并发- 锁的优化及 JVM 对锁优化所做的努力

    在高并发环境下 激烈的锁竞争会导致程序的性能下降 所以我们有必要讨论一下有关 锁 的性能问题及注意事项 如 避免死锁 减小锁粒度 锁分离等 一 锁优化 1 1 减小锁持有时间 在锁竞争过程中 单个线程对锁的持有时间与系统性能有着直接的关系
  • Unsafe学习

    一 介绍 一个管理内存的类 Unsafe类是 final 的 不允许继承 且构造函数是private的 使用单列模式模式获取类对象 1 1 测试的类 public class UnsafeBean private static int st
  • websocket请求用自定义注解@WSRequestMapping访问,类似springmvc @RequestMapping访问。

    AnnoWebsocket websocket请求用自定义注解方式访问 类似于springmvc RequestMapping注解方式访问 源码github地址 https github com luxiangzhou AnnoWebsoc
  • Java多线程两种实现

    在java中实现多线程的方式有两种 一种是继承Thread类 另一个是实现Runnable接口 对于两种实现 各有优缺点 接下来进行对比总结一下 这两种方法 都可以实现多线程 以下为两种实现的写法 继承Thread类的方式 package
  • TCP和UDP的区别和优缺点

    TCP与UDP区别 1连接方式 TCP 面向连接 UDP UDP是无连接的 即发送数据之前不需要建立连接 2提供服务 TCP 可靠的服务 传送的数据 无差错 不丢失 不重复 且按序到达 通过校验和 重传控制 序号标识 滑动窗口 确认应答实现
  • 关于elasticsearch与kibana、IK分词器

    初识elasticsearch 正向索引和倒排索引 什么是文档和词条 每一条数据就是一个文档 对文档中的内容分词 得到的词语就是词条 elasticsearch就是面对文档存储的 可以是数据库中的一条商品数据 一个订单信息 文档数据会被反序
  • 疑似APT组织响尾蛇的JavaScript脚本调试分析

    APT组织响尾蛇JavaScript脚本调试分析 样本描述 样本分析 投递手法 HTA JS代码 JavaScript调试方式 IE 打印参数 代码逻辑 样本描述 响尾蛇投递与巴基斯坦外交政策有关的LNK文件 LNK文件不携带主要的恶意代码
  • Kafka3.1安装配置,配置Kafka集群,Zookeeper集群

    1 下载Kafka安装包 Kafka官网下载地址 https kafka apache org downloads 2 解压压缩包 tar zxvf kafka 2 12 3 1 0 tgz c kafka 3 进入配置文件目录 cd ka
  • groovy语言单元测试(spock)

    一 spock groovy单元测试的五种情况 单元测试 given mock单测中指定mock数据 模拟入参 when 触发行为 比如调用指定方法或函数 then 做出断言表达式 expect 期望的行为 when then的精简版 si
  • 如何列出 Ubuntu 上已安装的软件包

    在本教程中 我们将向您展示如何列出和过滤 Ubuntu 上已安装的软件包 当您需要在另一台计算机上安装相同的软件包或想要重新安装系统时 了解如何列出 Ubuntu 系统上已安装的软件包会很有帮助 我们还将向您展示如何检查是否安装了特定的软件
  • centos7安装apt

    centos7安装apt 搜索并下载对应版本 rpmforge release 下载地址 https www rpmfind net linux rpm2html search php query rpmforge release 执行安装
  • APT攻击流程图画法参考

    APT攻击流程图画法参考 画图网站 多组件多阶段 多文件多次网络连接行为 ATTCK Kill Chain 画图网站 我用免费的ProcessOn 图标比较多也好看 个人免费文件是9个 如果不够用只能删了 花钱是不可能花钱的 删之前可以保存
  • 无法安装 Docker - 哈希和不匹配(Ubuntu 18.04、Vagrant、Virtualbox)

    我只是无法在 Ubuntu 18 04 的新映像中安装 Docker 在 Virtualbox 中 使用 Vagrant 我正在使用官方网站的说明 https docs docker com install linux docker ce
  • 为什么这些 apt 软件包在 Ubuntu 和 Heroku 上的行为不一样?

    我想用ZBar从Python开始Heroku 在常规 Ubuntu 14 04 服务器上我可以运行 sudo apt get install python qrtools OR sudo apt get install zbar tools
  • 方法体内注释的处理

    我正在使用可插入注释处理 API 处理 java 注释 是否可以以某种方式处理使用的注释inside方法体 感谢帮助 彼得 我想 我找到了解决方案 正如我所想 当前的 javac 是不可能的 本地注释只是简单的注释 不会被可插入注释处理 A
  • 由于 apt-key 已弃用,如何使用 ansible playbook 管理 trust.gpg.d 中的密钥环文件?

    Before apt key已弃用 我正在使用 Ansible 剧本来添加和更新服务器中的密钥 眼下 apt key不再更新密钥 在几次搜索中 我发现我需要使用gpg现在 但是 我有很多服务器 我不想为每台服务器手动执行此操作 有没有办法管
  • 如果失败,如何强制 ansible 重试“apt”任务?

    我有一个ansible在许多机器上运行的剧本 在该剧本中 我尝试使用一些软件包来安装apt 但偶尔会失败 要么是因为其他剧本正在运行 定期更新或任何其他apt实例并行运行并获取锁 我基本上想在放弃之前添加一个重试循环 但未能这样做 因为不支
  • Docker 错误:无法找到包 git

    我正在使用图像nginx这是基于dockerfile ubuntu 连接到 docker 容器的 shell docker exec it
  • 无法在 mysql-apt-config [Ubuntu 14.04] 中选择“确定”

    我使用的是 Ubuntu 14 04 sudo apt get update总是给我这个选项来配置 mysql apt config 我尝试选择版本 按 tab gt 在 确定 上突出显示的键 按 Enter 但没有任何反应 它再次返回并突

随机推荐

  • Linux下访问数据库

    Linux下访问数据库 声明 本文只简单描述Linux系统下访问mysql数据库的步骤 关于连接上数据库之后的简单的对于数据库的增删改查等操作只是稍微提及 关于增删改查的语句书写 本文不再讲述 一般来说 访问数据库有如下几个步骤 1 初始化
  • C#控制台程序中使用log4.net来输出日志

    Apache log4net 库是一个帮助程序员将日志语句输出到各种输出目标的工具 log4net 是优秀的 Apache log4j 框架到 Microsoft NE T 运行时的端口 我喜欢他可以自定义输出 区分等级等特点 导入库 我们
  • Android自定义控件(三)---实战篇(详解onMeasure)

    接着Android自定义控件 二 实战篇的讲解 这篇我们来详细讲一下测量 onMeasure 和绘制 onDraw 这两个方法 首先 我们来看测量 onMeasure 方法 在这个方法里 我们主要是设置控件的宽高 widthMeasureS
  • jqgrid 翻页记录选中行

    简单的jqgrid列表 list jqGrid url contextPath getList postData data datatype json colNames 用户名 密码 colModel name name index nam
  • 递归算法

    一 一半又一只 一个人赶着鸭子去每个村庄卖 每经过一个村子卖去所赶鸭子的一半又一只 这样他经过了七个村子后还剩两只鸭子 问他出发时共赶多少只鸭子 经过每个村子卖出多少只鸭子 1 题目分析 设经过第n个村子时有fun n 只鸭子 卖去fun
  • java 根据数据库表生成实体类工具

    public class CodeGenerator 修改生成配置 public static String dbUrl 数据库连接串 public static String dbName 账号 public static String
  • 【js】Object.entries的用法

    Object entries是返回一个键值对数组 const obj one 1 two 2 three 3 const result Object entries obj console log result one 1 two 2 th
  • 【Go】Go 的项目目录

    文章目录 一 Go 的项目目录 1 适合个人开发者 2 目前流行的项目结构 3 适合企业开发者 二 Go 项目构建及编译 第一个 Go 程序 参考链接 一 Go 的项目目录 进行Go语言开发的时候 我们的代码总是会保存在 GOPATH sr
  • 阿里云物联网——MQTT协议---CONNECT

    什么是MQTT 1 1简介 MQTT的中文含义 消息队列遥测传输 MQTT的英文 Message Queuing Telemetry Transport 它是基于TCP IP协议 为硬件性能低下的远程设备和网络情况糟糕的情况下设计发布的发布
  • 邻接表的存储

    include
  • 计算机网络——TCP三次握手详解为什么两次不行

    文章目录 1 TCP报文段结构 2 三次握手 3 SYN洪泛攻击 4 为什么是 三次 TCP是面向连接的 connection oriented 即收发双方在发送数据之前 必须首先建立一个连接 这样在连接断开之前 就一直使用这个连接传输数据
  • js-数组遍历方法forEach和map的原理解析和实际应用

    数组遍历方法forEach和map的原理解析和实际应用 目录 数组遍历方法forEach和map的原理解析和实际应用 一 前言 二 代码 1 相同点 2 forEach 3 map 三 结语 一 前言 forEach和map是数组的两个方法
  • 人们热议的Web3究竟是什么?

    Web3已经是一个热词 几乎每个行业 甚至很多的人都可以从中讲到机会 那么这个机会究竟是什么呢 又该作哪些准备呢 作为一个新兴领域 加密圈盛产各种新概念 哪怕是随便关注下 就知道它总能产生源源不断的行业术语 诸如NFT DApp DeFi和
  • 测试sqlite数据库可承载数据量

    环境 vmvare10 1 ubuntu14 04 sqlite3 一 创建数据库 在当前文件夹下 执行以下命令 如已有数据库则打开 若没有则创建 二 创建所需表 Creat table mysqlitetest id TEXT name
  • 同学,同事,KTV聚会的小游戏

    1 吸心大全 搞个扑克用嘴吸住 传给旁边的人 他在吸过去 依次传下去 谁掉了就喝酒 2 两个人一队 一定要男女搭配哦 一个人口里含一口水或者饮料都可以 然后说裁判指定的话 由另一人来猜 规定时间里猜对最多的就算赢 3 弄个超大的骰子 这个估
  • 最小二乘法,最大似然估计

    最小二乘法 最大似然估计 一 最小二乘法 1 基本思想 2 作用 3 如何求解最小二乘法 二 最大似然估计 1 概念 2 似然估计的思想是 3 如何求解最大似然估计 三 最小二乘法和最大似然估计的联系和区别 四总结 一 最小二乘法 最小二乘
  • gamma分布的推导与理解

    1 概述 gamma分布与指数分布 泊松分布甚至其它一些混合分布有较为紧密的联系 本文通过对比与之相关的概率分布 建立某种联系并推导其概率密度函数 以便加深理解与认知 2 Gamma分布的必要性 在设置 Gamma 分布的两个参数 并将它们
  • Spring Cloud 学习笔记十五:搭建微服务工程之Knife4j 介绍及使用

    目录 Knife4j 介绍及使用 Knife4j 介绍及使用 Knife4j的前身是swagger bootstrap ui 前身swagger bootstrap ui是一个纯swagger ui的ui皮肤项目 具体介绍见官方文档 htt
  • 商业数据分析概论

    我正在和鲸社区参加 商业数据分析训练营活动 https www heywhale com home competition 6487de6649463ee38dbaf58b 以下是我的学习笔记 学习主题 波士顿房价数据快速查看 日期 202
  • Java进阶--编译时注解处理器(APT)详解

    本文同步发布在掘金 未经本人允许不得转载 上篇文章 Java进阶 Java注解及其实例应用 我们使用注解 反射实现了一个仿ButterKnife功能的示例 考虑到反射是在运行时完成的 多少会影响程序性能 因此 ButterKnife本身并非