Spring 注解(二)注解工具类

摘要:
类别<仅一级元注释类<在此处找到;annotatedElement=annotation.annotationType();try{AmetaAnn=annotatedElement.getAnnotation(annotationType);returnnull;

本文转载自Spring 注解(二)注解工具类

导语

首先回顾一下 AnnotationUtilsAnnotatedElementUtils 这两个注解工具类的用法:

@Test
@GetMapping(value = "/GetMapping", consumes = MediaType.APPLICATION_JSON_VALUE)
public void test() throws NoSuchMethodException {
    Method method = ReflectUtils.findDeclaredMethod(
            AliasForTest.class, "test", null);

    // AnnotationUtils 不支持注解属性覆盖
    RequestMapping requestMappingAnn1 = AnnotationUtils.getAnnotation(method, RequestMapping.class);
    Assert.assertEquals(new String[]{}, requestMappingAnn1.value());
    Assert.assertEquals(new String[]{}, requestMappingAnn1.consumes());

    // AnnotatedElementUtils 支持注解属性覆盖
    RequestMapping requestMappingAnn2 = AnnotatedElementUtils.getMergedAnnotation(method, RequestMapping.class);
    Assert.assertEquals(new String[]{"/GetMapping"}, requestMappingAnn2.value());
    Assert.assertEquals(new String[]{MediaType.APPLICATION_JSON_VALUE}, requestMappingAnn2.consumes());
}
AnnotationUtils 源码分析

AnnotationUtils 解决注解别名,包括显式别名、隐式别名、传递的隐式别名,还可以查的指定注解的属性信息。

AnnotationUtils 底层使用动态代理的方式处理注解别名的问题。

get* 系列注解查找

get 遵循 JDK 的注解查找语义,只是增加了一级元注解的查找。

public static <A extends Annotation> A getAnnotation(Annotation annotation, Class<A> annotationType) {
    // 1. 直接查找本地注解
    if (annotationType.isInstance(annotation)) {
        return synthesizeAnnotation((A) annotation);
    }
    // 2. 元注解上查找,注意相对于 find* 而言,这里只查找一级元注解
    Class<? extends Annotation> annotatedElement = annotation.annotationType();
    try {
        A metaAnn = annotatedElement.getAnnotation(annotationType);
        return (metaAnn != null ? synthesizeAnnotation(metaAnn, annotatedElement) : null);
    }
    catch (Throwable ex) {
        handleIntrospectionFailure(annotatedElement, ex);
        return null;
    }
}

find* 系列注解查找

遵循 JDK 的注解查找语义,只是增加了多级元注解的查找。

// visited 表示已经查找的元素,Spring 的递归很多都用到了这个参数
private static <A extends Annotation> A findAnnotation(
        AnnotatedElement annotatedElement, Class<A> annotationType, Set<Annotation> visited) {
    try {
        // 1. 本地注解查找
        A annotation = annotatedElement.getDeclaredAnnotation(annotationType);
        if (annotation != null) {
            return annotation;
        }
        // 2. 元注解上查找
        for (Annotation declaredAnn : getDeclaredAnnotations(annotatedElement)) {
            Class<? extends Annotation> declaredType = declaredAnn.annotationType();
            if (!isInJavaLangAnnotationPackage(declaredType) && visited.add(declaredAnn)) {
                // 3. 元注解上递归查找
                annotation = findAnnotation((AnnotatedElement) declaredType, annotationType, visited);
                if (annotation != null) {
                    return annotation;
                }
            }
        }
    }
    catch (Throwable ex) {
        handleIntrospectionFailure(annotatedElement, ex);
    }
    return null;
}

synthesizeAnnotation 动态代理解决别名问题

static <A extends Annotation> A synthesizeAnnotation(A annotation, @Nullable Object annotatedElement) {
    // 1. SynthesizedAnnotation 为一个标记,表示已经动态代理过了
    //    hasPlainJavaAnnotationsOnly 如果是 java 中的注解不可能有注解别名,直接返回
    if (annotation instanceof SynthesizedAnnotation || hasPlainJavaAnnotationsOnly(annotatedElement)) {
        return annotation;
    }

    // 2. 判断是否需要进行动态代理,即注解中存在别名,包括显示别名、隐式别名、传递的隐式别名
    Class<? extends Annotation> annotationType = annotation.annotationType();
    if (!isSynthesizable(annotationType)) {
        return annotation;
    }

    // 3. AnnotationAttributeExtractor 用于从注解 annotation 中提取属性的值
    DefaultAnnotationAttributeExtractor attributeExtractor =
            new DefaultAnnotationAttributeExtractor(annotation, annotatedElement);
    // 4. SynthesizedAnnotationInvocationHandler 动态代理的类
    InvocationHandler handler = new SynthesizedAnnotationInvocationHandler(attributeExtractor);

    // 5. 接口中有 SynthesizedAnnotation,并返回动态代理的对象
    Class<?>[] exposedInterfaces = new Class<?>[] {annotationType, SynthesizedAnnotation.class};
    return (A) Proxy.newProxyInstance(annotation.getClass().getClassLoader(), exposedInterfaces, handler);
}

SynthesizedAnnotationInvocationHandler

下面主要看一下动态代理的 invoke 实现是怎么实现的。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (ReflectionUtils.isEqualsMethod(method)) {
        return annotationEquals(args[0]);
    }
    if (ReflectionUtils.isHashCodeMethod(method)) {
        return annotationHashCode();
    }
    if (ReflectionUtils.isToStringMethod(method)) {
        return annotationToString();
    }
    // 注解的 annotationType 返回注解的 Class 类型
    if (AnnotationUtils.isAnnotationTypeMethod(method)) {
        return annotationType();
    }
    if (!AnnotationUtils.isAttributeMethod(method)) {
        throw new AnnotationConfigurationException(String.format(
                "Method [%s] is unsupported for synthesized annotation type [%s]", method, annotationType()));
    }
    // 真正获取注解的属性值
    return getAttributeValue(method);
}

