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就是类对象。处理类对象时有两种不同的情况,分别对应两种不同的处理方式:

  1. 这个类已被编译 如我们的其他.jar中包涵已经被我们的注解编译过的的.class文件。这种情况下,注解处理器可以直接获取注解的类对象。
  2. 如果还没有被编译 直接获取类对象会抛出MirroredTypeException异常,所以我们需要try-catch去捕获这个异常,从中获取TypeMirror,再经过一系列强转,最终获得TypeElement类型,从中读取类对象信息。

元素检查

为了提高注解的健壮性,避免一些不必要的麻烦,当我们获取到TypeElement时,应该适时的做的一些元素的检查。(ps:如果不通过message输出错误信息,我们可能只会看到程序里报了个空指针,这时很容易一头雾水无法定位bug,但是message的错误信息毕竟需要手动去定义,所以还是推荐用在重要的错误上,那么元素检查就是必不可少的了!)

  1. 作用域检查:
if (!typeElement.getModifiers().contains(Modifier.PUBLIC)) {  error(classElement, "The class %s is not public.",  classElement.getQualifiedName().toString());  return false;
}
  1. 是否是抽象类:
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;
}
  1. 继承关系检查:(注意,整个检查也可以使用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);  }
}
  1. 构造方法检查:
// 检查是否提供了默认公开构造函数
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注解

  1. nullness注解

    |注解|释义| | --- | --- | |@Nullable|标记函数的参数或者返回值可以为空。| |@NonNull|与上面相反。|

  2. 类型定义注解(经常用来替换枚举) |注解|释义| | --- | --- | |@IntDef|整形。| |@StringDef|字符串。|

  3. 线程注解 |注解|释义| | --- | --- | |@UiThread|标记运行在UI线程,一个App可以有多个UI线程。| |@MainThread|标记运行在主线程,一个App只能有一个主线程。| |@WorkerThread|标记运行在后台线程。| |@BinderThread|标记运行在binder线程。|

  4. 值范围注解 |注解|释义| | --- | --- | |@Size(min=1)|标记集合不可为空。| |@Size(max=23)|标记字符串最大字符数是23。| |@Size(2)|标记数组元素个数为2.| |@Size(multiple=2)|标记数组大小是2的整数倍。|

  5. 强制调用父级方法 |注解|释义| | --- | --- | |@CallSuper|必须调用父级中被重写的方法。|

  6. 返回值检查 @CheckResult(suggest="#enforcePermission(string,int,int,string)") 提示开发者对返回值进行检查和处理

  7. 单元测试注解 |注解|释义| | --- | --- | |@VisibleForTesting|单元测试可能需要访问一些不可见的类、函数或者变量,这时可以使用这个注解使其对测试可见。|

  8. 避免混淆注解 注解|释义 | --- | --- | |@Keep|用来标记在Proguard混淆过程中不需要被混淆的类或方法。|

  9. 权限注解 @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");
      
  10. 资源注解

    注解 释义
    @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)注解相关推荐

  1. Java、Android注解代码生成(ButterKnife原理、ViewBinding)

    前言 首先需要一些先验知识: 浅谈Java/Android下的注解 Java.Android基础之-反射 Java.Android静态代理与动态代理 简介 在我们常用的框架中注解和自动生成代码的身影很 ...

  2. 浅谈Java/Android下的注解

    什么是注解 java.lang.annotation,接口 Annotation,在JDK5.0及以后版本引入. 注解是代码里的特殊标记,这些标记可以在编译.类加载.运行时被读取,并执行相应的处理.通 ...

  3. 开发自己的山寨Android注解框架

    目录 开发自己的山寨Android注解框架 开发自己的山寨Android注解框架 参考 Github黄油刀 Overview 在上一章我们学习了Java的注解(Annotation),但是我想大家可能 ...

  4. 自定义Android注解Part3:绑定

    上一节我们已经将自动生成注解代码部分介绍完毕,今天这篇文章是自定义Android注解系列的最后一篇文章.希望大家这一路走来有所收获. 经过前面的了解,我们三大部分:butterknife-annota ...

  5. java retentionpolicy_Java注解之如何利用RetentionPolicy.SOURCE生存周期

    上一篇文章简单讲了下Java注解的学习之元注解说明,学习了Java注解是如何定义的,怎么使用的,但是并没有介绍Java的注解是怎么起作用的,像Spring Boot里面的那些注解,到底是怎么让程序这样 ...

  6. 自定义Android注解Part2:代码自动生成

    上一期我们已经把butterknife-annotations中的注解变量都已经定义好了,分别为BindView.OnClick与Keep. 如果你是第一次进入本系列文章,强烈推荐跳到文章末尾查看上篇 ...

  7. Java基础-注解和反射

    Java基础-注解和反射 前言 对于注解,我主要还是在自定义APT还有运行时反射获取类来让自己能够构建出复用性更高的代码逻辑. 知识点1-注解: 注解的应用场景由元注解@Retention来进行指定, ...

  8. @retention注解作用_分分钟带你玩转SpringBoot自定义注解

    在工作中,我们有时候需要将一些公共的功能封装,比如操作日志的存储,防重复提交等等.这些功能有些接口会用到,为了便于其他接口和方法的使用,做成自定义注解,侵入性更低一点.别人用的话直接注解就好.下面就来 ...

  9. Android注解支持(Support Annotations)

    注解支持(Support Annotations) Android support library从19.1版本开始引入了一个新的注解库,它包含很多有用的元注解,你能用它们修饰你的代码,帮助你发现bu ...

