转载

深入理解 Java 注解

本文内容基于 JDK8。注解是 JDK5 引入的,后续 JDK 版本扩展了一些内容,本文中没有明确指明版本的注解都是 JDK5 就已经支持的注解。

:notebook: 本文已归档到:「 blog 」

:keyboard: 本文中的示例代码已归档到:「 javacore 」

简介

注解的形式

Java 中,注解是以 @ 字符开始的修饰符。如下:

@Override
void mySuperMethod() { ... }
复制代码

注解可以包含命名或未命名的属性,并且这些属性有值。

@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass() { ... }
复制代码

如果只有一个名为 value 的属性,那么名称可以省略,如:

@SuppressWarnings("unchecked")
void myMethod() { ... }
复制代码

如果注解没有属性,则称为 标记注解 。如: @Override

什么是注解

从本质上来说, 注解是一种标签,其实质上可以视为一种特殊的注释,如果没有解析它的代码,它并不比普通注释强。

解析一个注解往往有两种形式:

  • 编译期直接的扫描 - 编译器的扫描指的是编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。这种情况只适用于 JDK 内置的注解类。
  • 运行期的反射 - 如果要自定义注解,Java 编译器无法识别并处理这个注解,它只能根据该注解的作用范围来选择是否编译进字节码文件。如果要处理注解,必须利用反射技术,识别该注解以及它所携带的信息,然后做相应的处理。

注解的作用

注解有许多用途:

  • 编译器信息 - 编译器可以使用注解来检测错误或抑制警告。
  • 编译时和部署时的处理 - 程序可以处理注解信息以生成代码,XML 文件等。
  • 运行时处理 - 可以在运行时检查某些注解并处理。

作为 Java 程序员,多多少少都曾经历过被各种配置文件(xml、properties)支配的恐惧。过多的配置文件会使得项目难以维护。个人认为,使用注解以减少配置文件或代码,是注解最大的用处。

注解的代价

凡事有得必有失,注解技术同样如此。使用注解也有一定的代价:

  • 显然,它是一种侵入式编程,那么,自然就存在着增加程序耦合度的问题。
  • 自定义注解的处理需要在运行时,通过反射技术来获取属性。如果注解所修饰的元素是类的非 public 成员,也可以通过反射获取。这就违背了面向对象的封装性。
  • 注解所产生的问题,相对而言,更难以 debug 或定位。

但是,正所谓瑕不掩瑜,注解所付出的代价,相较于它提供的功能而言,还是可以接受的。

注解的应用范围

注解可以应用于类、字段、方法和其他程序元素的声明。

JDK8 开始,注解的应用范围进一步扩大,以下是新的应用范围:

类实例初始化表达式:

new @Interned MyObject();
复制代码

类型转换:

myString = (@NonNull String) str;
复制代码

实现接口的声明:

class UnmodifiableList<T> implements
    @Readonly List<@Readonly T> {}
复制代码

抛出异常声明:

void monitorTemperature()
    throws @Critical TemperatureException {}
复制代码

内置注解

JDK 中内置了以下注解:

@Override
@Deprecated
@SuppressWarnnings
@SafeVarargs
@FunctionalInterface

@Override

@Override 用于表明被修饰方法覆写了父类的方法。

如果试图使用 @Override 标记一个实际上并没有覆写父类的方法时,java 编译器会告警。

@Override 示例:

public class OverrideAnnotationDemo {

    static class Person {
        public String getName() {
            return "getName";
        }
    }


    static class Man extends Person {
        @Override
        public String getName() {
            return "override getName";
        }

        /**
         *  放开下面的注释,编译时会告警
         */
       /*
        @Override
        public String getName2() {
            return "override getName2";
        }
        */
    }

    public static void main(String[] args) {
        Person per = new Man();
        System.out.println(per.getName());
    }
}
复制代码

@Deprecated

@Deprecated 用于标明被修饰的类或类成员、类方法已经废弃、过时,不建议使用。

@Deprecated 有一定的 延续性 :如果我们在代码中通过继承或者覆盖的方式使用了过时的类或类成员,即使子类或子方法没有标记为 @Deprecated ,但编译器仍然会告警。

注意: @Deprecated 这个注解类型和 javadoc 中的 @deprecated 这个 tag 是有区别的:前者是 java 编译器识别的;而后者是被 javadoc 工具所识别用来生成文档(包含程序成员为什么已经过时、它应当如何被禁止或者替代的描述)。

@Deprecated 示例:

public class DeprecatedAnnotationDemo {
    static class DeprecatedField {
        @Deprecated
        public static final String DEPRECATED_FIELD = "DeprecatedField";
    }


    static class DeprecatedMethod {
        @Deprecated
        public String print() {
            return "DeprecatedMethod";
        }
    }


    @Deprecated
    static class DeprecatedClass {
        public String print() {
            return "DeprecatedClass";
        }
    }

    public static void main(String[] args) {
        System.out.println(DeprecatedField.DEPRECATED_FIELD);

        DeprecatedMethod dm = new DeprecatedMethod();
        System.out.println(dm.print());


        DeprecatedClass dc = new DeprecatedClass();
        System.out.println(dc.print());
    }
}
//Output:
//DeprecatedField
//DeprecatedMethod
//DeprecatedClass
复制代码

@SuppressWarnnings

@SuppressWarnings 用于关闭对类、方法、成员编译时产生的特定警告。

@SuppressWarning 不是一个标记注解。它有一个类型为 String[] 的数组成员,这个数组中存储的是要关闭的告警类型。对于 javac 编译器来讲,对 -Xlint 选项有效的警告名也同样对 @SuppressWarings 有效,同时编译器会忽略掉无法识别的警告名。

@SuppressWarning 示例:

@SuppressWarnings({"rawtypes", "unchecked"})
public class SuppressWarningsAnnotationDemo {
    static class SuppressDemo<T> {
        private T value;

        public T getValue() {
            return this.value;
        }

        public void setValue(T var) {
            this.value = var;
        }
    }

    @SuppressWarnings({"deprecation"})
    public static void main(String[] args) {
        SuppressDemo d = new SuppressDemo();
        d.setValue("南京");
        System.out.println("地名:" + d.getValue());
    }
}
复制代码

@SuppressWarnings 注解的常见参数值的简单说明:

deprecation
unchecked
fallthrough
path
serial
finally
all
@SuppressWarnings({"uncheck", "deprecation"})
public class InternalAnnotationDemo {

    /**
     * @SuppressWarnings 标记消除当前类的告警信息
     */
    @SuppressWarnings({"deprecation"})
    static class A {
        public void method1() {
            System.out.println("call method1");
        }

        /**
         * @Deprecated 标记当前方法为废弃方法,不建议使用
         */
        @Deprecated
        public void method2() {
            System.out.println("call method2");
        }
    }

    /**
     * @Deprecated 标记当前类为废弃类,不建议使用
     */
    @Deprecated
    static class B extends A {
        /**
         * @Override 标记显示指明当前方法覆写了父类或接口的方法
         */
        @Override
        public void method1() { }
    }

    public static void main(String[] args) {
        A obj = new B();
        obj.method1();
        obj.method2();
    }
}
复制代码

@SafeVarargs

@SafeVarargs 在 JDK7 中引入。

@SafeVarargs 的作用是:告诉编译器,在可变长参数中的泛型是类型安全的。可变长参数是使用数组存储的,而数组和泛型不能很好的混合使用。

简单的说,数组元素的数据类型在编译和运行时都是确定的,而泛型的数据类型只有在运行时才能确定下来。因此,当把一个泛型存储到数组中时,编译器在编译阶段无法确认数据类型是否匹配,因此会给出警告信息;即如果泛型的真实数据类型无法和参数数组的类型匹配,会导致 ClassCastException 异常。

@SafeVarargs 注解使用范围:

  • @SafeVarargs 注解可以用于构造方法。
  • @SafeVarargs 注解可以用于 staticfinal 方法。

@SafeVarargs 示例:

public class SafeVarargsAnnotationDemo {
    /**
     * 此方法实际上并不安全,不使用此注解,编译时会告警
     */
    @SafeVarargs
    static void wrongMethod(List<String>... stringLists) {
        Object[] array = stringLists;
        List<Integer> tmpList = Arrays.asList(42);
        array[0] = tmpList; // 语法错误,但是编译不告警
        String s = stringLists[0].get(0); // 运行时报 ClassCastException
    }

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");

        List<String> list2 = new ArrayList<>();
        list.add("1");
        list.add("2");

        wrongMethod(list, list2);
    }
}
复制代码

以上代码,如果不使用 @SafeVarargs ,编译时会告警

[WARNING] /D:/Codes/ZP/Java/javacore/codes/basics/src/main/java/io/github/dunwu/javacore/annotation/SafeVarargsAnnotationDemo.java: 某些输入文件使用了未经检查或不安全的操作。
[WARNING] /D:/Codes/ZP/Java/javacore/codes/basics/src/main/java/io/github/dunwu/javacore/annotation/SafeVarargsAnnotationDemo.java: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
复制代码