getAttributeValue 的核心其实就一句话 this.attributeExtractor.getAttributeValue(attributeMethod); 委托给了对应的 AnnotationAttributeExtractor 处理。

AnnotationAttributeExtractor

AnnotationAttributeExtractor 类图

AbstractAliasAwareAnnotationAttributeExtractor(
            Class<? extends Annotation> annotationType, @Nullable Object annotatedElement, S source) {

    Assert.notNull(annotationType, "annotationType must not be null");
    Assert.notNull(source, "source must not be null");
    this.annotationType = annotationType;
    this.annotatedElement = annotatedElement;
    this.source = source;
    this.attributeAliasMap = AnnotationUtils.getAttributeAliasMap(annotationType);
}

在构造方法中有一个很重要的方法 AnnotationUtils.getAttributeAliasMap(annotationType) 用于获取其别名。

public final Object getAttributeValue(Method attributeMethod) {
    String attributeName = attributeMethod.getName();
    // attributeValue 表示属性的真实值
    Object attributeValue = getRawAttributeValue(attributeMethod);

    // 获取所有的别名
    List<String> aliasNames = this.attributeAliasMap.get(attributeName);
    if (aliasNames != null) {
        // 属性的默认值,默认值肯定是一样的,因为在获取别名的时候已经校验了默认值
        Object defaultValue = AnnotationUtils.getDefaultValue(this.annotationType, attributeName);
        for (String aliasName : aliasNames) {
            // 别名的真实值
            Object aliasValue = getRawAttributeValue(aliasName);
            // 如果两个别名的值不相等,且都不等于默认值,直接抛异常
            if (!ObjectUtils.nullSafeEquals(attributeValue, aliasValue) &&
                    !ObjectUtils.nullSafeEquals(attributeValue, defaultValue) &&
                    !ObjectUtils.nullSafeEquals(aliasValue, defaultValue)) {
                throw new AnnotationConfigurationException();
            }
            if (ObjectUtils.nullSafeEquals(attributeValue, defaultValue)) {
                attributeValue = aliasValue;
            }
        }
    }
    return attributeValue;
}

AliasDescriptor

getAttributeAliasMap

在 AbstractAliasAwareAnnotationAttributeExtractor 的构造器中有一个很重要的方法 getAttributeAliasMap 获取注解中所有属性的别名。

static Map<String, List<String>> getAttributeAliasMap(@Nullable Class<? extends Annotation> annotationType) {
    map = new LinkedHashMap<>();
    for (Method attribute : getAttributeMethods(annotationType)) {
        List<String> aliasNames = getAttributeAliasNames(attribute);
        if (!aliasNames.isEmpty()) {
            map.put(attribute.getName(), aliasNames);
        }
    }
    return map;
}

