⑧JAVA 注解
⑧JAVA 注解
学习核心
- 注解
- 什么是注解?作用是什么?
- 怎么自定义注解?
学习资料
注解概念
注解概念
Java中,注解是以 @
字符开始的修饰符(例如最基础的@Override
)
包含命名或者未命名的属性(例如@Author name="oop" date="01/01/1900"
)
如果只有一个名为value的属性,则可以省略名称(例如@SuppressWarnings("unchecked")
)
从本质上来说,注解是一种标签,其实质上可以视为一种特殊的注释,如果没有解析它的代码,它并不比普通注释强。
解析一个注解往往有两种形式:
- 编译期直接的扫描 - 编译器的扫描指的是编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。这种情况只适用于 JDK 内置的注解类。
- 运行期的反射 - 如果要自定义注解,Java 编译器无法识别并处理这个注解,它只能根据该注解的作用范围来选择是否编译进字节码文件。如果要处理注解,必须利用反射技术,识别该注解以及它所携带的信息,然后做相应的处理
注解分类
- 按照运行机制分
- 源码注解:在源码中存在,编译称class文件就不存在了
- 编译时注解:在源码和.class文件中存在(例如JDK自带的注解,在编译时起作用)
- 运行时注解:在程序运行阶段还起作用,甚至会影响运行逻辑的注解
- 按照来源分
- JDK注解
- 第三方注解
- 自定义注解
- 元注解(注解的注解)
注解作用
注解有许多用途:
- 编译器信息 - 编译器可以使用注解来检测错误或抑制警告
- 编译时和部署时的处理 - 程序可以处理注解信息以生成代码,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 {}
1.Java中的常见注解
基本的 Annotation:
- @Override: 限定某个方法,是重写父类方法, 该注解只能用于方法
- @Deprecated: 用于表示某个程序元素(类, 方法等)已过时
- @SuppressWarnings: 抑制编译器警告
@SafeVarargs
(JDK7 引入)@FunctionalInterface
(JDK8 引入)
常见第三方注解
- Spring:@Autowire、@Service、@Repository
- MyBatis:@InsertProvider、@UpdateProvider、@Options
@Override
一般场景用于子类继承父类重写父类方法
public class OverrideDemo {
public static void main(String[] args) {
Son son = new Son();
son.fly();
}
}
class Father{
public void fly(){
System.out.println("父类 fly方法");
}
}
class Son extends Father{
@Override
public void fly(){
System.out.println("子类 fly方法");
}
}
@Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
结合源码记忆@Override的作用
- 只能作用于方法(看源码的@Target属性,@Target是修饰注解的注解成为元注解)
- @Override表示显示指定重写的父类(从编译层验证),如果父类没有要重写的方法就会报错
- 如果子类和父类满足重写规则,就算不加@Orerride仍然构成重写
@Deprecated
@Deprecated: 用于表示某个程序元素(类, 方法等)已过时。可以修饰方法、类、字段、包、参数等信息,一般在版本升级过渡时使用
例如此处标记A类以过期,此时如果在方法中使用A则会提示如下(表示A已经过期),但是A类还是可以正常使用的
public class DeprecatedDemo {
public static void main(String[] args) {
A a = new A();
a.test();
}
}
// 使用@Deprecated标识这个类已经过期
@Deprecated
class A{
public void test(){
System.out.println("testing");
}
}
@SafeVarargs
@SafeVarargs
在 JDK7 中引入。@SafeVarargs
的作用是:告诉编译器,在可变长参数中的泛型是类型安全的。可变长参数是使用数组存储的,而数组和泛型不能很好的混合使用。
简单的说,数组元素的数据类型在编译和运行时都是确定的,而泛型的数据类型只有在运行时才能确定下来。因此,当把一个泛型存储到数组中时,编译器在编译阶段无法确认数据类型是否匹配,因此会给出警告信息;即如果泛型的真实数据类型无法和参数数组的类型匹配,会导致 ClassCastException
异常。
@SafeVarargs
注解使用范围:
@SafeVarargs
注解可以用于构造方法。@SafeVarargs
注解可以用于static
或final
方法。
@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);
}
}
@SuppressWarnings
@SuppressWarnings: 抑制编译器警告
类型 | 说明 |
---|---|
unchecked | 忽略没有检查的警告 |
rawtypes | 忽略没有指定泛型的警告(传参时没有指定泛型的警告错误) |
unused | 忽略没有使用某个变量的警告错误 |
@SuppressWarnings 可以修饰的程序元素为,查看@Target 源码
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
@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);
}
}
2.自定义注解
元注解
JDK 的元 Annotation 用于修饰其他 Annotation,元注解种类(在看代码的时候可以关注)
Retention:指定注解的作用范围(生命周期: SOURCE、CLASS、RUNTIME)
Target:指定注解可以在哪些地方使用(指定注解可以修饰的元素类型)
Documented:指定该注解是否会在 javadoc 体现
Inherited:子类会继承父类注解(如果注解类型声明存在该元注解,则注解所修饰类的所有子类都是继承此注解)
Repeatable:表示注解可以重复使用
@Retention:RetentionPolicy
RetentionPolicy.SOURCE
:标记的注解仅在源文件中有效,编译器会忽略RetentionPolicy.CLASS
:标记的注解在 class 文件中有效,JVM 会忽略RetentionPolicy.RUNTIME
:标记的注解在运行时有效
@Target:ElementType
ElementType.ANNOTATION_TYPE
- 标记的注解可以应用于注解类型ElementType.CONSTRUCTOR
- 标记的注解可以应用于构造函数ElementType.FIELD
- 标记的注解可以应用于字段或属性ElementType.LOCAL_VARIABLE
- 标记的注解可以应用于局部变量ElementType.METHOD
- 标记的注解可以应用于方法。ElementType.PACKAGE
- 标记的注解可以应用于包声明ElementType.PARAMETER
- 标记的注解可以应用于方法的参数ElementType.TYPE
- 标记的注解可以应用于类的任何元素
@Documented:指定该注解是否会在 javadoc 体现(默认情况下注释不包括在Javadoc中)
@Inherited:表示注解类型可以被继承(默认情况下不是这样)
@Repeatable:表示注解可以重复使用(参考Spring的
@Scheduled
)
语法要求
使用 @interface
自定义注解时,自动继承了 java.lang.annotation.Annotation
接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface
用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过 default
来声明参数的默认值。
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@interface Description{
String desc();
String author();
int age() default 18;
}
// 基于上述注解其使用规则参考如下
@Description(desc = "用户",author = "holic-x",age = 18)
class User{
private String name;
}
idea中JAVADOC的生成(可以指定工程、单个文件)=》指定输出目录,生成文件
注解定义只是一种声明,更多的是需要借助反射去解析注解进而扩展代码
/**
* 自定义注解@Description
*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@interface Description{
String desc();
String author();
int age() default 18;
}
@Description(desc = "用户类",author = "holic-x",age = 18)
class User{
private String name;
@Description(desc = "getName方法",author = "noob",age = 33)
public String getName() {
return name;
}
}
// 定义注解解析类(通过反射机制)
class ParseDescriptionAnno{
// 获取类上的注解
public static void getClassAnno(){
// 1.使用类加载器加载类
Class clazz = User.class;
// 2.找到类上面的注解
boolean isExistAnno = clazz.isAnnotationPresent(Description.class);
// 3.获取到注解示例
if(isExistAnno){
Description d = (Description) clazz.getAnnotation(Description.class);
System.out.println(d.desc());
}
}
// 获取方法上的注解
public static void getMethodAnno(){
Method[] methods = User.class.getMethods();
for(Method method : methods){
if(method.isAnnotationPresent(Description.class)){
Description d = (Description) method.getAnnotation(Description.class);
System.out.println(d.desc());
}
}
}
// 获取方法上的注解(另一种方式获取)
public static void getMethodAnnoByOther(){
Method[] methods = User.class.getMethods();
for(Method method : methods){
// 获取方法上的所有注解
Annotation[] annotations = method.getDeclaredAnnotations();
for(Annotation annotation : annotations){
if(annotation instanceof Description){
Description d = (Description) annotation;
System.out.println(d.desc());
}
}
}
}
public static void main(String[] args) {
getClassAnno();
getMethodAnno();
getMethodAnnoByOther();
}
}
// output
用户类
getName方法
getName方法
可以通过调整元注解来验证元注解的功能,例如将@Retention(RetentionPolicy.RUNTIME)的作用域调整为SOURCE级别再运行,就会发现控制台不打印任何内容,是因为此时注解的作用域被限定为源码级别,当编译为class则该注解就没了
3.注解实战
(1)仿hibernate实现SQL解析
需求分析
用户表:用户ID、用户名、昵称、年龄、性别、所在城市、邮箱、手机号
实现:方便地对每个字段或者字段的组合条件进行检索,并打印出sql,使用方式要足够简单
基础框架内容参考如下,需要借助注解结合查询条件构建SQL语句
// 查询条件
class Filter{
private int userId;
private String userName;
private String nickName;
private int age;
private int sex;
private String city;
private String email;
private String phone;
public void setEmail(String email) {
this.email = email;
}
public void setUserId(String userId) {
this.userId = userId;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
public class CustomSQLAnnotationDemo {
public static void main(String[] args) {
Filter f1 = new Filter();
f1.setUserId("1");// 查询id为10的用户
Filter f2 = new Filter();
f2.setUserName("lucy");// 模糊查询用户名为lucy的用户
Filter f3 = new Filter();
f1.setEmail("lucy@163.com,xiaobai@126.com");// 查询邮箱为任意一个的用户信息
// 根据上述条件,生成相应的sql语句
String sql1 = query(f1);
String sql2 = query(f2);
String sql3 = query(f3);
System.out.println(sql1);
System.out.println(sql2);
System.out.println(sql3);
}
private static String query(Filter f1) {
return null;
}
}
构建思路:
针对Filter(它既是一个对应着数据表的实体、又可以作为查询条件),参考常见的ORM框架,此处设定两个注解(@Table、@Column)分别将Filter与数据表的内容进行映射
注解构建完成,通过反射解析Filter类对象,获取到对象上所有注解信息以及对象信息,随后再根据这些信息进行SQL封装(在构建SQL的时候可以理清思路,先获取到拼接SQL所需要的参数信息,然后再一步步结合语境优化不同场景下的SQL处理)
例如一开始可能是获取到注解信息和对应对象的字段和值信息,然后拼接成一条完整的SQL语句
# 初版
select * from user where 1=1
and userId=1 and userName=xx and email=xxx@qq.com,xxx@163.com
# V1:针对不同类型的字段相应做SQL转化处理(例如字符串需要‘’拼接)
- 程序上则可通过instanceof判断字段的不同数据类型,再进行转化处理
select * from user where 1=1
and userId=1 and userName='xx' and email='xxx@qq.com,xxx@163.com'
# V2:再进一步优化,针对email这种类型的字段,传入String字符串需要解析为in操作
- 程序上根据fieldValue是否包括指定分隔符(例如此处为,)进行判断,如果包含则将其结构进行拆分然后依次拼接为集合检索
- 这种拆分方式逻辑上是可行,但是场景分析来看存在缺陷,不能够单一的认为存在指定分隔符就将其解析为集合检索,会存在异议。而是应该考虑更加完善的机制去约束输入输出
select * from user where 1=1
and userId=1 and userName='xx' and email in ('lucy@163.com','xiaobai@126.com')
参考实践样例
// 自定义注解@Table
import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
// 自定义注解@Table
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Table{
String value();
}
// 自定义注解@Cloumn
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Column{
String value();
}
// 查询条件
@Table("user")
class Filter{
@Column("user_id")
private int userId;
@Column("user_name")
private String userName;
@Column("nick_name")
private String nickName;
@Column("age")
private int age;
@Column("sex")
private int sex;
@Column("city")
private String city;
@Column("email")
private String email;
@Column("phone")
private String phone;
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
public class CustomSQLAnnotationDemo {
public static void main(String[] args) throws Exception {
Filter f1 = new Filter();
f1.setUserId(1);// 查询id为10的用户
Filter f2 = new Filter();
f2.setUserName("lucy");// 模糊查询用户名为lucy的用户
Filter f3 = new Filter();
f1.setEmail("lucy@163.com,xiaobai@126.com");// 查询邮箱为任意一个的用户信息
// 根据上述条件,生成相应的sql语句
String sql1 = query(f1);
String sql2 = query(f2);
String sql3 = query(f3);
System.out.println(sql1);
System.out.println(sql2);
System.out.println(sql3);
}
/**
* query 方法:解析注解生成sql语句
* @param f
* @return
*/
private static String query(Filter f) throws Exception {
// 定义字符串对象封装sql
StringBuilder sb = new StringBuilder();
String tableName = "";
// 获取到class
Class c = f.getClass();
// 根据class获取到相关注解
boolean isExistTable = c.isAnnotationPresent(Table.class);
if(isExistTable){
// 获取到tableName
Table tableAnno = (Table)c.getAnnotation(Table.class);
tableName = tableAnno.value();
}
// 拼接检索语句
sb.append("select * from " + tableName).append(" where 1=1 ");
// 获取所有字段上的注解内容
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
boolean isExistFieldAnno = field.isAnnotationPresent(Column.class);
if(isExistFieldAnno){
Column columnAnno = (Column)field.getAnnotation(Column.class);
// 通过反射获取到字段值(因为方法名有规律,可以通过字段名获取到方法名)
String fieldName = field.getName();
String getMethodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
Method getMethod = c.getDeclaredMethod(getMethodName,null);
// 调用方法获取到字段值
Object fieldValue = getMethod.invoke(f,null);
// 处理不同字段类型转换、以及null字段值处理(String类型为null则不拼接、Integer类型为0也不拼接)
if(null == fieldValue || (fieldValue instanceof Integer && (Integer)fieldValue==0)){
continue;
}
if(fieldValue instanceof String){
if(((String) fieldValue).contains(",")){
// 对于String类型的列表数据(通过,进行分割),将其解析为in形式
sb.append("and ").append(fieldName).append(" in (");
String[] strs = fieldValue.toString().split(",");
for (String str : strs) {
sb.append("'" + str + "',");
}
// 删除最后一个逗号(,)
sb.deleteCharAt(sb.length()-1);
sb.append(")");
}else{
// 如果是String类型,则需要拼接‘’
sb.append("and " + fieldName + "='" + fieldValue + "' ");
}
}else if(fieldValue instanceof Integer){
// 如果是Integer类型,则直接拼接
sb.append("and " + fieldName + "=" + fieldValue + " ");
}
}
}
return sb.toString();
}
}
(2)自定义参数校验器
需求分析
自定义参数校验器:RegexValid注解、RegexValidUtil注解解析器(注解处理器)、RegexValidDemo使用自定义注解
- RegexValid注解:指定接收value、policy参数(其中Policy是枚举类型)
// 定义自定义注解RegexValid
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@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;
}
- RegexValidUtil注解解析器(注解处理器):解析对应作用于范围的注解信息,随后进行校验
- 例如此处注解是作用于字段(Field),用于校验参数是否符合约定
// 定义注解解析器(解析使用了该注解的内容)
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;
}
}
- 自定义Person类,使用@RegexValid注解进行校验
// 定义类使用自定义注解校验参数
class Person{
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 Person(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 + '\'' + '}';
}
}
- main方法测试(验证自定义注解解析器(处理器))
public class RegexValidDemo {
// 方法上使用自定义注解RegexValid对日期进行校验
static void printDate(@RegexValid(policy = RegexValid.Policy.DATE) String date){
System.out.println(date);
}
public static void main(String[] args) throws Exception {
Person p1 = new Person("Tom", "1990-01-31", "xxx@163.com", "18612341234");
Person p2 = new Person("Jack", "2019-02-29", "sadhgs", "183xxxxxxxx");
if (RegexValidUtil.check(p1)) {
System.out.println(p1 + "正则校验通过");
}
if (RegexValidUtil.check(p2)) {
System.out.println(p2 + "正则校验通过");
}
}
}