@FunctionalInterface

@FunctionalInterface 在 JDK8 引入。

@FunctionalInterface 用于指示被修饰的接口是函数式接口。

需要注意的是,如果一个接口符合"函数式接口"定义,不加 @FunctionalInterface 也没关系;但如果编写的不是函数式接口,却使用 @FunctionInterface ,那么编译器会报错。

什么是函数式接口?

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为 lambda 表达式。

函数式接口的特点:

  • 接口有且只能有个一个抽象方法(抽象方法只有方法定义,没有方法体)。
  • 不能在接口中覆写 Object 类中的 public 方法(写了编译器也会报错)。
  • 允许有 default 实现方法。

示例:

public class FunctionalInterfaceAnnotationDemo {

    @FunctionalInterface
    public interface Func1<T> {
        void printMessage(T message);
    }

    /**
     * @FunctionalInterface 修饰的接口中定义两个抽象方法,编译时会报错
     * @param <T>
     */
    /*@FunctionalInterface
    public interface Func2<T> {
        void printMessage(T message);
        void printMessage2(T message);
    }*/

    public static void main(String[] args) {
        Func1 func1 = message -> System.out.println(message);
        func1.printMessage("Hello");
        func1.printMessage(100);
    }
}
复制代码

元注解

JDK 中虽然内置了几个注解,但这远远不能满足开发过程中遇到的千变万化的需求。所以我们需要自定义注解,而这就需要用到元注解。

元注解的作用就是用于定义其它的注解。

Java 中提供了以下元注解类型:

@Retention
@Target
@Documented
@Inherited
@Repeatable

这些类型和它们所支持的类在 java.lang.annotation 包中可以找到。下面我们看一下每个元注解的作用和相应分参数的使用说明。

@Retention

@Retention 指明了注解的保留级别。

@Retention 源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}
复制代码

RetentionPolicy 是一个枚举类型,它定义了被 @Retention 修饰的注解所支持的保留级别:

RetentionPolicy.SOURCE
RetentionPolicy.CLASS
RetentionPolicy.RUNTIME

@Retention 示例:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    public String name() default "fieldName";
    public String setFuncName() default "setField";
    public String getFuncName() default "getField";
    public boolean defaultDBValue() default false;
}
复制代码

@Documented

@Documented 表示无论何时使用指定的注解,都应使用 Javadoc(默认情况下,注释不包含在 Javadoc 中)。更多内容可以参考:Javadoc tools page。

@Documented 示例:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
    public String name() default "fieldName";
    public String setFuncName() default "setField";
    public String getFuncName() default "getField";
    public boolean defaultDBValue() default false;
}
复制代码

@Target

@Target 指定注解可以修饰的元素类型。

@Target 源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}
复制代码

ElementType 是一个枚举类型,它定义了被 @Target 修饰的注解可以应用的范围:

ElementType.ANNOTATION_TYPE
ElementType.CONSTRUCTOR
ElementType.FIELD
ElementType.LOCAL_VARIABLE
ElementType.METHOD
ElementType.PACKAGE
ElementType.PARAMETER
ElementType.TYPE

@Target 示例:

@Target(ElementType.TYPE)
public @interface Table {
    /**
     * 数据表名称注解,默认值为类名称
     * @return
     */
    public String tableName() default "className";
}

@Target(ElementType.FIELD)
public @interface NoDBColumn {}
复制代码

@Inherited

@Inherited 表示注解类型可以被继承(默认情况下不是这样) 。

表示自动继承注解类型。 如果注解类型声明中存在 @Inherited 元注解,则注解所修饰类的所有子类都将会继承此注解。

注意: @Inherited 注解类型是被标注过的类的子类所继承。类并不从它所实现的接口继承注解,方法并不从它所覆写的方法继承注解。

此外,当 @Inherited 类型标注的注解的 @RetentionRetentionPolicy.RUNTIME ,则反射 API 增强了这种继承性。如果我们使用 java.lang.reflect 去查询一个 @Inherited 类型的注解时,反射代码检查将展开工作:检查类和其父类,直到发现指定的注解类型被发现,或者到达类继承结构的顶层。

@Inherited
public @interface Greeting {
    public enum FontColor{ BULE,RED,GREEN};
    String name();
    FontColor fontColor() default FontColor.GREEN;
}
复制代码

@Repeatable

@Repeatable 表示注解可以重复使用。

