注解( annontation )是 Java 1.5 之后引入的一个为程序添加元数据的功能。注解本身并不是魔法,只是在代码里添加了描述代码自身的信息,至于如何理解和使用这些信息,则需要专门的解析代码来负责。

本文首先介绍注解的基本知识,包括注解的分类和运用时的领域知识。随后,给出一个通过的在运行时解析注解的框架代码,介绍处理注解的一般思路。最后,通过现实世界里使用注解的例子,来加深对注解的实用性方面的认识。

注解的基本知识

注解作为程序中的元数据,其本身的性质也被其上的注解所描述。

刚刚我们提到,理解和使用注解信息,需要专门的解析代码。其中,Java 的编译器和虚拟机也包含解析注解信息的逻辑,而它们判断一个注解的性质,就是依赖注解之上的元注解。

能够注解一个注解的注解就是元注解,Java 本身能够识别的元注解有以下几个。

@Retention

Retention 注解的相关定义如下

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {RetentionPolicy value();
}public enum RetentionPolicy {SOURCE,CLASS,RUNTIME
}

首先我们看到它自己也被几个元注解包括自身所注解,因此在注解的源头有一个类似于自举的概念,最终触发自举的是编译器和源代码中的先验知识。

再看到 Retention 注解的值,是一个注解保留性质的枚举,包括三种情况。

  1. SOURCE 表示注解信息仅在编译时保留,在编译之后就被丢弃,这样的注解为代码的编译提供原信息。例如常用的 @Override 注解就提示 Java 编译器进行重写方法的检查。
  2. CLASS 表示注解信息保留在字节码中,但在运行时不可见。这是注解的默认行为,如果定义注解时没有使用 Retention 注解显式表明保留性质,默认的保留性质就是这个。
  3. RUNTIME 表示注解信息在运行时可见,当然,也就必须保留在字节码中。

SOURCE 标注的注解通常称为编译期注解,Lombok 项目提供大量的编译期注解,以帮助开发者简写自己的代码。例如 @Setter 注解注解在类上时,在编译期由 Lombok 的注解处理器处理,为被注解的类的每一个字段生成 Setter 方法。

编译期的注解需要专门的注解处理器来处理,并且在编译时指定处理器的名字提示编译期使用该处理器进行处理。技术上说,编译期处理注解和运行时处理注解完全是两个概念的事情。本文主要介绍运行时处理注解的技术,关于编译期处理注解的资料,可以参考这篇 ANNOTATION PROCESSING 101 的文章以及 Lombok 的源码。

CLASS 性质虽然是默认的保留性质,但实际使用中几乎没有采用这一保留性质的。准确需要这一性质的情形应该是某些专门的字节码处理框架,大多数时候使用这一性质的注解仅仅是在编译期使用,使用 SOURCE 足以,且使用 SOURCE 还可以减少字节码文件的大小。

本文介绍运行时处理注解的技术,所有在运行时可见的注解都需要显式地标注 @Retention(RetentionPolicy.RUNTIME) 注解。CLASS 和 RUNTIME 性质的注解都会出现在字节码中。编译器将注解信息写成字节码时,通过为 CLASS 性质的注解赋予 RuntimeInvisibleAnnotations 属性,为 RUNTIME 性质的注解赋予 RuntimeVisibleParameterAnnotations 来提示虚拟机在运行时加载的时候区别对待。

运行时,我们可以调用被注解对象的相应方法取得其上的注解,具体手段在【注解解析的框架代码】一节中介绍。

@Target

上一节最后我们提到,注解有不同的注解对象,这正是 Target 注解加入的元数据,其定义如下

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {ElementType[] value();
}public enum ElementType {TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE,MODULE
}

Target 元注解的信息解释了一个注解能够被注解在什么位置上,或者说能够接受该注解的对象集合。一个注解可以有多种类型的注解对象,所有这些对象类型存在 ElementType 枚举中。

大多数枚举值的含义就是字面含义,值得一提的取值包括

  • TYPE 在 Java 中指类、接口、注解或者枚举类
  • TYPE_PARAMETER 在 Java 1.8 中被引入,指的是泛型中的类型参数
  • TYPE_USE 在 Java 1.8 中被引入,指的是所有可以出现类型的位置,具体参考 Java 语言标准的对应章节

