JDK1.5后,Java语言提供了对注解(Annotation)的支持

JDK1.6中提供一组插件式注解处理器的标准API,可以实现API自定义注解处理器,干涉编译器的行为。

在这里,注解处理器可以看作编译器的插件,在编译期间对注解进行处理,可以对语法树进行读取、修改、添加任意元素;但如果有注解处理器修改了语法树,编译器将返回解析及填充符号表的过程,重新处理,直到没有注解处理器修改为止,每一次重新处理循环称为一个Round。

平时工作中,使用的注解,除了框架 等自带的注解,还有一些自定义的注解,而一般情况下使用自定义注解,主要是用来使用AOP对其

进行增强的。

而这篇博客所说的插件式注解处理器,是直接干预生成的字节码的文件的。

Java常用的Lombok , Android 常用的 ButterKnife 就属于此类,通过干预 Java的编译过程来达到代码增强的著名类库。

先简单描述一下Java文件的编译:

Java前端编译(Java三种编译方式:前端编译 JIT编译 AOT编译):Java源代码编译成Class文件的过程

javac编译器是官方JDK中提供的前端编译器,JDK/bin目录下的javac只是一个与平台相关的调用入口,具体实现在JDK/lib目录下的tools.jar。此外,JDK6开始提供在运行时进行前端编译,默认也是调用到javac

javac是由Java语言编写的,而HotSpot虚拟机则是由C++语言编写;标准JDK中并没有提供javac的源码,而在OpenJDK中的提供

javac编译器程序入口:com.sun.tools.javac.Main类中的main()方法

我们先来了解下javac的编译过程,大致可以分为3个过程,分别是:

  1. 解析与填充符号表过程
  2. 插入式注解处理器的注解处理过程(jsr269规范)
  3. 分析与字节码生成过程

解析与填充符号表过程会将源码转换为一棵抽象语法树(Abstract Syntax Tree,AST),AST是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。

因此可以利用Java的插入式注解处理器提供的API,读取、修改、添加抽象语法树中的任意元素。如果因为这些注解对语法树进行了修改,编译器会重新进行词法、语法的分析处理,直到所有的插入式注解没有对语法树进行修改为止。

那么,就以一个例子来说说这个插入式注解处理器:

平时工作中有时候需要查看一个方法的执行耗时,那么通常的做法是方法体前后通过两个时间戳变量来计算,这种情况对于单个方法使用,但是多个方法就显得比较麻烦了。当然也可以通过一些设计模式来解决这个问题

如果恰好使用Spring,那么使用AOP也可以解决这个问题!

但是说来说去,我就是想看一下方法的执行时间,上述的方法都太过麻烦!怎么才能方便的解决这个问题呢?

这里我先自定义一个注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
@Documented
public @interface TakeTime { /**
* 标记前缀,无实质作用,只是为了方便查找
* @return
*/
String tag() default "";
}

这是一个非常简单的自定义注解,注解只能标注在方法上,同时注解的有效期只在@Retention(RetentionPolicy.SOURCE) 源码期

这里要说明的是,这个有效期在我这个demo中可以是任意值,因为本博客说的插入式注解处理器,不管注解的生命周期是什么值,插入式注解处理器都会进行处理。

如果你的需求是,不但在编译时你需要这个注解,其他时候也需要该注解,比如我想在运行期拦截这个注解 等等!那么请更改注解的生命周期,已符合对应业务。

而如果仅仅只需要编译代码,那么设置为源码期就足够了

那么有了注解了,接下来就是注解处理器

package cn.kanyun.annotation_processor.taketime;