以 Spring @Scheduled 为例:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Schedules {
	Scheduled[] value();
}

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
  // ...
}
复制代码

应用示例:

public class TaskRunner {

    @Scheduled("0 0/15 * * * ?")
    @Scheduled("0 0 12 * ?")
    public void task1() {}
}
复制代码

自定义注解

使用 @interface 自定义注解时,自动继承了 java.lang.annotation.Annotation 接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。 @interface 用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过 default 来声明参数的默认值。

这里,我会通过实现一个名为 RegexValid 的正则校验注解工具来展示自定义注解的全步骤。

1. 注解的定义

注解的语法格式如下:

public @interface 注解名 {定义体}
复制代码

我们来定义一个注解:

@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface RegexValid {}
复制代码

说明:

通过上一节对于元注解、、的说明,这里就很容易理解了。

  • 上面的代码中定义了一个名为 @RegexValid 的注解。
  • @Documented 表示 @RegexValid 应该使用 javadoc。
  • @Target({ElementType.FIELD, ElementType.PARAMETER}) 表示 @RegexValid 可以在类成员或方法参数上修饰。
  • @Retention(RetentionPolicy.RUNTIME) 表示 @RegexValid 在运行时有效。

此时,我们已经定义了一个没有任何属性的注解,如果到此为止,它仅仅是一个标记注解。作为正则工具,没有属性可什么也做不了。接下来,我们将为它添加注解属性。

2. 注解属性

注解属性的语法形式如下:

[访问级别修饰符] [数据类型] 名称() default 默认值;
复制代码

例如,我们要定义在注解中定义一个名为 value 的字符串属性,其默认值为空字符串,访问级别为默认级别,那么应该定义如下:

String value() default "";
复制代码

注意:在注解中,我们定义属性时,属性名后面需要加 ()。

定义注解属性有以下要点:

  • 注解属性只能使用 public 或默认访问级别(即不指定访问级别修饰符)修饰。

  • 注解属性的数据类型有限制要求。支持的数据类型如下:

    • 所有基本数据类型(byte、char、short、int、long、float、double、boolean)
    • String 类型
    • Class 类
    • enum 类型
    • Annotation 类型
    • 以上所有类型的数组
  • 注解属性必须有确定的值,建议指定默认值。注解属性只能通过指定默认值或使用注解时指定属性值,相较之下,指定默认值的方式更为可靠。注解属性如果是引用类型,不可以为 null。这个约束使得注解处理器很难判断注解属性是默认值,或是使用注解时所指定的属性值。为此,我们设置默认值时,一般会定义一些特殊的值,例如空字符串或者负数。

  • 如果注解中只有一个属性值,最好将其命名为 value。因为,指定属性名为 value,在使用注解时,指定 value 的值可以不指定属性名称。

// 这两种方式效果相同
@RegexValid("^((//+)?86//s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))//d{8}$")
@RegexValid(value = "^((//+)?86//s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))//d{8}$")
复制代码

示例:

了解了注解属性的定义要点,让我们来为 @RegexValid 注解定义几个属性。

@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface RegexValid {
    enum Policy {
        // @formatter:off
        EMPTY(null),
        DATE("^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])//1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])//1"
            + "(?:29|30)|(?:0?[13578]|1[02])//1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|"
            + "(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2//2(?:29))$"),
        MAIL("^[A-Za-z0-9](([_//.//-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+)(([//.//-]?[a-zA-Z0-9]+)*)//.([A-Za-z]{2,})$");
        // @formatter:on

        private String policy;

        Policy(String policy) {
            this.policy = policy;
        }

        public String getPolicy() {
            return policy;
        }
    }

    String value() default "";
    Policy policy() default Policy.EMPTY;
}
复制代码

说明:

在上面的示例代码中,我们定义了两个注解属性: String 类型的 value 属性和 Policy 枚举类型的 policy 属性。 Policy 枚举中定义了几个默认的正则表达式,这是为了直接使用这几个常用表达式去正则校验。考虑到,我们可能需要自己传入一些自定义正则表达式去校验其他场景,所以定义了 value 属性,允许使用者传入正则表达式。

至此, @RegexValid 的声明已经结束。但是,程序仍不知道如何处理 @RegexValid 这个注解。我们还需要定义注解处理器。

3. 注解处理器

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

java.lang.annotation.Annotation 是一个接口,程序可以通过反射来获取指定程序元素的注解对象,然后通过注解对象来获取注解里面的元数据。

Annotation 接口源码如下:

public interface Annotation {
    boolean equals(Object obj);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}
复制代码

