面试|你不了解的注解(二)

这篇博文是接着上篇《面试|你不了解的注解(一)》写的,这里简单回顾下上篇博文的主要内容:

  • 注解的基本概念;
  • 注解内的四种标准注解:@Override,@Deprecated,@SuppressWarnings和@Native;
  • 注解内的五种元注解:@Target,@Retention,@Documented,@Inherited和@Repeatable;

分析上面九种注解的源码,解析每种注解的基本作用和使用场景。

这篇博文是基于元注解而自定义注解的例子,代码居多,所以还是建议自己运行下才能理解自定义注解的过程。主要内容包括两部分:

  • 自定义类注解
  • 自定义属性注解
  • 自定义注解注意事项
自定义注解

如果你熟悉Spring框架,那么@Autowired,@Controller以及@RequestMapping等注解你肯定见过并使用过,如果查看这些注解的源码,你会发现它们都是基于元注解而定制的注解。
自定义注解很简单,可关键在于定义注解处理器。下面尝试定义一个类注解和一个属性注解,以此熟悉注解的定义过程和注意事项。这里先给出自定义注解的步骤:

  • ①自定义注解;
  • ②定义注解处理器;
  • ③使用注解;
1.自定义类注解

注解功能:类根据注解的值确定分库策略,这里的类一般是指跟数据库对应的POJO类。

1.1 定义注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ShardMapping {
	String value() default "";  // 默认值不能是null
}

根据元注解@Target可知道,该注解作用于类上;根据元注解@Retention可知,该注解一直保留到JVM内;元注解@Documented表明如果形成doc文档,该注解会记录在javadoc文档内。

通过命令javap -c ShardMapping查看注解ShardMapping的字节码:

可以知道以下几点:

  • 自定义注解本质上是一个接口类型;
  • 自定义注解都会自动继承java.lang.annotation.Annotation接口;猜测可能是@字符的作用;
  • 自定义注解内的元素都是public的抽象方法;即只能用public和abstract修饰,可以省略不写;
  • 自定义注解内的元素可以有抽象方法的默认返回值,用default关键字来表示;

到这里,我们其实还不明确注解@ShardMapping有什么作用,下面是注解处理器。

1.2 定义注解处理器
public class ShardMappingAnnotationHandler {
	static Map<String, String> shardMap = new HashMap<String, String>(8);
	void processAnnotation(Object object) {
		if (object == null) {
			return;
		}
		Class<?> classObject = object.getClass();
		
		if (!classObject.isAnnotationPresent(ShardMapping.class)) {
			return;   // object对象没有被ShardMapping注解修饰
		}
		String className = classObject.getName();
		ShardMapping shardMapping = classObject.getAnnotation(ShardMapping.class);
		String value = shardMapping.value();
		if ("".equals(value)) {
			System.out.println(className + ": shardMapping value is default, go on...");
			shardMap.put(className, "defaule");
		} else if ("hashCode".equals(value)) {
			System.out.println(className + ": shardMapping value is hashCode, go on...");
			shardMap.put(className, "hashCode");
		} else if ("shardRef".equals(value)) {
			System.out.println(className + ": shardMapping value is shardRef, go on...");
			shardMap.put(className, "shardRef");
		} else {
			throw new RuntimeException(className + ": shardMapping value illegal. game over...");
		}
	}
}

注解处理器的大致思路是:

  • ①利用反射判断参数对象是否被@ShardMapping注解修饰;
  • ②利用反射获取该对象上注解@ShardMapping注解内的值,即value()方法返回的值;
  • ③建立类与不同策略的映射关系,我们这里把分库策略简单的用“default”,"hashCode"和“shardRef”等字符串来表示,等到使用的时候就可以根据这个映射关系来进行分库路由;

通过梳理注解处理器的编写思路,我们知道反射在其中起了很大的作用。关于反射的内容,可以看这篇博文

1.3 使用自定义注解

下面用@ShardMapping注解修饰类:

@ShardMapping("hashCode")   // 也可以写成 @ShardMapping(value="hashCode")
public class DeparementTable {
	public String depID;
	public String depName;
	public String depRank;
}
@ShardMapping("shardRef")     // 也可以写成 @ShardMapping(value="shardRef")
public class EmployeeTable {
	public String EmpID;
	public String EmpName;
	public String EmpRank;
}

