注解,和反射一样,是Java中最重要却最容易被人遗忘的知识点。哪怕Spring、SpringMVC、SpringBoot等框架中充满了注解,我们还是选择性地忽视它。很多人不明白它是怎么起作用的,甚至有人把它和注释混淆…工作中也只是机械性地在Controller上加@RequestMapping。是的,我们太习以为常了,以至于觉得它理所应当就是如此。
public @interface 注解名称{
属性列表;
}
如果说注释是给人看的,那么注解就是给程序看的,它就像是一个标签,贴在一个字段,类或者方法上,它的目的是为当前读取该注解的程序提供判断依据及少量附加信息。比如程序只要读到加了@Test的方法,就知道该方法是待测试方法。
@interface和interface从名字上看非常相似,我猜注解的本质是一个接口(当然,这是瞎猜)。
为了验证这个猜测,我们做个实验。先按上面的格式写一个注解(暂时不附加属性):
编译后得到字节码文件:通过XJad工具反编译MyAnnotation.class:
我们发现,@interface变成了interface,而且自动继承了Annotation:
既然确实是个接口,那么我们自然可以在里面写方法
得到class文件后反编译
虽说注解的本质是接口,但是仍然有很多怪异的地方,比如使用注解时,我们竟然可以给getValue()赋值:
@MyAnnotation(getValue = "annotation on class")
public class Demo {
@MyAnnotation(getValue = "annotation on field")
public String name;
@MyAnnotation(getValue = "annotation on method")
public void hello() {}
}
所以在注解里,类似于String getValue()这种,被称为“属性”,而给属性赋值显然听起来好接受多了。
上面说过,注解就像一个标签,是贴在程序代码上供另一个程序读取的。
要牢记,只要用到注解,必然有三角关系:
● 定义注解
● 使用注解
● 读取注解
反射获取注解信息:
public class AnnotationTest {
public static void main(String[] args) throws Exception {
// 获取类上的注解
Class<Demo> clazz = Demo.class;
MyAnnotation annotationOnClass = clazz.getAnnotation(MyAnnotation.class);
System.out.println(annotationOnClass.getValue());
// 获取成员变量上的注解
Field name = clazz.getField("name");
MyAnnotation annotationOnField = name.getAnnotation(MyAnnotation.class);
System.out.println(annotationOnField.getValue());
// 获取hello方法上的注解
Method hello = clazz.getMethod("hello", (Class<?>[]) null);
MyAnnotation annotationOnMethod = hello.getAnnotation(MyAnnotation.class);
System.out.println(annotationOnMethod.getValue());
// 获取defaultMethod方法上的注解
Method defaultMethod = clazz.getMethod("defaultMethod", (Class<?>[]) null);
MyAnnotation annotationOnDefaultMethod = defaultMethod.getAnnotation(MyAnnotation.class);
System.out.println(annotationOnDefaultMethod.getValue());
}
}
Class、Method、Field对象都有个getAnnotation()方法,可以获取各自位置上的注解信息,但这样程序运行的时候会报错,这是因为注解有所谓的保留策略,控制自己可以保留到哪个阶段。保留策略也是通过注解实现的,它属于元注解,也叫做元数据。
元注解就是加在注解上的注解,常用的就是:
● @Documented:用于制作文档,不是很重要,忽略便是
● @Target:加在注解上,限定该注解的使用位置。不写的话,好像默认各个位置都是可以的。
●@Retention:注解的保留策略
注解的保留策略有三种:SOURCE/ClASS/RUNTIME
一般来说,普通开发者使用注解的时机都是运行时,比如反射读取注解(也有类似Lombok这类编译期注解)。既然反射是运行时调用,那就要求注解的信息必须保留到虚拟机将.class文件加载到内存为止。如果你需要反射读取注解,却把保留策略设置为RetentionPolicy.SOURCE、RetentionPolicy.CLASS,那就读取不到了。
大多数情况下,三角关系中我们只负责使用注解,无需定义和执行,框架会将注解类和读取注解的程序隐藏起来,除非阅读源码,否则根本看不到。平时见不到定义和读取的过程,光顾着使用注解,久而久之很多人就忘了注解如何起作用了!
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyBefore {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAfter {
}
山寨junit
public class Junit {
//山赛Junit
@MyBefore
public void init(){
System.out.println("mybefore");
}
@MyTest
public void test(){
System.out.println("mytest");
}
@MyAfter
public void destory(){
System.out.println("myafger");
}
}
读取注解
public class MyJunitFrameWork {
//读取注解
public static void main(String[] args) throws Exception {
Class<Junit> aClass = Junit.class;
Object obj = aClass.newInstance();
//获取所有的方法
Method[] methods = aClass.getMethods();
//迭代出每一个Method对象,判断哪些方法上使用了@MyBefore/@MyAfter/@MyTest注解
ArrayList<Method> myBefore = new ArrayList<>();
ArrayList<Method> myAfter = new ArrayList<>();
ArrayList<Method> myTest = new ArrayList<>();
for (Method method : methods) {
if (method.isAnnotationPresent(MyBefore.class)){
myBefore.add(method);
}else if (method.isAnnotationPresent(MyAfter.class)){
myAfter.add(method);
}else if (method.isAnnotationPresent(MyTest.class)){
myTest.add(method);
}
}
//执行方法测试
for (Method method : myTest) {
//before
for (Method method1 : myBefore) {
method1.invoke(obj);
}
//test
method.invoke(obj);
//after
for (Method method1 : myAfter) {
method1.invoke(obj);
}
}
}
}
要写山寨JPA需要两个技能:注解+反射。
注解已经学过了,反射其实还有一个进阶内容,之前那篇反射文章里没有提到,放在这里补充。至于是什么内容,一两句话说不清楚。慢慢来吧。
首先,要跟大家介绍泛型中几个定义(记住最后一个):
● ArrayList中的E称为类型参数变量
● ArrayList中的Integer称为实际类型参数
● 整个ArrayList称为泛型类型
● 整个ArrayList称为参数化的类型ParameterizedType
接下来看问题:
class A<T>{
public A(){
/*
我想在这里获得子类B、C传递的实际类型参数的Class对象
class java.lang.String/class java.lang.Integer
*/
}
}
class B extends A<String>{
}
class C extends A<Integer>{
}
可能你会想了,直接T.class不就行了吗。明确一点,这样是不行的
是不是有点懵了,那就一步一步来
看代码:
public class Test {
public static void main(String[] args) {
new B();
}
}
class A<T>{
public A(){
// this是谁?A还是B?
Class clazz = this.getClass();
System.out.println(clazz.getName());
}
}
class B extends A<String>{
}
根据之前学习到的,这个this是B,这样我们就迈出了第一步,在父类中获得子类的Class对象
分析:
class A<T>{
public A(){
//clazz是B.class
Class clazz = this.getClass();
}
}
class B extends A<String>{
}
我们在泛型父类中获得了子类Class对象,而我们要等到泛型父类中泛型的Class对象,先不说泛型的class对象,我们怎么通过子类Class对象获得父类Class对象?
查阅API文档,我们发现有这么个方法:
Generic Super Class,直译就是“带泛型的父类”。也就是说调用getGenericSuperclass()就会返回泛型父类的Class对象。这非常符合我们的情况,因为Class A确实是泛型类。试着打印一下:
上面已经证明了通过子类Class对象可以获取父类Class对象,接下来我们尝试如何获取带有实际类型参数的父类class,虽然genericSuperclass是Type接收的,但可以看出实际类型为ParameterizedTypeImpl:
这里我们不去关心Type、ParameterizedType还有Class之间的继承关系,总之以我们多年的编码经验,子类的方法总是更多,所以毫不犹豫地向下转型:
public class JpaTest {
public static void main(String[] args) {
new B();
}
}
class A<T> {
public A() {
Class<? extends A> subClass = this.getClass();
// 得到泛型父类
Type genericSuperclass = subClass.getGenericSuperclass();
// 本质是ParameterizedTypeImpl,可以向下强转
ParameterizedType parameterizedTypeSuperclass = (ParameterizedType) genericSuperclass;
// 强转后可用的方法变多了,比如getActualTypeArguments()可以获取Class A的泛型的实际类型参数
Type[] actualTypeArguments = parameterizedTypeSuperclass.getActualTypeArguments();
// 由于A类只有一个泛型,这里可以直接通过actualTypeArguments[0]得到子类传递的实际类型参数
Class actualTypeArgument = (Class) actualTypeArguments[0];
System.out.println(actualTypeArgument);
System.out.println(subClass.getName());
}
}
class B extends A<String> {
}
class C extends A<Integer> {
}
这样我们就能在父类中等到子类继承时传递的泛型的实际类型参数。
CREATE TABLE `User` (
`name` varchar(255) DEFAULT NULL COMMENT '名字',
`age` tinyint(4) DEFAULT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@Data
@AllArgsConstructor
public class User {
private String name;
private Integer age;
}
BaseDao
public class BaseDao<T> {
private static BasicDataSource datasource;
//静态代码块,设置连接数据库的参数
static {
datasource = new BasicDataSource();
datasource.setDriverClassName("com.mysql.cj.jdbc.Driver");
datasource.setUrl("jdbc:mysql://localhost:3306/test");
datasource.setUsername("root");
datasource.setPassword("root");
}
//得到jdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(datasource);
private Class<T> beanClass;
/*
构造器,初始化的时候,获取实际类型参数,比如BaseDao ,那么beanClass就是user.class
*/
public BaseDao(){
beanClass= (Class<T>)((ParameterizedType)this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
public void add(T bean){
//获取user对象的所有字段
Field[] fields = beanClass.getFields();
//拼接sql语句
StringBuffer sb = new StringBuffer();
sb.append("insert into");
sb.append(beanClass.getSimpleName());
sb.append(" values(");
for (int i=0;i< fields.length;i++){
sb.append("?");
if (i< fields.length-1){
sb.append(",");
}
}
sb.append(")");
//获取要插入的数据的值
ArrayList<Object> list = new ArrayList<>();
try {
for (Field field : fields) {
field.setAccessible(true);
Object o = field.get(bean);
list.add(o);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
int size = list.size();
Object[] objects = list.toArray(new Object[size]);
int update = jdbcTemplate.update(sb.toString(), objects);
System.out.println(update);
}
UserDao
public class UserDao extends BaseDao<User>{
@Override
public void add(User bean) {
super.add(bean);
}
}
测试类
public class UserDaoTest {
public static void main(String[] args) {
UserDao userDao = new UserDao();
User user = new User("张三", 18);
userDao.add(user);
}
}
看到这里你不仅要问了,这和注解有什么关系呢,一个注解都没有用上!!!!
别急,马上上第二版JPA
CREATE TABLE `t_jpa_user` (
`name` varchar(255) DEFAULT NULL COMMENT '名字',
`age` tinyint(4) DEFAULT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
@Table注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
给新user加上注解
@Data
@Table("t_jpa_user")
@AllArgsConstructor
public class User {
private String name;
private Integer age;
}