static List<String> getAttributeAliasNames(Method attribute) {
    AliasDescriptor descriptor = AliasDescriptor.from(attribute);
    return (descriptor != null ? descriptor.getAttributeAliasNames() : Collections.emptyList());
}

可以别名获取的所有的工作都是委托给了 AliasDescriptor 完成,这一小节我们就主要看一下这个类。

AliasDescriptor 构造及校验

public static AliasDescriptor from(Method attribute) {
    AliasFor aliasFor = attribute.getAnnotation(AliasFor.class);
    if (aliasFor == null) {
        return null;
    }
    descriptor = new AliasDescriptor(attribute, aliasFor);
    descriptor.validate();
    return descriptor;
}

构建一个 AliasDescriptor 分为两步:一是获取注解信息(构造器),二是校验别名是否成立(validate)。@AliasFor 有以下的规约:

  • 规约1:显示别名可以不用配置 annotation 属性
  • 规约2:隐式别名默认和原注解属性名称一致,getAliasedAttributeName 中体现
  • 规约3:隐式别名 @AliasFor 配置的注解必须出现在元注解中,可以是多级元注解
  • 规约4:显示别名必须成对配置
  • 规约5:别名必须配置默认值,且默认值一致。注意别名可以为数组类型,而原属性为数组的元素类型
private AliasDescriptor(Method sourceAttribute, AliasFor aliasFor) {
        Class<?> declaringClass = sourceAttribute.getDeclaringClass();
    // 1. 注解原字段的信息
    this.sourceAttribute = sourceAttribute;
    this.sourceAnnotationType = (Class<? extends Annotation>) declaringClass;
    this.sourceAttributeName = sourceAttribute.getName();

    // 2. @AliasFor 注解的信息
    // 规约1:显示的别名可以不用配置 annotation 属性
    // 规约2:隐式别名默认和原注解属性名称一致,getAliasedAttributeName 中体现
    this.aliasedAnnotationType = (Annotation.class == aliasFor.annotation() ?
            this.sourceAnnotationType : aliasFor.annotation());
    this.aliasedAttributeName = getAliasedAttributeName(aliasFor, sourceAttribute);
    if (this.aliasedAnnotationType == this.sourceAnnotationType &&
            this.aliasedAttributeName.equals(this.sourceAttributeName)) {
        throw new AnnotationConfigurationException(...);
    }
    try {
        // @AliasFor 配置的别名不存在直接抛出异常
        this.aliasedAttribute = this.aliasedAnnotationType.getDeclaredMethod(this.aliasedAttributeName);
    } catch (NoSuchMethodException ex) {
        throw new AnnotationConfigurationException(..., ex);
    }
    // 3. isAliasPair=true 表示就同一个注解内的显示别名
    this.isAliasPair = (this.sourceAnnotationType == this.aliasedAnnotationType);
}

getAttributeAliasNames 获取别名

public List<String> getAttributeAliasNames() {
    // 1. 显示别名,直接返回
    if (this.isAliasPair) {
        return Collections.singletonList(this.aliasedAttributeName);
    }

    // 2. 隐式别名,包括可传递的隐式别名
    List<String> aliases = new ArrayList<>();
    // 2.1 遍历注解中的其它属性,一一判断是否互为别名
    //     getOtherDescriptors 获取其它的所有属性
    //     isAliasFor 判断两个属性是否互为别名,会递归向上查找
    for (AliasDescriptor otherDescriptor : getOtherDescriptors()) {
        if (this.isAliasFor(otherDescriptor)) {
            this.validateAgainst(otherDescriptor);
            aliases.add(otherDescriptor.sourceAttributeName);
        }
    }
    return aliases;
}

getAttributeOverrideName 获取当前属性在元注解中对应的别名

public String getAttributeOverrideName(Class<? extends Annotation> metaAnnotationType) {
    // 递归向上查找别名,如果 sourceAnnotationType==metaAnnotationType 则查找到了
    for (AliasDescriptor desc = this; desc != null; desc = desc.getAttributeOverrideDescriptor()) {
        if (desc.isOverrideFor(metaAnnotationType)) {
            return desc.aliasedAttributeName;
        }
    }
    return null;
}
AnnotatedElementUtils 源码分析