常见的 Override 注解只能注解在方法上,Spring 框架中的 Component 注解只能注解在类型上。SuppressWarnings 注解能注解在除了本地变量和类型参数以外的几乎所有地方,Spring 框架中的 Autowired 注解也能注解在字段、构造器、方法参数和注解等多种位置上。

@Inherited

Inherited 主要用来标注注解在类继承关系之间的传递关系。它本身不携带自定义信息,仅作为一个布尔信息存在,即是或者不是 Inherited 的注解。

标注 Inherited 元注解的注解,标注在某个类型上时,其子类也默认视为标注此注解。或者换个方向说,获取某个类的注解时,会递归的搜索其父类的注解,并获取其中标注 Inherited 元注解的注解。注意,标注 Inherited 元注解的注解在子类上也标注时,子类上的注解优先级最高。

技术上说,可以通过 getAnnotationsgetDeclaredAnnotations 的区别来获取确切标注在当前类型上的注解和按照上面描述的方法查找的注解。另一个值得强调的是这种继承仅发生在类的继承上,实现接口并不会导致标注 Inherited 元注解的注解的传递。

值得注意的是,注解本身是不能继承的。为了实现类似继承的效果,开发者们从基于原型的继承找到灵感,采用本节后续将讲到的组合注解技术来达到注解继承的目的。

@Repeatable

Repeatable 注解在 Java 1.8 中被引入,主要是为了解决相同的注解只能出现一次的情况下,为了表达实际中需要的相同注解被标注多次的逻辑,开发者不得不首先创建出一个容器注解,然后使用者在单个和多个注解的情况下分别使用基础注解和容器注解的繁琐逻辑。具体例子如下

@ComponentScan(basePackages = "my.package")
class MySimpleConfig { }@ComponentScans({ @ComponentScan(basePackages = "my.package") @ComponentScan(basePackages = "my.another.package")
})
class MyCompositeConfig { }

有了 Repeatable 注解,从注解处理方,代码不会精简,仍然需要分开处理两种注解类型,但是使用方就可以精简代码。例如上面 MyCompositeConfig 的标注可以变为

@ComponentScan(basePackages = "my.package")
@ComponentScan(basePackages = "my.another.package")
class MyCompositeConfig { }

对应的注解定义为

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {// ...
}@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ComponentScans {ComponentScan[] value();
}

对于注解的处理方,重复注解会在背后由 Java 编译器转化为容器注解的形式传递。就上面的例子而言,无论有没有 Repeatable 注解,MyCompositeConfig 在获取注解时,都会获取到 ComponentScans 注解及其 ComponentScan[] 形式的元数据信息。

值得注意的是,重复注解和容器注解不能同时存在,即在标记了 @Repeatable(ComponentScans.class) 之后,ComponentScansComponentScan 不能同时标注同一个对象。

@Documented

这个注解没有太多好说的,注解信息在生成文档时默认是不会留存的。如果使用此注解标注某个注解,那么被标注的注解注解的对象的文档会显示它被对应的注解所标注。

组合注解

严格来说,组合注解是一种设计模式而不是语言特性。

由于注解无法继承,例如 Spring 框架中具有 "is-a" 关系的 Service 注解和 Component 注解,无法通过继承将 Service 定义为 Component 的特例。但是在实际使用的时候,又确实有表达这样 "is-a" 关系的需求。

在框架代码中,无法穷尽对下游项目扩展注解实质上的继承关系的情况,但是又需要支持下游项目自定义框架注解的扩展。如何将下游项目自定义的注解和框架注解之间的继承关系表达出来,就是一个技术上实际的需求。

为了解决这个问题,开发者们注意到在注解设计之初,留下了注解能够标注注解的路径。这一路径使得我们可以采用一种类似基于原型的继承的方式,通过递归获取注解上的注解来追溯注解的链条,从而类似原型链上找父类的方式找到当前注解逻辑上继承的注解。

这一技术在 Spring 框架中被广泛使用,例如 Service/Repository/Controller 等注解组合了 Component 注解,从而在下一节的注解解析的框架代码中能够作为 Component 的某种意义上的子注解被识别,同时在需要时取出继承的注解的元数据信息。

注解解析的框架代码

Java 语言提供的方法

注解解析最基础的手段是通过 Java 语言本身提供的方法。哪怕是其他框架增强注解解析的功能,最终也需要依赖基本方法的支持。

运行时获取注解信息,可想而知是通过反射的手段来获取的。Java 为被注解的元素定义了一个 AnnotatedElement 的接口,通过这一接口的方法可以在运行时取得被注解元素之上的注解。该接口的实现类是运行时通过反射拿到的元素里面能够被注解的类。