import com.google.auto.service.AutoService;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Names; import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.util.Set; /**
* @TakeTime 注解的注解处理器
* @SupportedSourceVersion 表示对应的版本
* @SupportedAnnotationTypes 表示处理哪种类型的注解(这是一个集合, 其值注解的全限定名)
* @AutoService @AutoService(Processor.class) :向javac注册我们这个自定义的注解处理器,
* 这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。@AutoService这里主要是用来生成
* META-INF/services/javax.annotation.processing.Processor文件的。如果不加上这个注解,那么,你需要自己进行手动配置进行注册
* <p>
* AbstractProcessor是注解处理器的抽象类,我们通过继承AbstractProcessor类然后实现process方法来创建我们自己的注解处理器,
* 所有处理注解的代码放在process方法里面
*/ @SupportedSourceVersion(value = SourceVersion.RELEASE_8)
@SupportedAnnotationTypes(value = {"cn.kanyun.annotation_processor.taketime.TakeTime"})
@AutoService(Processor.class)
public class TakeTimeProcessor extends AbstractProcessor { /**
* Messager接口提供注解处理器用来报告错误消息、警告和其他通知的方式
* 它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的
* 注意:我们应该对在处理过程中可能发生的异常进行捕获,通过Messager接口提供的方法通知用户(在官方文档中描述了消息的不同级别。非常重要的是Kind.ERROR)。
* 此外,使用带有Element参数的方法连接到出错的元素,
* 用户可以直接点击错误信息跳到出错源文件的相应行。
* 如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样),
* 这样用户会从javac中得到一个非常难懂出错信息
*/
private Messager messager; /**
* 实现Filer接口的对象,用于创建文件、类和辅助文件。
* 使用Filer你可以创建文件
* Filer中提供了一系列方法,可以用来创建class、java、resources文件
* filer.createClassFile()[创建一个新的类文件,并返回一个对象以允许写入它]
* filer.createResource() [创建一个新的源文件,并返回一个对象以允许写入它]
* filer.createSourceFile() [创建一个用于写入操作的新辅助资源文件,并为它返回一个文件对象]
*/
private Filer filer; /**
* 用来处理Element的工具类
* Elements接口的对象,用于操作元素的工具类。
*/
private JavacElements elementUtils; /**
* 用来处理TypeMirror的工具类
* 实现Types接口的对象,用于操作类型的工具类。
*/
private Types typeUtils; /**
* 这个依赖需要将${JAVA_HOME}/lib/tools.jar 添加到项目的classpath,IDE默认不加载这个依赖
*/
private JavacTrees trees; /**
* 这个依赖需要将${JAVA_HOME}/lib/tools.jar 添加到项目的classpath,IDE默认不加载这个依赖
* TreeMaker创建语法树节点的所有方法,创建时会为创建出来的JCTree设置pos字段,
* 所以必须用上下文相关的TreeMaker对象来创建语法树节点,而不能直接new语法树节点。
*/
private TreeMaker treeMaker; private Names names; @Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
elementUtils = (JavacElements) processingEnv.getElementUtils();
typeUtils = processingEnv.getTypeUtils();
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
} /**
* 该方法将一轮一轮的遍历源代码
* 处理注解前需要先获取两个重要信息,
* 第一是注解本身的信息,具体来说就是获取注解对象,有了注解对象以后就可以获取注解的值。
* 第二是被注解元素的信息,具体来说就是获取被注解的字段、方法、类等元素的信息
*
* @param annotations 该方法需要处理的注解类型
* @param roundEnv 关于一轮遍历中提供给我们调用的信息.
* @return 该轮注解是否处理完成 true 下轮或者其他的注解处理器将不会接收到次类型的注解.用处不大.
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// roundEnv.getRootElements()会返回工程中所有的Class,在实际应用中需要对各个Class先做过滤以提高效率,避免对每个Class的内容都进行扫描
roundEnv.getRootElements();
messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor注解处理器处理中");
TypeElement currentAnnotation = null;
// 遍历注解集合,也即@SupportedAnnotationTypes中标注的类型
for (TypeElement annotation : annotations) {
messager.printMessage(Diagnostic.Kind.NOTE, "遍历本注解处理器处理的所有注解,当前遍历到的注解是:" + annotation.getSimpleName());
currentAnnotation = annotation;
}
// 获取所有包含 TakeTime 注解的元素(roundEnv.getElementsAnnotatedWith(TakeTime.class))返回所有被注解了@Factory的元素的列表。你可能已经注意到,我们并没有说“所有被注解了@TakeTime的方法的列表”,因为它真的是返回Element的列表。请记住:Element可以是类、方法、变量等。所以,接下来,我们必须检查这些Element是否是一个方法)
Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(TakeTime.class);
messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor注解处理器处理@TakeTime注解");
for (Element element : elementSet) {
//获取注解
TakeTime TakeTimeAnnotation = element.getAnnotation(TakeTime.class);
//获取注解中配置的值
String tag = TakeTimeAnnotation.tag();
messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName() + "注解上设置的值为:" + tag); // TypeSpec typeSpec = generateCodeByPoet(typeElement, null); // 方法名(这里之所以是方法名,是因为这个注解是标注在方法上的)
String methodName = element.getSimpleName().toString(); // 类名[全限定名]
// element.getEnclosingElement()返回封装此元素(非严格意义上)的最里层元素,由于我们在上面判断了element是method类型,所以直接封装method的的就是类了
// http://www.169it.com/article/3400309390285698450.html
String className = element.getEnclosingElement().toString(); messager.printMessage(Diagnostic.Kind.NOTE, "当前被标注注解的方法所在的类是:" + className);
messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName() + "当前被标注注解的方法是:" + methodName); // JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
enhanceMethodDecl(elementUtils.getTree(element), tag, className + "." + methodName); if (element.getKind() == ElementKind.FIELD) {
// 当前element是字段类型
VariableElement variableElement = (VariableElement) element;
messager.printMessage(Diagnostic.Kind.ERROR, "字段不能使用@TakeTime注解", element);
} if (element.getKind() == ElementKind.CONSTRUCTOR) {
// 当前element是构造方法类型 }
} return false;
} /**
* 方法增强
*
* @param jcTree
* @param methodName 方法的全限定名
* @param tag 标识
* @return
*/
private JCTree.JCMethodDecl enhanceMethodDecl(JCTree jcTree, String tag, String methodName) {
JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) jcTree; // 生成表达式System.currentTimeMillis()
JCTree.JCExpressionStatement time = treeMaker.Exec(treeMaker.Apply(
//参数类型(传入方法的参数的类型) 如果是无参的不能设置为null 使用 List.nil()
List.nil(),
memberAccess("java.lang.System.currentTimeMillis"),
//因为不需要传递参数,所以直接设置为List.nil() 不能设置为null
List.nil()
//参数集合[集合中每一项的类型需要跟第一个参数对照]
// List.of(treeMaker.Literal())
)
); // 编译后该方法会存在一个startTime的变量,其值为编译时的时间
JCTree.JCVariableDecl startTime = createVarDef(treeMaker.Modifiers(0), "startTime", memberAccess("java.lang.Long"), treeMaker.Literal(System.currentTimeMillis())); // 耗时计算表示式
JCTree.JCExpressionStatement timeoutStatement = treeMaker.Exec(
treeMaker.Apply(
List.of(memberAccess("java.lang.Long"), memberAccess("java.lang.Long")),
memberAccess("java.lang.Math.subtractExact"),
List.of(time.expr, treeMaker.Ident(startTime.name))
) );
//
messager.printMessage(Diagnostic.Kind.NOTE, "::::::::::::::::::::");
messager.printMessage(Diagnostic.Kind.NOTE, timeoutStatement.expr.toString()); // 生成表达式System.out.println()
JCTree.JCExpressionStatement TakeTime = treeMaker.Exec(treeMaker.Apply(
//参数类型(传入方法的参数的类型) 如果是无参的不能设置为null 使用 List.nil()
List.of(memberAccess("java.lang.String"), memberAccess("java.lang.String"), memberAccess("java.lang.Long")),
// 因为这里要传多个参数,所以此处应使用printf,而不是println
memberAccess("java.lang.System.out.printf"),
//取到前面定义的startTime的变量
// List.of(treeMaker.Ident(startTime.name))
// 取得结果
List.of(treeMaker.Literal(">>>>>>>>TAG:%s -> 方法%s执行用时:%d<<<<<<<"), treeMaker.Literal(tag), treeMaker.Literal(methodName), timeoutStatement.getExpression())
)
); // catch中的代码块
JCTree.JCBlock catchBlock = treeMaker.Block(0, List.of(
treeMaker.Throw(
// e 这个字符是catch块中定义的变量
treeMaker.Ident(getNameFromString("e"))
)
));
// finally代码块中的代码
JCTree.JCBlock finallyBlock = treeMaker.Block(0, List.of(TakeTime)); List<JCTree.JCStatement> statements = jcMethodDecl.body.getStatements();
// 遍历方法体中每一行(断句符【分号/大括号】)代码
for (JCTree.JCStatement statement : statements) {
messager.printMessage(Diagnostic.Kind.NOTE, "遍历方法体中的statement:" + statement);
messager.printMessage(Diagnostic.Kind.NOTE, "该statement的类型:" + statement.getKind());
if (statement.getKind() == Tree.Kind.RETURN) {
messager.printMessage(Diagnostic.Kind.NOTE, "该statement是Return语句");
break;
} } // jcMethodDecl.body即为方法体,利用treeMaker的Block方法获取到一个新方法体,将原来的替换掉
jcMethodDecl.body = treeMaker.Block(0, List.of(
// 定义开始时间,并附上初始值 ,初始值为编译时的时间
startTime,
treeMaker.Exec(
// 这一步 将startTime变量进行赋值 其值 为(表达式也即运行时时间) startTime = System.currentTimeMillis()
treeMaker.Assign(
treeMaker.Ident(getNameFromString("startTime")),
time.getExpression()
)
),
// 添加TryCatch
treeMaker.Try(jcMethodDecl.body,
List.of(treeMaker.Catch(createVarDef(treeMaker.Modifiers(0), "e", memberAccess("java.lang.Exception"),
null), catchBlock)), finallyBlock) // 下面这段是IF代码,是我想在try catch finally后添加return代码(如果有需要的话),结果发现 如果不写下面的代码的话
// Javac会进行判断,如果这个方法有返回值的话,那么Javac会自动在try块外定义一个变量,同时找到要上一个return的变量并赋值
// 然后返回,具体可以查看编译后的字节码的反编译文件,如果该方法没有返回值,那么什么也不做 // 根据返回值类型,判断是否在方法末尾添加 return 语句 判断返回类型的Kind是否等于TypeKind.VOID
// treeMaker.If(treeMaker.Parens(
// treeMaker.Binary(
// JCTree.Tag.EQ,
// treeMaker.Literal(returnType.getKind().toString()),
// treeMaker.Literal(TypeKind.VOID.toString()))
// ),
//
// //符合IF判断的Statement
// treeMaker.Exec(treeMaker.Literal("返回类型是Void,不需要return")),
//// 不符合IF判断的Statement
// null
// )
) ); return jcMethodDecl;
} /**
* 创建变量语句
*
* @param modifiers
* @param name 变量名
* @param varType 变量类型
* @param init 变量初始化语句
* @return
*/
private JCTree.JCVariableDecl createVarDef(JCTree.JCModifiers modifiers, String name, JCTree.JCExpression varType, JCTree.JCExpression init) {
return treeMaker.VarDef(
modifiers,
//名字
getNameFromString(name),
//类型
varType,
//初始化语句
init
);
} /**
* 根据字符串获取Name,(利用Names的fromString静态方法)
*
* @param s
* @return
*/
private com.sun.tools.javac.util.Name getNameFromString(String s) {
return names.fromString(s);
} /**
* 创建 域/方法 的多级访问, 方法的标识只能是最后一个
*
* @param components
* @return
*/
private JCTree.JCExpression memberAccess(String components) {
String[] componentArray = components.split("\\.");
JCTree.JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0]));
for (int i = 1; i < componentArray.length; i++) {
expr = treeMaker.Select(expr, getNameFromString(componentArray[i]));
}
return expr;
} }