Processor 对匹配的注解进行后置处理

Processor 对匹配的注解进行后置处理,可以通过 process 方法的返回值来控制查找的流程:返回 null 时继续查找,非 null 时直接返回。有一种情况例外就是 aggregates=true,这种情况要查找所有的注解,所以会继续查找。

接口

private interface Processor<T> {
    // 两个作用:一是根据返回值是否为 null 控制查询的流程;二是对查询的注解进行处理,主要是用于获取该注解的属性值
    T process(@Nullable AnnotatedElement annotatedElement, Annotation annotation, int metaDepth);

    // 只有 MergedAnnotationAttributesProcessor 有效,用于处理元注解属性覆盖
    // annotation 为当前注解,result 为元注解属性信息,annotation 会覆盖元注解中的属性信息
    void postProcess(@Nullable AnnotatedElement annotatedElement, Annotation annotation, T result);

    // 查询所有元注解时有效,不管是否匹配都要执行 process 方法
    boolean alwaysProcesses();

    // MergedAnnotationAttributesProcessor 查找所有的注解有效
    boolean aggregates();
    List<T> getAggregatedResults();
}

有两个方法要特别关注:

  • process(@Nullable AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) 有两个作用:一是根据返回值来控制查找的流程;二是 MergedAnnotationAttributesProcessor 的 process 方法返回查找到的注解信息 AnnotationAttributes
  • postProcess(@Nullable AnnotatedElement element, Annotation annotation, AnnotationAttributes attributes) 只有 MergedAnnotationAttributesProcessor 有效,用来处理元注解属性覆盖。其中 annotation 表示当前的注解,attributes 表示元注解的属性信息,执行时会用 annotation 覆盖 attributes。

类图

Processor 类图

Processor 的有几个实现:SimpleAnnotationProcessor 相当于一个简单的适配器;AlwaysTrueBooleanAnnotationProcessor 的 process 方法永远返回 TRUE;MergedAnnotationAttributesProcessor 用于处理元注解属性覆盖。

常用的方法对应的 Processor 返回值如下:

  • getMetaAnnotationTypes 获取指定注解上的所有元注解,所以 process 方法返回 null 且 alwaysProcesses=true
  • hasMetaAnnotationTypes 判断指定的注解上是否有元注解,所以 process 方法返回 metaDepth > 0 ? Boolean.TRUE : CONTINUE,即当 metaDepth>0 表示有元注解就停止查询
  • isAnnotated 是否存在指定的注解,所以只配匹配到 process 方法就返回 TRUE,使用 AlwaysTrueBooleanAnnotationProcessor
  • getMergedAnnotationAttributes 元注解会进行属性覆盖,使用 MergedAnnotationAttributesProcessor
  • getAllMergedAnnotations 查找所有的注解,使用 MergedAnnotationAttributesProcessor 且 aggregates=true

MergedAnnotationAttributesProcessor

// AnnotationUtils#retrieveAnnotationAttributes 方法获取当前注解的属性
public AnnotationAttributes process(@Nullable AnnotatedElement annotatedElement, Annotation annotation, int metaDepth) {
    return AnnotationUtils.retrieveAnnotationAttributes(annotatedElement, annotation,
            this.classValuesAsString, this.nestedAnnotationsAsMap);
}

