玩转java(Android)注解
2019独角兽企业重金招聘Python工程师标准>>>
玩转java(Android)注解
1. java标准(原生)注解概览
Java API 中,在java.lang、java.lang.annotation和javax.anotation包中定义了原生注解,分为三类:
- 编译注解
- 资源注解
- 元注解
1.1 编译相关注解
注解 | 释义 |
---|---|
@Override | 这个不用多说了,天天看到,是重写检查。 |
@Deprecated | 这个相信在阅读源码的时候会看到,表示方法过期或者不推荐了。 |
@SuppressWarnings | 用于包之外的其他声明项中,用来抑制某种类型的警告。(这让我想起了项目中handle的⚠️) |
@SafeVarargs | 用来断言函数的不定长参数可以放心使用。 |
@Generated | 用来告诉开发者,下面的代码是自动生成的,不建议手动去修改。 |
@FuncationInterface | 用来修饰接口,表示对应的接口是带单个方法的函数式接口。 |
1.2 资源相关注解
注解 | 释义 |
---|---|
@PostConstruct | 用在控制对象生命周期的环境中,例如Web服务器中,表示需要在构造方法之后应该立即调用被该注解修饰的方法。 |
@PreDestroy | 表示在删除一个被注入的对象之前应该先调用被该注解修饰的方法。 |
@Resource | 用于对Web容器注入资源。(我的博客就用到了,一开始还以为是SpringBoot的注解?。 |
@Resources | 同上,表示注入一个资源数组。 |
1.3 元注解
@Target 用在编写注解类时,用来指定该注解所适用的对象范围,范围如下:
| 类型 | 释义 | | --- | --- | |ANNOTATION|注解类型声明| |CONSTRUCTOR|构造函数| |FIELD|实例变量| |LOCAL_VARIABLE|局部变量| |METHOD|方法| |PACKAGE|包| |PARAMETER|方法参数| |TYPE|类,包涵枚举、接口、注解(是否包含struct?) |TYPE_PARAMETER|类型参数,是范型还是Class对象?| |TYPE_USE|类型用途?| 注意: 如果注解不使用@Target标注,那么就不能用在TYPE_PARAMETER和TYPE_USE这两种类型。
例子:
@Target({ElementType.TYPE,ElementType.PACKAGE}) public @interface CrashReport
@Retention 用来指明注解的访问范围,也就是在什么时候保留注解。 范围|释义 -|- SOURCE|源码注解,当源码被编译后就会失效。 CLASS|编译时注解,会被编译进.class文件,但程序运行时失效。能够自动处理java源文件,并且生成更多源码、配置文件、脚本或其他玩意,这种神操作是通过在编译期间执行
javac -processor
调起Java编译器内置的注解处理器来实现的,比如黄油刀、GreenDao、Databinding这些玩意。 RUNTIME|运行时注解,一直保留有效性,并可通过反射读取注解信息。相对编译时注解,性能较低,但是灵活性好。注意: 不指定@Retention注解时默认可访问到CLASS范围。
注意: 以上三种范围的注解都可以使用注解处理器进行处理
例子:
@Retention(RetentionPolicy.CLASS) public @interface CrashReport
另注:编译时注解的好处当然就是自动生成代码,减少了编码逻辑,但是坏处就是每次修改都需要重新编译生效,让我的小mbp风扇狂转的元凶终于找到了?。
- @Document 表示被修饰的注解应该被包含在「被注解项」的文档中,如JavaDoc生成的文档。
- @Inherited 表示该注解可以被子类继承。
- @Repeatable 表示这个注解可以在同一个项上面应用多次。
2.玩玩注解处理器
注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation),它诞生自java5,从java6开始开放API。注解处理器是运行在自己的jvm中的,在编译期间,javac会启动一个完整的jvm来单独跑注解处理器!
一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这具体的含义什么呢?你可以生成Java代码!这些生成的Java代码是在生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。
2.1定义注解处理器
注意:在Android中自定义注解处理器,不能放在AndroidLibrary,因为Javac不能识别AndroidLibrary,而应该放在JavaLibrary中。
由于注解处理器的一大特色就是可以在编译时生成新的java文件,所以我们先来看一下java文件的相关知识。 所谓的java文件,只不过是个以.java为拓展名的文件而已,其中也存在类似xml的dom结构一样的语法树。在这个「语法树中」,包名、类生命、类的成员都是一个个「元素」Element。如:
package com.example; // PackageElementpublic class Foo { // TypeElement(包括接口、枚举、结构体)private int a; // VariableElementprivate Foo other; // VariableElementpublic Foo () {} // ExecuteableElementpublic void setA ( // ExecuteableElementint newA //形参 TypeElement) {}
}
那么也就意味着,如果我们得到了一个类,就可以遍历这个类的成员:
TypeElement fooClass = ... ;
for (Element e : fooClass.getEnclosedElements()){ // iterate over children Element parent = e.getEnclosingElement(); // parent == fooClass
}
现在我们认识了注解处理器中三个重要的定义:
元素 | 释义 |
---|---|
Element | 代表源代码文本结构。 |
TypeElement | 代表源代码中的类型信息,我先理解成范型。 |
TypeMirror | 类文件的主要信息,通过elements.asType()获取。 |
注意:注解不能被继承,即给父类添加了注解,但是其子类是继承不到的。
自定义注解处理器,首先要继承自AbstractProcessor类。
public class FactoryProcessor extends AbstractProcessor {private Types types;private Elements elements;private Filer filer;//这个不是过滤器filter,这个是用来创建文件的private Messager messager;//Messager提供给注解处理器一个报告错误、警告以及提示信息的途径。/*** init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。* ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer** @param processingEnvironment 提供给 processor 用来访问工具框架的环境*/@Overridepublic synchronized void init(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment);types = processingEnvironment.getTypeUtils();elements = processingEnvironment.getElementUtils();filer = processingEnvironment.getFiler();messager = processingEnvironment.getMessager();}/*** 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称** @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合*/@Overridepublic Set<String> getSupportedAnnotationTypes() {Set<String> set = new LinkedHashSet();set.add(Factory.class.getCanonicalName());return set;}/*** 指定注解处理器使用的java版本** @return*/@Overridepublic SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();//一般返回java的最后版本即可}/*** 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。* 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素** @param set 请求处理的注解类型* @param roundEnvironment 有关当前和以前的信息环境* @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;* 如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们*/@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {...}
}
我们来关注一下getSupportedSourceVersion()
和getSupportedAnnotationTypes()
。这两个方法不要求强制重写,是因为在java7中添加了对应的注解,我们可以直接通过两个注解来替代重写这两个方法,如:
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.myannotation.Factory"
)
public class FactoryProcessor extends AbstractProcessor {...
}
网上很多文章都说不推荐使用注解方式来设置java版本和支持的注解类型,理由是提高兼容性。但愚以为...Java6才开放注解处理器的API,而Java7立刻就推出了这两个注解,之间仅相隔一个版本!而且现在java6跑的全都是古董级的软件。所以我就喜欢用注解的方式?。但是相应的,注解方式在java8上不能动态获取java版本,只能以版本常量做value,这点不是很方便,果然说java就是个c++--是有一定道理的!
2.2注册注解处理器
我们有了自定义的注解处理器,但他现在仅仅是一个类,并没有被调用,所以不会产生任何作用,所以我们还需要把它注册进javac,让javac在编译期间来调用它。
2.2.1 正规方法
在java的同级目录新建resources目录, 新建META-INF/services/javax.annotation.processing.Processor文件, 文件中填写你自定义的Processor全类名,注意大小写。在这个文件里写入自定义的注解处理器全名,以换行分隔:
com.example.MyProcessor
com.foo.OtherProcessor
net.blabla.SpecialProcessor
把MyProcessor.jar放到你的builpath中,javac会自动检查和读取javax.annotation.processing.Processor中的内容,并且注册MyProcessor作为注解处理器。
2.2.2 简单方法
上面的方法很简单,却比较麻烦,所以谷歌为懒人提供了福音,通过一个注解一句代码直接搞定!
首先在注解处理所在的module中添加一个依赖,我喜欢Gradle:
implementation 'com.google.auto.service:auto-service:1.0-rc2'
然后在注解处理器头部,添加注解:
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {...
}
就这样简单明了。AutoService注解可以自动在javac中注册服务,在这里我们注册的就是注解处理器,所以AutoService的Processor.class值是固定的。添加好之后,编译时javac就会自动调起注解处理器了,但是到目前为止自定义的注解处理器还是不会起作用,因为我们还没有真正使用它。
2.3 通过APT把注解处理器挂载到项目
我们之前只是在javaLibrary(即.jar,或者俗称的‘炸包?’)中定义了注解处理器,但还没有把这个炸包引入工程,但这里的引入并不是添加依赖,而是使用APT工具来挂载。
APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。现在有很多主流库都用上了 APT,比如 Dagger2, ButterKnife, EventBus3 等
项目的Gradle中代码如下:
buildscript { repositories { jcenter() mavenCentral() // add } dependencies { classpath 'com.android.tools.build:gradle:2.1.2' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // add }
}
然后在module的Gradle中:
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // add
// ...
dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.4.0' compile project(':annotations')
// compile project(':processors') 替换为下面 apt project(':processors')
}
全部搞定之后发现AS提升报错,意思是说现在已经不用手动添加APT插件了,只需要在module的gradle中添加:
annotationProcessor project(':MyClassProcessor')
就可以了, 其他的什么都不用做!
最后make一下项目,注解使用成功,完活!
3. 反射的一些知识
关于使用「类对象」
try { Class<?> clazz = annotation.type();qualifiedGroupClassName = clazz.getCanonicalName();simpleFactoryGroupName = clazz.getSimpleName();
} catch (MirroredTypeException mte) {DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();qualifiedGroupClassName = classTypeElement.getQualifiedName().toString();simpleFactoryGroupName = classTypeElement.getSimpleName().toString();
}
我们看上面的这段代码,其中的Class<?> clazz
就是类对象。处理类对象时有两种不同的情况,分别对应两种不同的处理方式:
- 这个类已被编译 如我们的其他.jar中包涵已经被我们的注解编译过的的.class文件。这种情况下,注解处理器可以直接获取注解的类对象。
- 如果还没有被编译 直接获取类对象会抛出MirroredTypeException异常,所以我们需要try-catch去捕获这个异常,从中获取
TypeMirror
,再经过一系列强转,最终获得TypeElement
类型,从中读取类对象信息。
元素检查
为了提高注解的健壮性,避免一些不必要的麻烦,当我们获取到TypeElement时,应该适时的做的一些元素的检查。(ps:如果不通过message输出错误信息,我们可能只会看到程序里报了个空指针,这时很容易一头雾水无法定位bug,但是message的错误信息毕竟需要手动去定义,所以还是推荐用在重要的错误上,那么元素检查就是必不可少的了!)
- 作用域检查:
if (!typeElement.getModifiers().contains(Modifier.PUBLIC)) { error(classElement, "The class %s is not public.", classElement.getQualifiedName().toString()); return false;
}
- 是否是抽象类:
if (typeElement.getModifiers().contains(Modifier.ABSTRACT)) { error(classElement, "The class %s is abstract. You can't annotate abstract classes with @%", classElement.getQualifiedName().toString(), Factory.class.getSimpleName()); return false;
}
- 继承关系检查:(注意,整个检查也可以使用typeUtils.isSubtype()来实现)
// 检查继承关系: 必须是@Factory.type()指定的类型子类
TypeElement superClassElement = elementUtils.getTypeElement(item.getQualifiedFactoryGroupName());
if (superClassElement.getKind() == ElementKind.INTERFACE) { // 检查接口是否实现了 if(!classElement.getInterfaces().contains(superClassElement.asType())) { error(classElement, "The class %s annotated with @%s must implement the interface %s", classElement.getQualifiedName().toString(), Factory.class.getSimpleName(), item.getQualifiedFactoryGroupName()); return false; }
} else { // 检查子类 TypeElement currentClass = classElement; while (true) { TypeMirror superClassType = currentClass.getSuperclass(); if (superClassType.getKind() == TypeKind.NONE) { // 到达了基本类型(java.lang.Object), 所以退出 error(classElement, "The class %s annotated with @%s must inherit from %s", classElement.getQualifiedName().toString(), Factory.class.getSimpleName(), item.getQualifiedFactoryGroupName()); return false; } if (superClassType.toString().equals(item.getQualifiedFactoryGroupName())) { // 找到了要求的父类 break; } // 在继承树上继续向上搜寻 currentClass = (TypeElement) typeUtils.asElement(superClassType); }
}
- 构造方法检查:
// 检查是否提供了默认公开构造函数
for (Element enclosed : typeElement.getEnclosedElements()) { if (enclosed.getKind() == ElementKind.CONSTRUCTOR) { ExecutableElement constructorElement = (ExecutableElement) enclosed; if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers() .contains(Modifier.PUBLIC)) { // 找到了默认构造函数 return true; } }
}
// 没有找到默认构造函数
error(classElement, "The class %s must provide an public empty default constructor", classElement.getQualifiedName().toString());
return false;
}
4. 代码生成
接着就是生成java的源码文件.java。说白了就是前面遇到的filer
对象提供了一个writer
用来向文件写入字符串,想想就累?,好在业界驰名Square公司首先注意到了这个问题,提供了JavaWriter和JavaPoet两个开源库。我们注意到前者已经4years没有维护了(版本才1.11.0就另起山头了。。)所以我们只来说JavaPoet的用法?。
/*** 生成代码** @param elements* @param filer* @throws IOException*/public void generateCode(Elements elements, Filer filer) throws IOException {TypeElement typeElement = elements.getTypeElement(qualifiedClassName);//得到生成的类的名称String factoryClassName = typeElement.getSimpleName() + SUFFIX;
// String factoryClassName = "Meal_Factory";//得到包名PackageElement packageElement = elements.getPackageOf(typeElement);String packageName = packageElement.isUnnamed() ? "" : packageElement.getQualifiedName().toString();ClassName meal = ClassName.get("com.example.william.entity", "Meal");ClassName margheritaPizza = ClassName.get("com.example.william.entity", "MargheritaPizza");ClassName calzonePizza = ClassName.get("com.example.william.entity", "CalzonePizza");ClassName tiramisu = ClassName.get("com.example.william.entity", "Tiramisu");//梳理要生成的代码结构MethodSpec create = MethodSpec.methodBuilder("create").addModifiers(Modifier.PUBLIC, Modifier.STATIC).returns(meal).addParameter(Integer.class, "id").addCode(" if (id == 0) { \n" +" return new $T();\n" +" } else if (id == 1) { \n" +" return new $T();\n" +" } else if (id == 2) { \n" +" return new $T(); \n" +" } else{\n" +" throw new IllegalArgumentException(\"id超出范围: \" + id);\n" +" }\n ", margheritaPizza, calzonePizza, tiramisu).build();TypeSpec helloWorld = TypeSpec.classBuilder(factoryClassName).addModifiers(Modifier.PUBLIC).addMethod(create).build();JavaFile javaFile = JavaFile.builder(packageName, helloWorld).addFileComment(" This codes are generated automatically. Do not modify!").build();javaFile.writeTo(filer);}
附录:Android支持库support-Annotation注解
nullness注解
|注解|释义| | --- | --- | |@Nullable|标记函数的参数或者返回值可以为空。| |@NonNull|与上面相反。|
类型定义注解(经常用来替换枚举) |注解|释义| | --- | --- | |@IntDef|整形。| |@StringDef|字符串。|
线程注解 |注解|释义| | --- | --- | |@UiThread|标记运行在UI线程,一个App可以有多个UI线程。| |@MainThread|标记运行在主线程,一个App只能有一个主线程。| |@WorkerThread|标记运行在后台线程。| |@BinderThread|标记运行在binder线程。|
值范围注解 |注解|释义| | --- | --- | |@Size(min=1)|标记集合不可为空。| |@Size(max=23)|标记字符串最大字符数是23。| |@Size(2)|标记数组元素个数为2.| |@Size(multiple=2)|标记数组大小是2的整数倍。|
强制调用父级方法 |注解|释义| | --- | --- | |@CallSuper|必须调用父级中被重写的方法。|
返回值检查 @CheckResult(suggest="#enforcePermission(string,int,int,string)") 提示开发者对返回值进行检查和处理
单元测试注解 |注解|释义| | --- | --- | |@VisibleForTesting|单元测试可能需要访问一些不可见的类、函数或者变量,这时可以使用这个注解使其对测试可见。|
避免混淆注解 注解|释义 | --- | --- | |@Keep|用来标记在Proguard混淆过程中不需要被混淆的类或方法。|
权限注解 @RequiresPermission
只需一个权限:
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
需多个权限:
@RequiresPermission(allOf = {Manifest.permission.SET_WALLPAPER,Manifest.permission.SET_WALLPAPER})
需要至少一个权限:
@RequiresPermission(anyOf = {Manifest.permission.SET_WALLPAPER,Manifest.permission.SET_WALLPAPER})
Intent可在Action字符串定义上添加注解:
@RequiresPermission(anyOf = {Manifest.permission.SET_WALLPAPER,Manifest.permission.SET_WALLPAPER}) String action = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";
对于ContentProvider可能会同时用到读写两个操作,可以分别定义权限
@RequiresPermission.Read(Manifest.permission.SET_WALLPAPER) @RequiresPermission.Write(Manifest.permission.SET_WALLPAPER) Uri BOOKMARKS_URI = Uri.parse("content://brower/bookmarks");
资源注解
注解 释义 @StringRes 字符串资源 @ColorRes 这个对应的是颜色资源 @AnimationRes 动画资源 @DimensionRes 尺寸资源 @DimensionPixelOffsetRes 同上,为了获取尺寸资源,但这个是会尺寸资源的单位转换为像素,并且返回的是一个int型,如有小数,则全部舍去。 @DimensionPixelSizeRes 依然同上,但这个对小数的处理是四舍五入。 @BooleanRes @ColorStateListRes @DrawableRes @IntArrayRes @IntegerRes @LayoutRes @MovieRes @TextRes @TextArrayRes @StringArrayRes @UiThread 约束方法只能运行在UI线程 @MainThread 约束方法只能在主线程运行 @WorkerThread 约束方法只能在工作线程运行 @BinderThread 约束方法只能在Binder线程运行
鸣谢:json_it的博客
转载于:https://my.oschina.net/JiangTun/blog/1808808
玩转java(Android)注解相关推荐
- Java、Android注解代码生成(ButterKnife原理、ViewBinding)
前言 首先需要一些先验知识: 浅谈Java/Android下的注解 Java.Android基础之-反射 Java.Android静态代理与动态代理 简介 在我们常用的框架中注解和自动生成代码的身影很 ...
- 浅谈Java/Android下的注解
什么是注解 java.lang.annotation,接口 Annotation,在JDK5.0及以后版本引入. 注解是代码里的特殊标记,这些标记可以在编译.类加载.运行时被读取,并执行相应的处理.通 ...
- 开发自己的山寨Android注解框架
目录 开发自己的山寨Android注解框架 开发自己的山寨Android注解框架 参考 Github黄油刀 Overview 在上一章我们学习了Java的注解(Annotation),但是我想大家可能 ...
- 自定义Android注解Part3:绑定
上一节我们已经将自动生成注解代码部分介绍完毕,今天这篇文章是自定义Android注解系列的最后一篇文章.希望大家这一路走来有所收获. 经过前面的了解,我们三大部分:butterknife-annota ...
- java retentionpolicy_Java注解之如何利用RetentionPolicy.SOURCE生存周期
上一篇文章简单讲了下Java注解的学习之元注解说明,学习了Java注解是如何定义的,怎么使用的,但是并没有介绍Java的注解是怎么起作用的,像Spring Boot里面的那些注解,到底是怎么让程序这样 ...
- 自定义Android注解Part2:代码自动生成
上一期我们已经把butterknife-annotations中的注解变量都已经定义好了,分别为BindView.OnClick与Keep. 如果你是第一次进入本系列文章,强烈推荐跳到文章末尾查看上篇 ...
- Java基础-注解和反射
Java基础-注解和反射 前言 对于注解,我主要还是在自定义APT还有运行时反射获取类来让自己能够构建出复用性更高的代码逻辑. 知识点1-注解: 注解的应用场景由元注解@Retention来进行指定, ...
- @retention注解作用_分分钟带你玩转SpringBoot自定义注解
在工作中,我们有时候需要将一些公共的功能封装,比如操作日志的存储,防重复提交等等.这些功能有些接口会用到,为了便于其他接口和方法的使用,做成自定义注解,侵入性更低一点.别人用的话直接注解就好.下面就来 ...
- Android注解支持(Support Annotations)
注解支持(Support Annotations) Android support library从19.1版本开始引入了一个新的注解库,它包含很多有用的元注解,你能用它们修饰你的代码,帮助你发现bu ...
最新文章
- 机器学习中的数据泄露是什么?构建模型中如何防止数据泄露?正确的方案是什么?如何使用pipeline防止数据泄露?
- 首例基因编辑干细胞治疗艾滋病:北大邓宏魁参与,达到最佳治疗效果
- 【安全报告】揭秘创建进程时ebx为什么指向peb的答案
- 外部方法调用内部_私有属性和私有方法
- 根据从日期控件选定的时间以表格形式显示数据_VB项目开发FlexGrid控件使用讲解...
- python 生成字符串_Python字符串生成器,按照特定的顺序
- 精美UI版iApp对接hybbs论坛功能APP源码
- PRML-系列一之1.5
- 叫板英特尔,英伟达发布首个 CPU,集齐“三芯”!
- springboot配置手动提交_kafka教程-springboot消费者-手动提交offset
- 什么是Symbian
- win10相机计算机无法使用,win10系统相机怎么用 win10系统相机无法使用怎么解决...
- pythonmacd指标编写_利用python编写macd、kdj、rsi、ma等指标
- 云服务器更换系统后tomcat,云服务器CentOS7系统环境配置(jdk和tomcat)(示例代码)...
- c语言程序设计对称数,对称数 问题
- 大天使之剑服务器维修公告,大天使之剑————【维护】6月12日更新维护公告...
- 孢子社群:今日推荐ARVR微信群:游乐VR智能
- 【考研经历】二战终于上岸了,还得继续努力
- 宝宝眼皮又长“痘”了!麦粒肿和霰粒肿怎么区分?
- 29 Linux 防火墙
热门文章
- PHP整理笔记八正则表达式
- LVS/HAProxy/Nginx负载均衡对比
- java连接sqlserver 2005执行存储过程的几种情况
- ThinkPHP 模板循环输出 Volist 标签
- 德国阿尔迪成功启示录(转载)
- [NOI2010]航空管制(拓扑排序+贪心)
- SpringMvc 3.x跨域+ajax请求
- loadrunner11使用常见问题(不断整理中)
- Java8 stream操作
- grunt使用watch和livereload的Gruntfile.js的配置