这里我自定义的注解处理器,主要操作Javac在编译被注解标注的方法时,在生成字节码时添加自己的逻辑

首先在 方法体的开头 插入一条当前时间的变量 ,并赋值为 System.currentTimeMillis()

然后将整个方法体包括在try块中,添加catch 即finally

catch块中直接定义异常并跑出,finally块中打印用时语句!

之所以添加try catch finally语句,主要是考虑有的方法是在if判断中返回,所以如果在每个return前插入打印用时代码,就十分麻烦

所以直接使用try块包裹方法体来实现!

另外为什么catch块中直接要出异常?因为如果原来的方法体捕获了异常,那么自然不会走自己创建的catch块,如果没有捕获,那么自定义的catch块

会把这个异常原封不动的抛出去,这样并不影响原来的业务了!

直接说并不直观,放两张图片说明问题

这张图是源码

这是编译后的源码,编译后的源码是.class文件,我用IDEA直接打开class文件 就是反编译后的文件

可以看到反编译后的代码,添加了时间变量,和try块代码,需要注意的是,startTime被赋了初始值,这个值其实是这个class被编译的时间,这个初始值也可以设置为其他值,当然必须是Long类型的,或者设置为null,我这里设置这个时间主要是用来测试!建议设置为null

最后看一下 原方法的打印结果:

因为源码中 sleep了3秒中,所以最后直接时间是3001毫秒

同时 mmm 是 注解中的自定义的tag的值,之所以设置这个,是因为如果方法较多时,方便进行查找等!(其实是参照的Android的Logger)

关于注解处理器的文章有很多,我这里只写一些自己认为比较重要的!

1.关于@AutoServier注解 :如果使用的是maven话,那么直接引入google的 auto-service依赖即可,如果使用的是gradle的话且版本在5(包含)之后

需要添加两条依赖:

//    声明注解处理器的注解,用于代替手动编辑resources/META-INF/services的文件
compile group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'
// 这行配置也需要添加,gradle升级到5之后,不加此配置,不会生成META-INF/services/javax.annotation.processing.Processor文件
annotationProcessor group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'

如果当注解处理器打完包后,被其他项目(gradle)引用,也要 使用 compile / annotationProcessor 来引入两次

2.理解几个常见的类 :

 声明变量 
 
JCTree.JCVariableDecl
定义变量 long a = 1
JCTree.JCExpressionStatement
生成表达式
a = System.currentTimeMillis()
JCTree.JCBlock
代码块(主要是用来放其他代码块,或者JCExpressionStatement的)  

关于 更多API参照:https://blog.csdn.net/a_zhenzhen/article/details/86065063