// annotation 为当前注解,result 为元注解属性信息,这个元注解的属性信息是 process 方法提取的
// annotation 会覆盖元注解中的属性信息
public void postProcess(@Nullable AnnotatedElement element, Annotation annotation, AnnotationAttributes attributes) {
    annotation = AnnotationUtils.synthesizeAnnotation(annotation, element);
    Class<? extends Annotation> targetAnnotationType = attributes.annotationType();

    // 1. 已经解析过的属性,避免循环查找
    Set<String> valuesAlreadyReplaced = new HashSet<>();

    for (Method attributeMethod : AnnotationUtils.getAttributeMethods(annotation.annotationType())) {
        String attributeName = attributeMethod.getName();
        // 2. 查找 attributeMethod 属性到底覆盖了 targetAnnotationType 元注解的那个属性
        String attributeOverrideName = AnnotationUtils.getAttributeOverrideName(attributeMethod, targetAnnotationType);

        // 3 显示进行属性覆盖,通过 @AliasFor 注解
        if (attributeOverrideName != null) {
            if (valuesAlreadyReplaced.contains(attributeOverrideName)) {
                continue;
            }

            List<String> targetAttributeNames = new ArrayList<>();
            targetAttributeNames.add(attributeOverrideName);
            valuesAlreadyReplaced.add(attributeOverrideName);

            // 确保所有的别名都要进行属性覆盖 (SPR-14069)
            List<String> aliases = AnnotationUtils.getAttributeAliasMap(targetAnnotationType).get(attributeOverrideName);
            if (aliases != null) {
                for (String alias : aliases) {
                    if (!valuesAlreadyReplaced.contains(alias)) {
                        targetAttributeNames.add(alias);
                        valuesAlreadyReplaced.add(alias);
                    }
                }
            }
            // 将 targetAttributeNames 的属性值设置为 attributeName 的值
            overrideAttributes(element, annotation, attributes, attributeName, targetAttributeNames);
        }
        // 3.2 隐式的进行属性覆盖,只要字段与元注解的属性字段一下致(规约)
        else if (!AnnotationUtils.VALUE.equals(attributeName) && attributes.containsKey(attributeName)) {
            overrideAttribute(element, annotation, attributes, attributeName, attributeName);
        }
    }
}

searchWithGetSemantics

searchWithGetSemantics 有 7 个参数:

  • element 注解标注的 AnnotatedElement
  • annotationTypes、annotationName、containerType 分别表示要查找的注解类型、注解名称、以及可重复注解的容器对象
  • processor 后置的处理器,process 返回 null 继续查找,否则停止查找。aggregates=true 时例外,因为此时查找全部的注解。
  • visited 已经查找的元素,避免重复查找。
  • metaDepth 注解深度,普通注解为 0
// 用于查找 element 上的 annotationTypes、annotationName、containerType 类型注解
// 返回后置处理器对查找后的注解 process 后的值
private static <T> T searchWithGetSemantics(AnnotatedElement element,
        Set<Class<? extends Annotation>> annotationTypes, @Nullable String annotationName,
        @Nullable Class<? extends Annotation> containerType, Processor<T> processor,
        Set<AnnotatedElement> visited, int metaDepth) {

    if (visited.add(element)) {
        try {
            // 1. 本地注解查找 Start searching within locally declared annotations
            List<Annotation> declaredAnnotations = Arrays.asList(AnnotationUtils.getDeclaredAnnotations(element));
            T result = searchWithGetSemanticsInAnnotations(element, declaredAnnotations,
                    annotationTypes, annotationName, containerType, processor, visited, metaDepth);
            if (result != null) {
                return result;
            }

            // 2. @Inherited 类型查找
            if (element instanceof Class) {  // otherwise getAnnotations doesn't return anything new
                Class<?> superclass = ((Class<?>) element).getSuperclass();
                if (superclass != null && superclass != Object.class) {
                    List<Annotation> inheritedAnnotations = new LinkedList<>();
                    for (Annotation annotation : element.getAnnotations()) {
                        if (!declaredAnnotations.contains(annotation)) {
                            inheritedAnnotations.add(annotation);
                        }
                    }
                    // Continue searching within inherited annotations
                    result = searchWithGetSemanticsInAnnotations(element, inheritedAnnotations,
                            annotationTypes, annotationName, containerType, processor, visited, metaDepth);
                    if (result != null) {
                        return result;
                    }
                }
            }
        }
        catch (Throwable ex) {
            AnnotationUtils.handleIntrospectionFailure(element, ex);
        }
    }

    return null;
}

searchWithGetSemanticsInAnnotations 真正用于在指定的注解集合 annotations 中查找指定的注解。