我们先看到这一接口提供的方法。

public interface AnnotatedElement {<T extends Annotation> T getAnnotation(Class<T> annotationClass);Annotation[] getAnnotations();<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass);Annotation[] getDeclaredAnnotations();<T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass);<T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass);boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);
}

这些方法没必要一个一个讲,其实可以简单地分成两类

  • 获取被注解对象上声明的注解,即 getDeclaredAnnotations 系列的方法
  • 获取被注解对象所拥有的注解,即 getAnnotations 系列的方法,比起上一类,额外包括 @Inherited 的注解

最后 isAnnotationPresent 方法仅仅是一个判断标签式注解的简易方法,内容只有一行。

default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {return getAnnotation(annotationClass) != null;
}

我们可以通过 Java 语言自身的 AnnotationSupport#getIndirectlyPresent 方法来看看怎么用这套基础支持解析注解。

private static <A extends Annotation> A[] getIndirectlyPresent(Map<Class<? extends Annotation>, Annotation> annotations,Class<A> annoClass
) {Repeatable repeatable = annoClass.getDeclaredAnnotation(Repeatable.class);if (repeatable == null)return null;  // Not repeatable -> no indirectly present annotationsClass<? extends Annotation> containerClass = repeatable.value();Annotation container = annotations.get(containerClass);if (container == null)return null;// Unpack containerA[] valueArray = getValueArray(container);checkTypes(valueArray, container, annoClass);return valueArray;
}

以上这段代码是在 Java 1.8 引入 Repeatable 注解后,由于默认的会将重复的 Repeatable 的注解在获取时直接合并成容器注解,为了提供一个方便的按照基础注解来获取注解信息的手段提供的方法。

我们看到,传入的内容包括一个根据 Class 对象查找实现类对象的映射,这个是被注解类所取得的所拥有的注解的类到实例的字典,不用过多关注。另一方面 annoClass 则是我们想要获取的基础注解的类。

例如,annoClass 为上面提过的 Spring 的 ComponentScan 类,对于仅注解了 ComponentScans 的类来说,以 ComponentScan.class 作为参数调用 getDeclaredAnnotationsByType 方法一路走到上面这个方法里,代码逻辑将会看到 ComponentScan 标注了 @Repeatable(ComponentScans.class) 注解,从而在 annotations 映射里查找 ComponentScans 注解的信息,并将它转换为 ComponentScan 的数组返回。

Spring 解析注解的方案

Spring 解析注解的核心是 MergedAnnotation 接口及相关的工具类。

Spring 框架重度使用了注解来简化开发的复杂度。对于具体的某一个或某几个注解,围绕它展开的代码散布在其逻辑链条的各处。但是,Spring 的注解处理的特别之处就在于它定义了 MergedAnnotation 接口,并支持了基于组合注解和 AliasFor 的注解增强机制。

AliasFor 注解的解析非常简单,就是查看当前注解或者 targetAnnotation 注解里面相应名称的注解。在 5.2.7.RELEASE 版本中,其解析逻辑基本在 AnnotationTypeMapping#resolveAliasTarget 方法里,最终组装出来的 AnnotationTypeMapping 对象能够在获取属性值的时候显示处理了 AliasFor 之后的属性值。

下面我们展开说一下如何递归解析组合注解。

为了支持前面提到的组合注解,即注解上的注解的递归查找,Spring 中提供了 AnnotationUtils#findAnnotation 系列方法来做查询,区别于 AnnotationUtils#getAnnotation 的单层查找。

Spring 对这个查找逻辑的演化花了很多心思。

在最新的 Spring 5.2.7.RELEASE 版本中,这两个方法都对 AnnotatedElement 构造了 MergedAnnotation 实例,在最终查找的时候通过不同的谓词策略来做筛选。构造 MergedAnnotation 实例的过程经由几个工厂函数之后构造出一个 TypeMappedAnnotations 的实例,调用其上的 get 方法构造出实际的 MergedAnnotation 对象,这个对象就是对要查找的注解递归查找的结果。

相关逻辑为了定制各种策略变得非常复杂,我们从 4.3.8.RELEASE 版本入手,查看在复杂的定制引入之前,这一查找过程核心逻辑的实现框架。

