首页 > 基础资料 博客日记
Java核心知识体系2:注解机制详解
2023-07-24 18:38:17基础资料围观264次
1 Java注解基础
注解是JDK1.5版本开始引入的一个特性,用于对程序代码的说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。
它主要的作用有以下四方面:
- 生成javadoc文档,通过在代码里面标识元数据生成javadoc文档。
- 编译期的检查,通过标识的元数据让编译器在编译期间对代码进行验证。
- 编译时动态处理,编译时通过代码中标识的元数据动态处理,比如动态生成代码。
- 运行时动态处理,运行时通过代码中标识的元数据动态处理,比如使用反射技术注入实例。
注解的常见分类有三种:
- Java自带的标准注解,包括 @Override、@Deprecated和@SuppressWarnings,分别代表 方法重写、某个类或方法过时、以及忽略警告,用这些注解标明后编译器就会进行检查。
- 元注解,元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented 等6种
- @Retention:指定其所修饰的注解的保留策略
- @Document:该注解是一个标记注解,用于指示一个注解将被文档化
- @Target:用来限制注解的使用范围
- @Inherited:该注解使父类的注解能被其子类继承
- @Repeatable:该注解是Java8新增的注解,用于开发重复注解
- 类型注解(Type Annotation):该注解是Java8新增的注解,可以用在任何用到类型的地方
- 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
接下来我们通过这三种分类来逐一理解注解。
1.1 Java内置注解
我们先从Java内置注解开始说起,先看下下面的代码:
class Parent {
public void rewriteMethod() {
}
}
class Child extends Parent {
/**
* 重载父类的 rewriteMethod() 方法
*/
@Override
public void rewriteMethod() {
}
/**
* 被弃用的过时方法
*/
@Deprecated
public void oldMethod() {
}
/**
* 忽略告警
*
* @return
*/
@SuppressWarnings("keep run")
public List infoList() {
List list = new ArrayList();
return list;
}
}
Java 1.5开始自带的标准注解,包括@Override、@Deprecated和@SuppressWarnings:
@Override
:表示当前类中的方法定义将覆盖父类中的方法@Deprecated
:表示该代码段被弃用,但是可以使用,只是编译器会发出警告而已@SuppressWarnings
:表示关闭编译器的警告信息
我们再具体看下这几个内置注解,同时通过这几个内置注解中的元注解的定义来引出元注解。
1.1.1 内置注解 - @Override
我们先来看一下这个注解类型的定义:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
从它的定义我们可以看到,这个注解可以被用来修饰方法,并且它只在编译时有效,在编译后的class文件中便不再存在。这个注解的作用我们大家都不陌生,那就是告诉编译器被修饰的方法是重写的父类的中的相同签名的方法,编译器会对此做出检查,
若发现父类中不存在这个方法或是存在的方法签名不同,则会报错。
1.1.2 内置注解 - @Deprecated
这个注解的定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
从它的定义我们可以知道,它会被文档化,能够保留到运行时,能够修饰构造方法、属性、局部变量、方法、包、参数、类型。这个注解的作用是告诉编译器被修饰的程序元素已被“废弃”,不再建议用户使用。
1.1.3 内置注解 - @SuppressWarnings
这个注解我们也比较常用到,先来看下它的定义:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
它能够修饰的程序元素包括类型、属性、方法、参数、构造器、局部变量,只能存活在源码时,取值为String[]。它的作用是告诉编译器忽略指定的警告信息,它可以取的值如下所示:
参数 | 作用 | 原描述 |
---|---|---|
all | 抑制所有警告 | to suppress all warnings |
boxing | 抑制装箱、拆箱操作时候的警告 | to suppress warnings relative to boxing/unboxing operations |
cast | 抑制映射相关的警告 | to suppress warnings relative to cast operations |
dep-ann | 抑制启用注释的警告 | to suppress warnings relative to deprecated annotation |
deprecation | 抑制过期方法警告 | to suppress warnings relative to deprecation |
fallthrough | 抑制确在switch中缺失breaks的警告 | to suppress warnings relative to missing breaks in switch statements |
finally | 抑制finally模块没有返回的警告 | to suppress warnings relative to finally block that don’t return |
hiding | 抑制与隐藏变数的区域变数相关的警告 | to suppress warnings relative to locals that hide variable() |
incomplete-switch | 忽略没有完整的switch语句 | to suppress warnings relative to missing entries in a switch statement (enum case) |
nls | 忽略非nls格式的字符 | to suppress warnings relative to non-nls string literals |
null | 忽略对null的操作 | to suppress warnings relative to null analysis |
rawtype | 使用generics时忽略没有指定相应的类型 | to suppress warnings relative to un-specific types when using |
restriction | 抑制与使用不建议或禁止参照相关的警告 | to suppress warnings relative to usage of discouraged or |
serial | 忽略在serializable类中没有声明serialVersionUID变量 | to suppress warnings relative to missing serialVersionUID field for a serializable class |
static-access | 抑制不正确的静态访问方式警告 | to suppress warnings relative to incorrect static access |
synthetic-access | 抑制子类没有按最优方法访问内部类的警告 | to suppress warnings relative to unoptimized access from inner classes |
unchecked | 抑制没有进行类型检查操作的警告 | to suppress warnings relative to unchecked operations |
unqualified-field-access | 抑制没有权限访问的域的警告 | to suppress warnings relative to field access unqualified |
unused | 抑制没被使用过的代码的警告 | to suppress warnings relative to unused code |
1.2 元注解
上述内置注解的定义中使用了一些元注解(注解类型进行注解的注解类),在JDK 1.5中提供了4个标准的元注解:@Target,@Retention,@Documented,@Inherited, 在JDK 1.8中提供了两个新的元注解 @Repeatable和@Native。
1.2.1 元注解 - @Target
Target注解的作用是:描述注解的使用范围(即:被修饰的注解可以用在什么地方) 。
Target注解用来说明那些被它所注解的注解类可修饰的对象范围:
- packages、types(类、接口、枚举、注解类)
- 类成员(方法、构造方法、成员变量、枚举值)
- 方法参数和本地变量(如循环变量、catch参数)
在定义注解类时使用了@Target 能够更加清晰的知道它能够被用来修饰哪些对象,它的取值范围定义在ElementType 枚举中。枚举信息如下:
public enum ElementType {
TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类
PACKAGE, // 可用于修饰:包
TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
}
1.2.2 元注解 - @Retention & @RetentionTarget
Reteniton注解的作用是:描述注解保留的时间范围(即:被描述的注解在它所修饰的类中可以被保留到何时) 。
Reteniton注解用来限定那些被它所注解的注解类在注解到其他类上以后,可被保留到何时,一共有三种策略,定义在RetentionPolicy枚举中。枚举如下:
public enum RetentionPolicy {
SOURCE, // 源文件保留
CLASS, // 编译期保留,默认为该值,CLASS
RUNTIME // 运行期保留,可通过反射去获取注解信息
}
我们测试下这三种策略,在定义注解类的时候什么区别:
@Retention(RetentionPolicy.SOURCE)
public @interface SourcePolicy {
// 源文件保留策略
}
@Retention(RetentionPolicy.CLASS)
public @interface ClassPolicy {
// 编译器保留策略
}
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimePolicy {
// 运行期保留策略
}
上面已经定义好了三个注解类,我们再用这三个注解类再去注解方法,如下:
public class RetentionTest {
@SourcePolicy
public void sourcePolicy() {
}
@ClassPolicy
public void classPolicy() {
}
@RuntimePolicy
public void runtimePolicy() {
}
}
通过执行 javap -verbose RetentionTest命令获取到的RetentionTest 的 class 字节码内容如下。
{
public retention.RetentionTest();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public void sourcePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
public void classPolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
RuntimeInvisibleAnnotations:
0: #11()
public void runtimePolicy();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 15: 0
RuntimeVisibleAnnotations:
0: #14()
}
从 RetentionTest 的字节码内容我们可以得出以下两点结论:
- 编译器并没有记录下 sourcePolicy() 方法的注解信息
- 编译器使用 RuntimeInvisibleAnnotations 去记录 classPolicy()方法的注解信息
- 编译器使用 RuntimeVisibleAnnotations 去记录 runtimePolicy()方法的注解信息
1.2.3 元注解 - @Documented
Documented注解的作用如下:使用 javadoc 工具为类生成帮助文档,并确认是否保留注解信息。
以下代码在使用Javadoc工具可以生成 @DocAnnotation注解信息。
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DocAnnotation {
public String value() default "default";
}
@DocAnnotation("some method doc")
public void testMethod() {
// 测试方法的文档注解
}
1.2.4 元注解 - @Inherited
Inherited注解的作用:被它修饰的Annotation将具有继承特性。父类使用了被@Inherited修饰的Annotation,则子类将自动具备该注解。
我们来测试下这个注解:
- 定义@Inherited注解:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface InheritedAnnotation {
String [] values();
int number();
}
- 使用这个注解
@InheritedAnnotation(values = {"brand"}, number = 100)
public class UserInfo {
}
class Customer extends UserInfo {
@Test
public void testMethod(){
Class clazz = Student.class;
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.toString());
}
}
}
- 输出
xxx.InheritedAnnotation(values=[brand], number=100)
虽然Customer类没有显示地被注解@InheritedAnnotation,但是它的父类UserInfo被注解,而且@InheritedAnnotation被@Inherited注解,因此Customer类自动继承注解
1.2.4 元注解 - @Repeatable (Java8)
Repeatable是可重复使用的意思,允许在同一声明的类型(类,属性,或方法)中,可以多次使用同一个注解
JDK8之前要想实现注解重复使用,需要组合模式,编写和可读性都不是很好
public @interface Pet {
String myPet();
}
public @interface Pets {
Pet[] value();
}
public class RepeatAnnotationOV {
@Pets({@Pet(myPet="dog"),@Pet(myPet="cat")})
public void workMethod(){
}
}
由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解。
Java 8中的做法:
@Repeatable(Pets.class)
public @interface Pet {
String myPet();
}
public @interface Pets {
Pet[] value();
}
public class RepeatAnnotationNV {
@Pet(role="dog")
@Pet(role="cat")
public void workMethod(){ }
}
不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点
1.2.5 元注解 - @Native (Java8)
使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可
1.3 注解与反射接口
定义注解后,如何获取注解中的内容呢?反射包java.lang.reflect下的AnnotatedElement接口提供这些方法。这里注意:只有注解被定义为RUNTIME后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的方法来访问Annotation信息。我们看下具体的先关接口
- boolean isAnnotationPresent(Class<?extends Annotation> annotationClass)
判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false。注意:此方法会忽略注解对应的注解容器。 T getAnnotation(Class annotationClass)
返回该程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。- Annotation[] getAnnotations()
返回该程序元素上存在的所有注解,若没有注解,返回长度为0的数组。 T[] getAnnotationsByType(Class annotationClass)
返回该程序元素上存在的、指定类型的注解数组。没有注解对应类型的注解时,返回长度为0的数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。getAnnotationsByType方法与 getAnnotation的区别在于,getAnnotationsByType会检测注解对应的重复注解容器。若程序元素为类,当前类上找不到注解,且该注解为可继承的,则会去父类上检测对应的注解。T getDeclaredAnnotation(Class annotationClass)
返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释。如果没有注释直接存在于此元素上,则返回nullT[] getDeclaredAnnotationsByType(Class annotationClass)
返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注释- Annotation[] getDeclaredAnnotations()
返回直接存在于此元素上的所有注解及注解对应的重复注解容器。与此接口中的其他方法不同,该方法将忽略继承的注解。如果没有注释直接存在于此元素上,则返回长度为零的一个数组。该方法的调用者可以随意修改返回的数组,而不会对其他调用者返回的数组产生任何影响。
1.4 自定义注解
当我们理解了内置注解, 元注解和获取注解的反射接口后,我们便可以开始自定义注解了。这个例子我把上述的知识点全部融入进来, 代码很简单:
- 定义自己的注解
package com.helenlyn.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <p>Description: 水果供应者注解 </p>
* <p>Copyright: Copyright (c) 2021 </p>
* <p>Company: helenlyn Co., Ltd. </p>
*
* @author brand
* @date 2021/5/16 16:35
* <p>Update Time: </p>
* <p>Updater: </p>
* <p>Update Comments: </p>
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**
* 供应商编号
* @return
*/
public int id() default -1;
/**
* 供应商名称
* @return
*/
public String name() default "";
/**
* 供应商地址
* @return
*/
public String address() default "";
}
- 使用注解
package com.helenlyn.common.dto;
import com.helenlyn.common.annotation.FruitColor;
import com.helenlyn.common.annotation.FruitName;
import com.helenlyn.common.annotation.FruitProvider;
/**
* <p>Description: </p>
* <p>Copyright: Copyright (c) 2021 </p>
* <p>Company: helenlyn Co., Ltd. </p>
*
* @author brand
* @date 2021/5/16 16:28
* <p>Update Time: </p>
* <p>Updater: </p>
* <p>Update Comments: </p>
*/
public class AppleDto {
@FruitName("Apple")
private String appleName;
@FruitColor(fruitColor=FruitColor.Color.RED)
private String appleColor;
@FruitProvider(id=1,name="helenlyn 贸易公司",address="福州xx路xxx大楼")
private String appleProvider;
}
- 用反射接口获取注解信息
在 FruitInfoUtil 中进行测试:
/**
* <p>Description: FruitInfoUtil注解实现 </p>
* <p>Copyright: Copyright (c) 2021 </p>
* <p>Company: helenlyn Co., Ltd. </p>
*
* @author brand
* @date 2021/5/16 16:37
* <p>Update Time: </p>
* <p>Updater: </p>
* <p>Update Comments: </p>
*/
public class FruitInfoUtil {
public static String getFruitInfo(Class<?> clazz) {
String strFruitName = " 水果名称:";
String strFruitColor = " 水果颜色:";
String strFruitProvicer = "供应商信息:";
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(FruitName.class)) {
FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
strFruitName += fruitName.value();
System.out.println(strFruitName);
} else if (field.isAnnotationPresent(FruitColor.class)) {
FruitColor fruitColor = (FruitColor) field.getAnnotation(FruitColor.class);
strFruitColor += fruitColor.fruitColor().toString();
System.out.println(strFruitColor);
} else if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:" + fruitProvider.name() + " 供应商地址:" + fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
return String.format("%s;%s;%s;", strFruitName, strFruitColor, strFruitProvicer);
}
}
- 测试的输出
2022-07-09 11:33:41.688 INFO 5895 --- [TaskExecutor-35] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: cl-debug-rabbitmq-erp-service-7w0cpa.docker.sdp:9146
Hibernate: update UserBasicInfo set personName=? where personCode=?
水果名称:Apple
水果颜色:RED
供应商编号:1 供应商名称:helenlyn 贸易公司 供应商地址:福州xx路xxx大楼
2 理解注解的原理
2.1 Java 8 提供了哪些新的注解
- @Repeatable
- ElementType.TYPE_USE
- ElementType.TYPE_PARAMETER
ElementType.TYPE_USE(此类型包括类型声明和类型参数声明,是为了方便设计者进行类型检查)包含了ElementType.TYPE(类、接口(包括注解类型)和枚举的声明)和ElementType.TYPE_PARAMETER(类型参数声明), 可以看下面这个例子:
// 自定义ElementType.TYPE_PARAMETER注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_PARAMETER)
public @interface MyNotEmpty {
}
// 自定义ElementType.TYPE_USE注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
public @interface MyNotNull {
}
// 测试类
public class TypeParameterAndTypeUseAnnotation<@MyNotEmpty T>{
//使用TYPE_PARAMETER类型,会编译不通过
// public @MyNotEmpty T test(@MyNotEmpty T a){
// new ArrayList<@MyNotEmpty String>();
// return a;
// }
//使用TYPE_USE类型,编译通过
public @MyNotNull T test2(@MyNotNull T a){
new ArrayList<@MyNotNull String>();
return a;
}
}
2.2 注解支持继承吗?
注解是不支持继承的
不能使用关键字extends来继承某个@interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口。 虽然反编译后发现注解继承了Annotation接口,请记住,即使Java的接口可以实现多继承,但定义注解时依然无法使用extends关键字继承@interface。 区别于注解的继承,被注解的子类继承父类注解可以用@Inherited: 如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注解。
3 注解的应用场景
自定义注解多喝AOP - 通过切面实现解耦
笔者曾经在 《基于AOP的动态数据源切换》 这篇文章中有个典型的例子,就是使用AOP切面来对多数据源进行使用场景的切换,下面展示下如何通过注解实现解耦的。
- 自定义Annotation,映射的目标范围为 类型和方法。
/**
* @author brand
* @Description: 数据源切换注解
* @Copyright: Copyright (c) 2021
* @Company: Helenlyn, Inc. All Rights Reserved.
* @date 2021/12/15 7:36 下午
*/
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String name() default "";
}
- 编写AOP实现,切面代码,以实现对注解的PointCut,切点拦截
/**
* @author brand
* @Description:
* @Copyright: Copyright (c) 2021
* @Company: Helenlyn, Inc. All Rights Reserved.
* @date 2021/12/15 7:49 下午
*/
@Aspect
@Component
public class DataSourceAspect implements Ordered {
/**
* 定义一个切入点,匹配到上面的注解DataSource
*/
@Pointcut("@annotation(com.helenlyn.dataassist.annotation.DataSource)")
public void dataSourcePointCut() {
}
/**
* Around 环绕方式做切面注入
* @param point
* @return
* @throws Throwable
*/
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource ds = method.getAnnotation(DataSource.class);
String routeKey = ds.name(); // 从头部中取出注解的name(basic 或 cloudoffice 或 attend),用这个name进行数据源查找。
String dataSourceRouteKey = DynamicDataSourceRouteHolder.getDataSourceRouteKey();
if (StringUtils.isNotEmpty(dataSourceRouteKey)) {
// StringBuilder currentRouteKey = new StringBuilder(dataSourceRouteKey);
routeKey = ds.name();
}
DynamicDataSourceRouteHolder.setDataSourceRouteKey(routeKey);
try {
return point.proceed();
} finally { // 最后做清理,这个步骤很重要,因为我们的配置中有一个默认的数据源,执行完要回到默认的数据源。
DynamicDataSource.clearDataSource();
DynamicDataSourceRouteHolder.clearDataSourceRouteKey();
}
}
@Override
public int getOrder() {
return 1;
}
}
- 测试,在Control中写三个测试方法
/**
* 无注解默认情况:数据源指向basic
* @return
*/
@RequestMapping(value = "/default/{user_code}", method = RequestMethod.GET)
public UserInfoDto getUserInfo(@PathVariable("user_code") String userCode) {
return userInfoService.getUserInfo(userCode);
}
/**
* 数据源指向attend
* @return
*/
@DataSource(name= Constant.DATA_SOURCE_ATTEND_NAME)
@RequestMapping(value = "/attend/{user_code}", method = RequestMethod.GET)
public UserInfoDto getUserInfoAttend(@PathVariable("user_code") String userCode) {
return userInfoService.getUserInfo(userCode);
}
/**
* 数据源指向cloud
* @return
*/
@DataSource(name= Constant.DATA_SOURCE_CLOUD_NAME)
@RequestMapping(value = "/cloud/{user_code}", method = RequestMethod.GET)
public UserInfoDto getUserInfoCloud(@PathVariable("user_code") String userCode) {
return userInfoService.getUserInfo(userCode);
}
- 执行效果
除此之外,我们可以看到很多日志管理、权限管理,也都是也是通过类似的注解机制来实现的,通过注解+AOP来最终实现模块之间的解耦,以及业务与系统层面的解耦 。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
上一篇:linux 安装java环境变量
下一篇:Java实现短信发送