最新文章

  1. 机器学习中的数据泄露是什么?构建模型中如何防止数据泄露?正确的方案是什么?如何使用pipeline防止数据泄露?
  2. 首例基因编辑干细胞治疗艾滋病:北大邓宏魁参与,达到最佳治疗效果
  3. 【安全报告】揭秘创建进程时ebx为什么指向peb的答案
  4. 外部方法调用内部_私有属性和私有方法
  5. 根据从日期控件选定的时间以表格形式显示数据_VB项目开发FlexGrid控件使用讲解...
  6. python 生成字符串_Python字符串生成器,按照特定的顺序
  7. 精美UI版iApp对接hybbs论坛功能APP源码
  8. PRML-系列一之1.5
  9. 叫板英特尔,英伟达发布首个 CPU,集齐“三芯”!
  10. springboot配置手动提交_kafka教程-springboot消费者-手动提交offset
  11. 什么是Symbian
  12. win10相机计算机无法使用,win10系统相机怎么用 win10系统相机无法使用怎么解决...
  13. pythonmacd指标编写_利用python编写macd、kdj、rsi、ma等指标
  14. 云服务器更换系统后tomcat,云服务器CentOS7系统环境配置(jdk和tomcat)(示例代码)...
  15. c语言程序设计对称数,对称数 问题
  16. 大天使之剑服务器维修公告,大天使之剑————【维护】6月12日更新维护公告...
  17. 孢子社群:今日推荐ARVR微信群:游乐VR智能
  18. 【考研经历】二战终于上岸了,还得继续努力
  19. 宝宝眼皮又长“痘”了!麦粒肿和霰粒肿怎么区分?
  20. 29 Linux 防火墙

热门文章

  1. PHP整理笔记八正则表达式
  2. LVS/HAProxy/Nginx负载均衡对比
  3. java连接sqlserver 2005执行存储过程的几种情况
  4. ThinkPHP 模板循环输出 Volist 标签
  5. 德国阿尔迪成功启示录(转载)
  6. [NOI2010]航空管制(拓扑排序+贪心)
  7. SpringMvc 3.x跨域+ajax请求
  8. loadrunner11使用常见问题(不断整理中)
  9. Java8 stream操作
  10. grunt使用watch和livereload的Gruntfile.js的配置