Annotation[] anns = clazz.getDeclaredAnnotations();
for (Annotation ann : anns) {if (ann.annotationType() == annotationType) {return (A) ann;}
}
for (Annotation ann : anns) {if (!isInJavaLangAnnotationPackage(ann) && visited.add(ann)) {A annotation = findAnnotation(ann.annotationType(), annotationType, visited);if (annotation != null) {return annotation;}}
}

无论后期代码演化得再复杂,其核心还是一个递归查找的过程,也就是以上的代码。

  1. 首先,获取当前的类上的注解,注意这里的类可以是一个注解类,如果此次获取的注解就包含了我们要查找的注解,那么直接返回。
  2. 如果没有包含,对刚才取得的注解递归的查找。注意这里有一个类似于深度优先搜索的 visited 集合。这是因为有些注解可以以自己为目标,导致出现递归查找的自环。典型的例如 Java 自带的元注解 Retention 也被自己所注解。
  3. 如果深度优先搜索穷尽之后没有得到结果,则返回空。

可以看到,上面的逻辑中对 Repeatable 和 Inherited 等元注解的复杂组合情况没有定制的逻辑,而是采用了一些默认的硬编码策略。

最新版本的 Spring 之所以变得相当复杂,有一部分代码量是为了解决搜索的不同策略以及跟进新版 Java 的注解特性。另一部分,注意到上述逻辑在获取注解时没有关心 AliasFor 注解的逻辑,在早期版本中这是由 AnnotationUtils 中的一个全局静态映射来管理的。在最新版本中,产生 MergedAnnotation 时将构造并维护一个本地的 alias 映射。

现实世界的注解解析

上一节介绍了处理注解的两个通用套路,背后的思想是基础的注解信息获取和递归的注解信息获取。本节我们将从现实世界的注解解析入手,介绍实际项目里面特定的注解是如何被解析的。

Flink

@RpcTimeout

Flink 采用类似 RMI 的方式来进行远程调用,为了避免无限阻塞,方法调用时可以传递一个超时参数。本地拦截远端调用的动作时,从方法的签名中反射取得标注 RpcTimeout 的参数,将它作为超时参数传递到实际的方法调用过程中,以在超过限定时间时返回超时异常而非阻塞等待远端调用的返回。

取得标注 RpcTimeout 的参数的逻辑代码展开如下

final Annotation[][] parameterAnnotations = method.getParameterAnnotations();for (int i = 0; i < parameterAnnotations.length; i++) {for (Annotation annotation : parameterAnnotations[i]) {if (annotation.annotationType().equals(RpcTimeout.class)) {if (args[i] instanceof Time) {return (Time) args[i];} else {throw new RuntimeException(/* ... */)}}}
}return defaultTimeout;

可以看到,是针对先验知识能得知的可能出现该注解的位置进行遍历获取。其实,所有的注解解析代码都遵循这样的模式,这也是最基础的模式。

JUnit 4

@Test

JUnit 4 测试框架的用户最熟悉的就是 Test 注解了。不同于上一节提到的基础解析和递归解析,JUnit 4 的 Test 注解有一个特殊的场景需要支持,即在获取当前类的所有待测试方法时,获取到父类中的 Test 标注的方法。

这是因为我们常常把相似的测试的配置和基础测试方法抽成抽象基类,在根据不同的实现场景实现不同的测试子类。虽然类似的功能可以用 Parameterized Runner 和 Parameter 注解来实现,但是 Parameter 的方案只能支持参数化字段,如果测试方法是有和没有的区别而不是参数的不同,子类是比使用 Parameter 向量并加入 Enable 开关更好的解决方案。

总之,JUnit 4 支持查找父类中标注 Test 的其他方法,此逻辑实现如下。

// TestClass#scanAnnotatedMembers
for (Class<?> eachClass : getSuperClasses(clazz)) {for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) {addToAnnotationLists(new FrameworkMethod(eachMethod), methodsForAnnotations);}// ensuring fields are sorted to make sure that entries are inserted// and read from fieldForAnnotations in a deterministic orderfor (Field eachField : getSortedDeclaredFields(eachClass)) {addToAnnotationLists(new FrameworkField(eachField), fieldsForAnnotations);}
}// TestClass#addToAnnotationLists
for (Annotation each : member.getAnnotations()) {Class<? extends Annotation> type = each.annotationType();List<T> members = getAnnotatedMembers(map, type, true);T memberToAdd = member.handlePossibleBridgeMethod(members);if (memberToAdd == null) {return;}if (runsTopToBottom(type)) {members.add(0, memberToAdd);} else {members.add(memberToAdd);}
}