除此之外,Java 中支持 注解处理器接口 java.lang.reflect.AnnotatedElement ,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:

Class
Constructor
Field
Method
Package

java.lang.reflect 包下主要包含一些实现反射功能的工具类。实际上, java.lang.reflect 包所有提供的反射 API 扩充了读取运行时注解信息的能力。当一个注解类型被定义为运行时的注解后,该注解才能是运行时可见,当 class 文件被装载时被保存在 class 文件中的注解才会被虚拟机读取。 AnnotatedElement 接口是所有程序元素(Class、Method 和 Constructor)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象之后,程序就可以调用该对象的如下四个个方法来访问注解信息:

getAnnotation
getAnnotations
isAnnotationPresent
getDeclaredAnnotations

了解了以上内容,让我们来实现 @RegexValid 的注解处理器:

import java.lang.reflect.Field;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexValidUtil {
    public static boolean check(Object obj) throws Exception {
        boolean result = true;
        StringBuilder sb = new StringBuilder();
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            // 判断成员是否被 @RegexValid 注解所修饰
            if (field.isAnnotationPresent(RegexValid.class)) {
                RegexValid valid = field.getAnnotation(RegexValid.class);

                // 如果 value 为空字符串,说明没有注入自定义正则表达式,改用 policy 属性
                String value = valid.value();
                if ("".equals(value)) {
                    RegexValid.Policy policy = valid.policy();
                    value = policy.getPolicy();
                }

                // 通过设置 setAccessible(true) 来访问私有成员
                field.setAccessible(true);
                Object fieldObj = null;
                try {
                    fieldObj = field.get(obj);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                if (fieldObj == null) {
                    sb.append("/n")
                        .append(String.format("%s 类中的 %s 字段不能为空!", obj.getClass().getName(), field.getName()));
                    result = false;
                } else {
                    if (fieldObj instanceof String) {
                        String text = (String) fieldObj;
                        Pattern p = Pattern.compile(value);
                        Matcher m = p.matcher(text);
                        result = m.matches();
                        if (!result) {
                            sb.append("/n").append(String.format("%s 不是合法的 %s !", text, field.getName()));
                        }
                    } else {
                        sb.append("/n").append(
                            String.format("%s 类中的 %s 字段不是字符串类型,不能使用此注解校验!", obj.getClass().getName(), field.getName()));
                        result = false;
                    }
                }
            }
        }

        if (sb.length() > 0) {
            throw new Exception(sb.toString());
        }
        return result;
    }
}
复制代码

说明:

以上示例中的注解处理器,执行步骤如下:

  1. 通过 getDeclaredFields 反射方法获取传入对象的所有成员。
  2. 遍历成员,使用 isAnnotationPresent 判断成员是否被指定注解所修饰,如果不是,直接跳过。
  3. 如果成员被注解所修饰,通过 RegexValid valid = field.getAnnotation(RegexValid.class); 这样的形式获取,注解实例化对象,然后,就可以使用 valid.value()valid.policy() 这样的形式获取注解中设定的属性值。
  4. 根据属性值,进行逻辑处理。

4. 使用注解

完成了以上工作,我们就可以使用自定义注解了,示例如下:

public class RegexValidDemo {
    static class User {
        private String name;
        @RegexValid(policy = RegexValid.Policy.DATE)
        private String date;
        @RegexValid(policy = RegexValid.Policy.MAIL)
        private String mail;
        @RegexValid("^((//+)?86//s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))//d{8}$")
        private String phone;

        public User(String name, String date, String mail, String phone) {
            this.name = name;
            this.date = date;
            this.mail = mail;
            this.phone = phone;
        }

        @Override
        public String toString() {
            return "User{" + "name='" + name + '/'' + ", date='" + date + '/'' + ", mail='" + mail + '/'' + ", phone='"
                + phone + '/'' + '}';
        }
    }

    static void printDate(@RegexValid(policy = RegexValid.Policy.DATE) String date){
        System.out.println(date);
    }

    public static void main(String[] args) throws Exception {
        User user = new User("Tom", "1990-01-31", "xxx@163.com", "18612341234");
        User user2 = new User("Jack", "2019-02-29", "sadhgs", "183xxxxxxxx");
        if (RegexValidUtil.check(user)) {
            System.out.println(user + "正则校验通过");
        }
        if (RegexValidUtil.check(user2)) {
            System.out.println(user2 + "正则校验通过");
        }
    }
}
复制代码
原文  https://juejin.im/post/5ca755676fb9a05e4868c973
正文到此结束
Loading...