注意:此处可以直接使用@ShardMapping而不需要括号以及括号内内容,value()方法则返回默认值
注解处理器解析注解:

public class Client {
	public static void main(String[] args) {
		DeparementTable deparementTable = new DeparementTable();
		EmployeeTable employeeTable = new EmployeeTable();
		
		ShardMappingAnnotationHandler handler = new ShardMappingAnnotationHandler();
		handler.processAnnotation(deparementTable);
		handler.processAnnotation(employeeTable);
	}
}

从定义注解,到定义注解处理器,再到注解使用,可以知道注解处理器是自定义注解的重点,我们每自定义一个注解,就需要为其编写一个注解处理器。在某种程度上来说,这其实挺麻烦的,Java为了减少麻烦,内置了AnnotationProcessFactory工厂类,关于它的使用这里不作介绍,感兴趣的可以自行百度。

2.自定义属性注解

注解功能:根据注解值设置注解修饰的属性是否可以为空。

2.1 定义注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Nullable {
	boolean value() default true;
}

元注解@Target表明该注解只能用在属性上;注解元素value()方法默认返回true。

2.2 注解@Nullable的注解处理器:
public class NullAbleAnnotationHandler<T> {
	void processAnnotation(T object) {
		if (null == object) {
			return;
		}
		Class<? extends Object> classT = object.getClass();
		Field[] fields = classT.getDeclaredFields();
		for (Field field : fields) {
			if (!field.isAnnotationPresent(Nullable.class)) {
				continue;  // 没有注解的属性直接跳过
			}
			Nullable nullable = field.getAnnotation(Nullable.class);
			boolean value = nullable.value();
			System.out.println(classT.getName() + " " + field.getName() + ": nullable is " + value);
			if (value) {
				continue;   // 为true或者默认  则不检查是否有值
			}
			String fieldValue = null;
			try {
				fieldValue = field.get(object).toString();
			} catch (Exception e) {
				System.out.println(e);
			}
			if (fieldValue == null) {
				// 属性不可以为空
				System.out.println("属性不可以为空");
				throw new RuntimeException("属性不可以为空");
			}
		}
	}
}

注解处理器的主要思路:

  • ①通过反射获取对象的所有属性;
  • ②遍历属性,判断属性是否被@Nullable注解修饰;
  • ③对于被注解@Nullable修饰的属性,通过反射获取该注解以及注解内的value值;
  • ④如果value值为true,表明该属性可以为空;否则需要进一步判断;
  • ⑤通过反射获取该对象当前属性的值,如果属性为空,则表明不符合注解内的要求,则抛异常;
    注解处理器的逻辑稍微有点绕,其利用的还是反射原理。
2.3 使用@Nullable注解:
public class Student {
	@Nullable(false)
	String id;
	String name;
	String sex;
	// 省略get set方法
}

表明id属性不能为空。
使用注解处理器处理注解:

public class Client {
	public static void main(String[] args) {
		Student xiaoLi = new Student();
		xiaoLi.setId("001");
		xiaoLi.setName("xiaoLi");
		xiaoLi.setSex("man");
		
		Student xiaoSi = new Student();
		xiaoSi.setName("xiaoSi");
		xiaoSi.setId(null);     // 该对象的id为null
		
		NullAbleAnnotationHandler<Student> handler = new NullAbleAnnotationHandler<Student>();
		handler.processAnnotation(xiaoLi);
		handler.processAnnotation(xiaoSi);
	}
}

对象xiaoSi的id为null,这不符合@Nullable设置的值,因此运行过程中会抛出异常。

3 注意

这里有个点需要注意,定义注解内元素时,元素名称必须与使用时一致,如:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Nullable {
	boolean name() default true;      // 不是value(), 而是name()
}

使用:

public class Student {
	@Nullable(name = false)    // 使用时必须要写name=false
	String id;
        //  省略
}

如果采用自定义的元素(抽象方法)名称,则使用注解的时候要么采用默认的fefault值,即直接@Nullable,要么按照上面的方式写全,不能采用省略@Nullable(false)写法。所以,当只有一个元素时,建议直接使用value()而别再定义其他名称。

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值