其实也很简单,在初始化 TestClass 时遍历测试的候选类及其父类的所有方法和字段,将它们的注解信息存在一个注解类型到被注解对象的列表的映射中。后续需要查找的时候从该映射查找,即可查找到标注对应注解的所有方法或字段。

@RunWith

另一个常见的注解是 RunWith 注解,用于标注运行测试时采用自定义的 Runner 实现。其代码如下

for (Class<?> currentTestClass = testClass; currentTestClass != null;currentTestClass = getEnclosingClassForNonStaticMemberClass(currentTestClass)) {RunWith annotation = currentTestClass.getAnnotation(RunWith.class);if (annotation != null) {return buildRunner(annotation.value(), testClass);}
}

可以看到,是从内到外层层查找的形式。注意这里没有去查找父类的 RunWith 注解,这是由于 RunWith 注解本身被 @Inherited 所标注,调用 Java 提供的基础方法获取类的注解时已经做了相应的处理。

Spring

@SpringBootApplication

SpringBootApplication 可以说是最好的解释 Spring 中重度使用组合注解的例子了。对于这一注解的解析,我们甚至不需要或者说不能列举出任何解析代码,因为 SpringBootApplication 从来没有作为它自己被解析。该注解的定义如下

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {@AliasFor(annotation = EnableAutoConfiguration.class)Class<?>[] exclude() default {};@AliasFor(annotation = EnableAutoConfiguration.class)String[] excludeName() default {};@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")String[] scanBasePackages() default {};@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")Class<?>[] scanBasePackageClasses() default {};@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;@AliasFor(annotation = Configuration.class)boolean proxyBeanMethods() default true;
}

这里有两件事情值得关注,分别对应介绍 Spring 的注解解析框架的时候指出的 Spring 的两个关键的增强

  1. 实际使用 SpringBootApplication 时,Spring 框架的解析代码是通过 findAnnotation 查找其组合的注解来实现具体功能的。
  2. SpringBootApplication 通过 AliasFor 支持用户在使用该注解时覆盖其所组合的注解的属性。

从这里我们也看出,组合注解仅仅是一种形式上相关联的组合,与任一形式的继承不同,不会以某种形式继承属性。

@Autowired

Autowired 可以说是 Spring 框架中使用最为广泛的注解之一了,它和 Value 注解以及 JSR-330 的 Inject 注解一起组成了注入 Bean 的核心手段。

Autowired 的处理逻辑在 AutowiredAnnotationBeanPostProcessor 中,即 Bean 被创造和加载之后的一个后处理逻辑或者成为装饰逻辑。其中涉及到 Autowired 等注解的地方主要是筛选出需要为目标注入 Bean 的候选。

首先,在初始化的时候,会将对应的 Autowired 系列注解保存到 autowiredAnnotationTypes 集合字段中。

随后,当 Bean 处理框架调用后处理逻辑时,调用后处理器的 findAutowiringMetadata 方法,通过标记型注解找到需要 Autowired 的候选。整个过程通过反射将被 Autowired 注解的对象及 Autowired 注解中持有的是否必须( required )的信息保存到 InjectElement 中。

再之后,对获取到的所有 InjectElement 调用 inject 方法进行注入。根据不同的被注入对象,注入的逻辑有所不同。例如,对于字段的注入,由 AutowiredFieldElement 对象处理,从 BeanFactory 中根据依赖关系初始化 Bean 并将 Bean 赋值给字段。

这一套逻辑支持了 Bean 注入最常用的字段注入的功能,以及运行配置方法的功能。

Autowired 注解还能被用在参数和构造函数上,其中参数上的标注目前仅用于在 JUnit Jupiter 框架测试时使用,而构造函数的标注广泛替代了直接标注字段的用法,其代码路径存在于 AbstractAutowireCapableBeanFactory 创建 Bean 实例的时候。从结果来说,标注在构造函数的 Autowired 能够将参数对应类型的 Bean 作为构造函数的实参,调用构造函数以构造出对象。