private static <T> T searchWithGetSemanticsInAnnotations(@Nullable AnnotatedElement element,
        List<Annotation> annotations, Set<Class<? extends Annotation>> annotationTypes,
        @Nullable String annotationName, @Nullable Class<? extends Annotation> containerType,
        Processor<T> processor, Set<AnnotatedElement> visited, int metaDepth) {

    // 1. 直接匹配 Search in annotations
    for (Annotation annotation : annotations) {
        Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
        if (!AnnotationUtils.isInJavaLangAnnotationPackage(currentAnnotationType)) {
            // 1.1 注解类型或注解名相同
            if (annotationTypes.contains(currentAnnotationType) ||
                    currentAnnotationType.getName().equals(annotationName) ||
                    processor.alwaysProcesses()) {
                T result = processor.process(element, annotation, metaDepth);
                if (result != null) {
                    if (processor.aggregates() && metaDepth == 0) {
                        processor.getAggregatedResults().add(result);
                    }
                    else {
                        return result;
                    }
                }
            }
            // 1.2 可重复注解,注意可重复注解不可能是组合注解 Repeatable annotations in container?
            else if (currentAnnotationType == containerType) {
                for (Annotation contained : getRawAnnotationsFromContainer(element, annotation)) {
                    T result = processor.process(element, contained, metaDepth);
                    if (result != null) {
                        processor.getAggregatedResults().add(result);
                    }
                }
            }
        }
    }

    // 2. 递归查找元注解  Recursively search in meta-annotations
    for (Annotation annotation : annotations) {
        Class<? extends Annotation> currentAnnotationType = annotation.annotationType();
        if (!AnnotationUtils.hasPlainJavaAnnotationsOnly(currentAnnotationType)) {
            T result = searchWithGetSemantics(currentAnnotationType, annotationTypes,
                    annotationName, containerType, processor, visited, metaDepth + 1);
            if (result != null) {
                // MergedAnnotationAttributesProcessor 用于元注解属性覆盖
                // annotation 表示当前的注解,attributes 表示元注解的属性信息,annotation 会覆盖 attributes。
                processor.postProcess(element, annotation, result);
                if (processor.aggregates() && metaDepth == 0) {
                    processor.getAggregatedResults().add(result);
                } else {
                    return result;
                }
            }
        }
    }
    return null;
}
参考
  1. 《spring注解工具类AnnotatedElementUtils和AnnotationUtils》:https://blog.csdn.net/qq_22845447/article/details/83210559

免责声明:文章转载自《Spring 注解(二)注解工具类》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇在HTML页面加载完毕后运行某个jsspringboot-quartz 实现动态添加,修改,删除,暂停,恢复等功能下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

CSS中如何把Span标签设置为固定宽度

一、形如<span>ABC</span>独立行设置SPAN为固定宽度方法如下: span {60px; text-align:center; display:block; } 实际验证结果:IE6 OK, FIREFOX 3 OK。 一、形如<span>ABC</span>DEF格式行设置SPAN为固定宽度的...

如何解决读取到文件末尾时碰到EOF导致的重复输出或者无用输出

当读取到文件末尾时,会碰到EOF,如何解决呢?    方法一:我们可以通过(ch=fin.get())!=EOF来结束读取,这样就不会像eof()那样碰到EOF之后,还会再进行一次读取,导致输出一个无用的结束符或者重复前一字符 //a.txt中的内容为abc #include <iostream> #include <fstream&g...

css中div透明度有几种方法设置?

css设置div背景透明有两种方法:第一种使用opacity:0~1,这个方法有个缺点,就是内容也会跟着透明;第二种方法就是rgba(0,0,0,0~1),使用这个方法就只会设置div背景透明,而不会影响到div里的内容。 一、css rgba()设置颜色透明度 语法: rgba(R,G,B,A); RGBA 是代表Red(红色) Green(绿色) Bl...

纯css实现鼠标感应弹出二级菜单

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><...

显式等待-----Selenium快速入门(十)

  上一篇说了元素定位过程中的隐式等待,今天我们来探讨一下显示等待。显式等待,其实就是在使用WebDriverWait这个对象,进行等待。显式等待对比隐式等待,多了一些人性化的设置,可以说是更细化的隐式等待。   WebDriverWait 类继承自泛型类 FluentWait<T> ,而这个泛型类,又是泛型接口 Wait<T> 的...

对文件内容就行修改-java代码

在实际的生产中,会遇到数据库版本的不同,sql语句的语法会出现改变, 我在这次的金仓数据库中就遇到了,V8版本的建表语句跟V7版本的不一样,所以需要对里面内容进行修改  从图中我们可以看到到,这个是金仓数据库v8版本导出的建表语句,这个语句在v7版本是不能运行的 我们需要做的就是 在这分享一小段java代码,本人是个菜鸟,大家看到写得不好也请见谅! 首...