简单用法参照:https://blog.csdn.net/dap769815768/article/details/90448451

源码:https://github.com/chenwuwen/annotation_processor

JAVA 插入注解处理器的更多相关文章

  1. JAVA通过注解处理器重构代码,遵循单一职责

    前言:最近在看一个内部网关代码的时候,发现处理Redis返回数据这块写的不错,今天有时间好好研究下里面的知识点. 业务流程介绍: #项目是采用Spring Boot框架搭建的.定义了一个@Redis注 ...

  2. Java编译期注解处理器详细使用方法

    目录 Java编译期注解处理器 启用注解处理器 遍历语法树 语法树中的源节点 语法树节点的操作 给类增加注解 给类增加import语句 构建一个内部类 使用方法 chainDots方法 总结 Java ...

  3. 深入理解Java:注解(Annotation)--注解处理器

    如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了.使用注解的过程中,很重要的一部分就是创建于使用注解处理器.Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处 ...

  4. java 命名代码检查-注解处理器

    命名代码检查 根据 <Java 语言规范( 第 3 版 ) > 中第6.8节的要求, Java 程序命名应当符合下列格式的书写规范: 类 ( 或接口 ) : 符合驼式命名法, 首字母大写. ...

  5. Java注解处理器(转)

    Java中的注解(Annotation)是一个很神奇的东西,特别现在有很多Android库都是使用注解的方式来实现的.一直想详细了解一下其中的原理.很有幸阅读到一篇详细解释编写注解处理器的文章.本文的 ...

  6. Java注解处理器使用详解

    在这篇文章中,我将阐述怎样写一个注解处理器(Annotation Processor).在这篇教程中,首先,我将向您解释什么是注解器,你可以利用这个强大的工具做什么以及不能做什么:然后,我将一步一步实 ...

  7. Java注解(3)-注解处理器(编译期|RetentionPolicy.SOURCE)

    注解的处理除了可以在运行时通过反射机制处理外,还可以在编译期进行处理.在编译期处理注解时,会处理到不再产生新的源文件为止,之后再对所有源文件进行编译. Java5中提供了apt工具来进行编译期的注解处 ...

  8. Java注解(2)-注解处理器(运行时|RetentionPolicy.RUNTIME)

    如果没有用来读取注解的工具,那注解将基本没有任何作用,它也不会比注释更有用.读取注解的工具叫作注解处理器.Java提供了两种方式来处理注解:第一种是利用运行时反射机制:另一种是使用Java提供的API ...

  9. Java注解处理器--编译时处理的注解

    1. 一些基本概念 在开始之前,我们需要声明一件重要的事情是:我们不是在讨论在运行时通过反射机制运行处理的注解,而是在讨论在编译时处理的注解.注解处理器是 javac 自带的一个工具,用来在编译时期扫 ...