如何获取注解中的值_如何在运行时利用注解信息相关推荐

  1. js获取request中的值_基于node.js的开发框架 — Koa

    一.简介 Koa 基于nodeJs平台的下一代web开发框架,由 Express 幕后的原班人马打造,致力于成为一个更小.更富有表现力.更健壮的 Web 框架.使用 koa 编写 web 应用,通过组 ...

  2. vue获取div中的值_一篇文章看懂Vue.js的11种传值通信方式

    面试的时候,也算是常考的一道题目了,而且,在日常的开发中,对于组件的封装,尤其是在 ui组件库中,会用到很多,下面,就来详细的了解下,通过这篇文章的学习,可以提升项目中组件封装的灵活性,可维护性,话不 ...

  3. vue中获取输入框中得到值_如何获取vue input的值

    登陆 //var modal = weex.requireModule('modal'); var modal = weex.requireModule('modal'); var stream = ...

  4. pandas获取dataframe中索引值最大值所在的数据行(get dataframe row of max index value)

    pandas获取dataframe中索引值最大值所在的数据行(get dataframe row of max index value) 目录 pandas获取dataframe中索引值最大值所在的数 ...

  5. 获取数组中元素值为偶数的累加和与元素值为奇数的累加和,并计算他们之间的差值

    /*** 1.获取数组中元素值为偶数的累加和与元素值为奇数的累加和,并计算他们之间的差值* 1.定义int getNum(int[] arr)静态方法,该方法要求完成* 1.1 获取指定数组arr中元 ...

  6. php获取数组中的全部可以吗,php获取数组中所有值的方法

    php的数组操作函数array_values 可以提取一个数组中所有元素值,具体的使用方法,可以参考下面的教程. array_values() 函数的作用是返回数组中所有元素的值,使用起来非常简单,只 ...

  7. Jquery获取列表中的值和input单选、多选框控制选中与取消

    一.Jquery获取列表中的值 1.jsp页面代码 <tbody><c:forEach var="model" items="${listRefEnti ...

  8. selenium+java:获取列表中的值

    selenium+java:获取列表中的值 (2011-08-23 17:14:48) 标签: 杂谈 分类: selenium 初步研究利用java+testNg框架下写selenium测试用例,今天 ...

  9. html使用thymeleaf模板时,获取数据库中字符串值,拆分为list根据下标获取对应的值的方法

    1. 需求 html使用thymeleaf模板时,获取数据库中字符串值,拆分为list根据下标获取对应的值的方法 2. 方法 2.1 参考官网:https://www.thymeleaf.org/do ...

最新文章

  1. 第88天:HTML5中使用classList操作css类
  2. 运维基础(1)Nginx
  3. pyperclip模块
  4. 最长回文子串的不同解法
  5. 自然语言处理工具pyhanlp分词与词性标注
  6. 如何在Eclipse里显示BPMN格式的流程图
  7. 2012年我读过的十本好书
  8. java mysql nullpointerexception_无法从Java连接到MySQL:MySQL驱动程序连接逻辑中的NullPointerException...
  9. linux系统in命令,Linux中的In命令
  10. 硬件基础知识----(20)KVM 深入理解
  11. 苹果阻止《堡垒之夜:拯救世界》Mac版更新
  12. Lorenz混沌系统建模与电路仿真实现
  13. 【matlab笔记】寻找极小值
  14. 数据可视化的目的 ECharts的基本使用步骤
  15. 开始学习英语的七个步骤。
  16. 我说MySQL联合索引遵循最左前缀匹配原则,面试官让我回去等通知
  17. ​黑白照片怎么上色?黑白照片变彩色方法分享
  18. linux 修改hosts
  19. python怎么换背景颜色_用opencv给图片换背景色的示例代码
  20. Docker02 狂神Docker视频学习笔记 :【狂神说Java 哔哩哔哩】Docker最新超详细版教程通俗易懂

热门文章

  1. android 记录路线轨迹_基于百度地图SDK记录运动轨迹
  2. AUTOSAR从入门到精通100讲(131)-AURIX中DMA模块对TIM的FIFO数据搬运
  3. 少儿编程150讲轻松学Scratch(十一)-用Scratch巧解数学题——判定质数
  4. 算力云服务器是干啥的,云服务器将成趋势计算力和安全性是考验
  5. mongodb java id 查询数据_java 用 _id 查找 MongoDB 下的数据
  6. c语言程序设计徐立辉答案,C语言程序设计 牛志成,徐立辉,刘冬莉著 清华大学出版社 9787302165620...
  7. React 父组件和子组件中的方法相互调用
  8. vue 开发过程中遇到的问题
  9. 自适应宽度元素单行文本省略用法探究
  10. JS中的数据类型转换