随机推荐

  1. PHPStorm提示:phpdoc comment doesn't contain all necessary @throw tag(s)

    选择Settings => Editor => Inspection, 选择PHP => PHPDoc => Missing @throws tag(s) ,把后面的勾勾去掉就 ...

  2. un-资源-开源-WebGallery:Windows Web App Gallery

    ylbtech-资源-开源-WebGallery:Windows Web App Gallery Windows Web App Gallery 1.返回顶部   2.返回顶部   3.返回顶部   ...

  3. 转 mysql awr 报告

    1. https://github.com/noodba/myawr 2. https://www.cnblogs.com/zhjh256/p/5779533.html

  4. php环境选择

    第一个    PHPStudy 推荐这个.简单好用. 链接:https://pan.baidu.com/s/1yWRDjfnadkkUE-JX5pqZmg 提取码:4imw 第二个 PHPnow 第三 ...

  5. Flink MysqlSink 简单样例

    在大数据领域中,有很多nosql 的数据库,典型的 hbase,可以实现大数据量下的快速查询,但是关系型数据的地位还是没办法替代.比如上个项目中,计算完的结果数据,还是会输出到关系型数据库当中.Fli ...

  6. tensor&ndarray&int、float

    (1)如果tensor只有一个元素,然后转换成int或者float类型的时候直接用int()或者float()就可以了: (2)如果tensor含有多个元素,转换成ndarray时就要用x.detac ...

  7. [ kvm ] 学习笔记 6:virsh 命令及功能详解

    1. 虚拟机管理操作 attach-device 从XML文件附加设备 attach-disk 附加磁盘设备 attach-interface 连接网络接口 autostart 自动启动一个域 blk ...

  8. angular2-cookie 如何升级到 ngx-cookie

    angular2-cookie 如何升级到  ngx-cookie https://github.com/salemdar/angular2-cookie#readme

  9. JavaScript图形实例:图形的扇形变换和环形变换

    1.1  扇形变换 将如图1所示的上边长方形的图形变换为下边的扇形图形的变换称为扇形变换. 设长方形图形中任一点P1(X1,Y1)变换为扇形图形上的点P2(X2,Y2),长方形的长为X,扇形圆心坐标为 ...

  10. 《C语言程序设计》课程教学目录

    <C语言程序设计>课程教学目录 不要去精确定义单个论题,应将一整套问题组织成体系.--黎曼(1826-1866)德国著名数学家 教学过程 2018年秋季学期 C语言程序设计I-第一周教学 ...