zoukankan      html  css  js  c++  java
  • [Java工程实践] 注解

    三、注解    

    注解,和反射一样,是Java中最重要却最容易被人遗忘的知识点。哪怕Spring、SpringMVC、SpringBoot等框架中充满了注解,我们还是选择性地忽视它。很多人不明白它是怎么起作用的,甚至有人把它和注释混淆...工作中也只是机械性地在Controller上加@RequestMapping。是的,我们太习以为常了,以至于觉得它应该就是如此。

    内容介绍:

    • 两件小事
    • 注解的作用
    • 注解的本质
    • 反射注解信息
    • 元注解
    • 属性的数据类型及特别的属性:value和数组

    两件小事

    去年工作中,我遇到的两件事让注解重新走进我的视野。

    第一,18年6月我去了北京,参与开发了某中国五百强企业的一个加密系统,第一次接触到了SpringBoot。当我发现一个demo项目只要简单地写个启动类并加上 @SpringBootApplication就可以直接访问Controller时,感到非常震撼。整个demo没有一个配置文件,连web.xml也没有。

    由于开发进度很赶,当时没时间去研究它是如何做到的,但这件事让我意识到自己对注解还是了解得太少。

    第二,来到杭州后我又参与开发了一个金融借贷系统,那阵子对接了很多第三方的风控接口:

    对签名验签不了解的朋友,可以百度一下。总之,每对接一个接口,都要在开头进行数据校验。一两个接口也就算了,但每次对接风控,基本上都要写10+多个方法。每个方法开头都写一份签名验签的代码,显然太冗余了。我当时的做法是将验签代码抽取成方法,方便复用,自以为算是一种改良了,直到我看到同事用了切面...40米的大刀拦腰砍去,给每个方法都做了签名验签:

    注意,实际上切面的作用是在方法前后,而不是方法内部的前后。上面这样画,仅仅为了更形象

    这两件事,让我知道,是时候重新学习一下注解了。


    注解的作用

    格式

    public @interface 注解名称{
        属性列表;
    }

    格式有点奇怪,我们稍后再研究。

    分类

    大致分为三类:自定义注解、JDK内置注解、还有第三方框架提供的注解。

    自定义注解就是我们自己写的注解。JDK内置注解,比如@Override检验方法重写,@Deprecated标识方法过期等。第三方框架定义的注解比如SpringMVC的@Controller等。

    使用位置

    实际开发中,注解常常出现在类、方法、成员变量、形参位置。当然还有其他位置,这里不提及。

    作用

    如果说注释是写给人看的,那么注解就是写给程序看的。它更像一个标签,贴在一个类、一个方法或者字段上。它的目的是为当前读取该注解的程序提供判断依据。比如程序只要读到加了@Test的方法,就知道该方法是待测试方法,又比如@Before注解,程序看到这个注解,就知道该方法要放在@Test方法之前执行。

    级别

    注解和类、接口、枚举是同一级别的。


    注解的本质

    @interface和interface这么相似,我猜注解的本质是一个接口。

    为了验证这个猜测,我们做个实验。先按上面的格式写一个注解

    属性先不写

    编译后得到字节码文件

    通过XJad工具反编译MyAnnotation.class

    我们发现,@interface变成了interface,而且自动继承了Annotation

    既然确实是个接口,那么我们自然可以在里面写方法

    得到class文件后反编译

    由于接口默认方法的修饰符就是public abstract,所以可以省略,直接写成:

    虽说注解的本质是接口,但是仍然有很多怪异的地方,比如使用注解时,我们竟然可以给getValue赋值:

    你见过给方法赋值的操作吗?(别闹了,你脑中想到的是给方法传参)。虽然这里的getValue可能不是指getValue(),底层或许是getValue()返回的一个同名变量。但不管怎么说,还是太怪异了。所以在注解里,类似于String getValue()这种,被称为“属性”。给属性赋值显然听起来好接受多了。

    另外,我们还可以为属性指定默认值:

    当没有赋值时,属性将使用默认值,比如上面的defaultMethod(),它的getValue就是“no description"。

    基于以上差异,以后还是把注解单独归为一类,不要当成接口使用。


    反射注解信息

    上文已经说过,注解就像一个标签,是贴在程序代码上供另一个程序读取的。所以三者关系是:

    要牢记,只要用到注解,必然有三角关系:定义注解,使用注解,读取注解。仅仅完成前两步,是没什么卵用的。就好比你写了一本武林秘籍却没人去学,那么这门武功还不如一把菜刀。

    所以,接下来我们写一个程序读取注解。读取注解的思路是:

    反射获取注解信息:

    我们发现,Class、Method、Field对象都有个getAnnotation(),可以获取各自位置的注解信息。

    但是控制台提示“空指针异常”,IDEA提示我们:Annotation 'MyAnnotation.class' is not retained for reflective。直译的话就是:注解MyAnnotation并没有为反射保留。

    这是因为注解其实有所谓“保留策略”的说法。大家学习JSP时,应该学过<!-- -->和<%-- -->的区别:前者可以在浏览器检查网页源代码时看到,而另一个在服务器端输出时就被抹去了。同样的,注解通过保留策略,控制自己可以保留到哪个阶段。保留策略也是通过注解实现,它属于元注解,也叫元数据。


    元注解

    所谓元注解,就是加在注解上的注解。作为普通程序员,常用的就是:

    • @Documented

    用于制作文档,不是很重要,忽略便是

    • @Target

    加在注解上,限定该注解的使用位置。不写的话,好像默认各个位置都是可以的。如果需要限定注解的使用位置,可以在自定义的注解上使用该注解。我们本次默认即可,不特别限定。

    • @Retention(注解的保留策略)

    注解的保留策略有三种:SOURCE/ClASS/RUNTIME

    • 注解主要被反射读取
    • 反射只能读取内存中的字节码信息
    • RetentionPolicy.CLASS指的是保留到字节码文件,它在磁盘内,而不是内存中。虚拟机将字节码文件加载进内存后注解会消失
    • 要想被反射读取,保留策略只能用RUNTIME,即运行时仍可读取

    重新运行程序,成功读取注解信息:

    注意,defaultMethod()反射得到的注解信息是:no description。就是MyAnnotion中getValue的默认值。

    但是,注解的读取并不只有反射一种途径。比如@Override,它由编译器读取(你写完代码ctrl+s时就编译了),而编译器只是检查语法错误,此时程序尚未运行。所以,我猜@Override的保留策略肯定不是RUNTIME:

    保留策略为SOURCE,仅仅是源码阶段,编译成.class文件后就消失


    属性的数据类型及特别的属性:value和数组

    属性的数据类型

    • 八种基本数据类型
    • String
    • 枚举
    • Class
    • 注解类型
    • 以上类型的一维数组

     

    value属性

    如果注解的属性只有一个,且叫value,那么使用该注解时,可以不用指定属性名,因为默认就是给value赋值:

    但是注解的属性如果有多个,无论是否叫value,都必须写明属性的对应关系:

     

    数组属性

    如果数组的元素只有一个,可以省略{}:


    小结

    • 注解就像标签,是程序判断执行的依据。比如,程序读到@Test就知道这个方法是待测试方法,而@Before的方法要在测试方法之前执行
    • 注解需要三要素:定义、使用、读取并执行
    • 注解分为自定义注解、JDK内置注解和第三方注解(框架)。自定义注解一般要我们自己定义、使用、并写程序读取,而JDK内置注解和第三方注解我们只要使用,定义和读取都交给它们
    • 大多数情况下,三角关系中我们只负责使用注解,无需定义和执行,框架会将注解类和读取注解的程序隐藏起来,除非阅读源码,否则根本看不到。平时见不到定义和读取的过程,光顾着使用注解,久而久之很多人就忘了注解如何起作用了!

    关于注解的使用案例,请参考注解(下)

    上篇讲了什么是注解,以及注解的简单使用,这篇我们一起用注解+反射模拟几个框架,探讨其中的运行原理。

    内容介绍:

    • 自定义Junit框架
    • 山寨JPA

    自定义Junit框架

    上一篇已经讲的很详细了,这里就直接上代码了。请大家始终牢记,用到注解的地方,必然存在三角关系,,并且别忘了设置保留策略为RetentionPolicy.RUNTIME。

    代码结构

    MyBefore注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface MyBefore {
    }

    MyTest注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface MyTest {
    }

    MyAfter注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface MyAfter {
    }

    EmployeeDAOTest

    //EmployeeDAO的测试类
    public class EmployeeDAOTest {
    	@MyBefore
    	public void init() {
    		System.out.println("初始化...");
    	}
    
    	@MyAfter
    	public void destroy() {
    		System.out.println("销毁...");
    	}
    
    	@MyTest
    	public void testSave() {
    		System.out.println("save...");
    	}
    
    	@MyTest
    	public void testDelete() {
    		System.out.println("delete...");
    	}
    }

    MyJunitFrameWork

    public class MyJunitFrameWork {
    	public static void main(String[] args) throws Exception {
    		// 1.先找到测试类的字节码:EmployeeDAOTest
    		Class clazz = EmployeeDAOTest.class;
    		Object obj = clazz.newInstance();
    		// 2.获取EmployeeDAOTest类中所有的公共方法
    		Method[] methods = clazz.getMethods();
    		/* 3.迭代出每一个Method对象
                         判断哪些方法上使用了@MyBefore/@MyAfter/@MyTest注解
                    */
    		List<Method> mybeforeList = new ArrayList<>();
    		List<Method> myAfterList = new ArrayList<>();
    		List<Method> myTestList = new ArrayList<>();
    		for (Method method : methods) {
    			if(method.isAnnotationPresent(MyBefore.class)){
    				//存储使用了@MyBefore注解的方法对象
    				mybeforeList.add(method);
    			}else if(method.isAnnotationPresent(MyTest.class)){
    				//存储使用了@MyTest注解的方法对象
    				myTestList.add(method);
    			}else if(method.isAnnotationPresent(MyAfter.class)){
    				//存储使用了@MyAfter注解的方法对象
    				myAfterList.add(method);
    			}
    		}
    
    		// 执行方法测试
    		for (Method testMethod : myTestList) {
    			// 先执行@MyBefore的方法
    			for (Method beforeMethod : mybeforeList) {
    				beforeMethod.invoke(obj);
    			}
    			// 测试方法
    			testMethod.invoke(obj);
    			// 最后执行@MyAfter的方法
    			for (Method afterMethod : myAfterList) {
    				afterMethod.invoke(obj);
    			}
    		}
    	}
    }

    执行结果:


    山寨JPA

    要写山寨JPA需要两个技能:注解+反射。

    注解已经学过了,反射还有一个进阶内容,之前那篇反射文章里没有提到。至于是什么内容,一两句话说不清楚。慢慢来吧。

    首先,要跟大家介绍泛型中几个定义(记住最后一个):

    • ArrayList<E>中的E称为类型参数变量
    • ArrayList<Integer>中的Integer称为实际类型参数
    • 整个ArrayList<E>称为泛型类型
    • 整个ArrayList<Integer>称为参数化的类型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是错误的。

    所以,你还有别的想法吗?

    我觉得大部分人可能都想不到,这不是技术水平高低的问题,而是知不知道API的问题。知道就简单,不知道想破脑袋也没辙。

    我们先不直接说怎么做,一步步慢慢来。

    请先看下面代码:

    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>{
    
    }

    请问,clazz.getName()打印的是A还是B?

    答案是:B。因为从头到尾,我们new的是B,这个Demo里至始至终只初始化了一个对象,所以this指向B。

    好的,到了这里,我们迈出了第一步:在泛型父类中得到了子类的Class对象!

    我们再来分析:

    class A<T>{
    	public A(){
                    //clazz是B.class
    		Class clazz = this.getClass();
    	}
    }
    
    class B extends A<String>{
    
    }

    现在我们已经在class A<T>中得到B类的Class对象。而我们想要得到的是父类A<T>中泛型的Class对象。且先不说泛型的Class对象,我们先考虑,如何获得通过子类Class对象获得父类Class对象?

    查阅API文档,我们发现有这么个方法:

    Generic Super Class,直译就是“带泛型的父类”。也就是说调用getGenericSuperclass()就会返回泛型父类的Class对象。这非常符合我们的情况。试着打印一下:

    打印发现,A<T>的Class对象是ParameterizedType的类型的。

    这里我们不去关心Type、ParameterizedType还有Class之间的继承关系,总之以我们多年的编码经验,子类对象的方法总是更多。所以毫不犹豫地向下转型:

    果然多了好几个方法,还有个getActualTypeArguments(),可以得到泛型参数~

    成了!现在我们能在父类中得到子类继承时传来的泛型的Class对象。接下来正式开始编写山寨JPA。

     

    User

    package myJPA;
    
    public class User {
    	private String name;
    	private Integer age;
    
    	public User(String name, Integer age) {
    		this.name = name;
    		this.age = age;
    	}
    
    	public String getName() {
    
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public Integer getAge() {
    		return age;
    	}
    
    	public void setAge(Integer age) {
    		this.age = age;
    	}
    }

     

    BaseDao<T>

    package myJPA;
    
    import org.apache.commons.dbcp.BasicDataSource;
    import org.springframework.jdbc.core.JdbcTemplate;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.ParameterizedType;
    import java.util.ArrayList;
    
    public class BaseDao<T> {
    
    	private static BasicDataSource datasource = new BasicDataSource();
    
    	//静态代码块,设置连接数据库的参数
    	static{
    		datasource.setDriverClassName("com.mysql.jdbc.Driver");
    		datasource.setUrl("jdbc:mysql://localhost:3306/test");
    		datasource.setUsername("root");
    		datasource.setPassword("123456");
    	}
    
    	//得到jdbcTemplate
    	private JdbcTemplate jdbcTemplate = new JdbcTemplate(datasource);
    	//泛型参数的Class对象
    	private Class<T> beanClass;
    
    	public BaseDao() {
    		/*this指代子类
                      通过子类得到子类传给父类的泛型Class对象,假设是User.class
                    */
    		beanClass = (Class) ((ParameterizedType) this.getClass()
    				.getGenericSuperclass())
    				.getActualTypeArguments()[0];
    	}
    
    	public void add(T bean) {
    		//得到User对象的所有字段
    		Field[] declaredFields = beanClass.getDeclaredFields();
    
    		//拼接sql语句,表名直接用POJO的类名
                    //所以创建表时,请注意写成User,而不是t_user
    		String sql = "insert into "
    				+ beanClass.getSimpleName() + " values(";
    		for (int i = 0; i < declaredFields.length; i++) {
    			sql += "?";
    			if (i < declaredFields.length - 1) {
    				sql += ",";
    			}
    		}
    		sql += ")";
    
    		//获得bean字段的值(要插入的记录)
    		ArrayList<Object> paramList = new ArrayList<>();
    		for (int i = 0; i < declaredFields.length; i++) {
    			try {
    				declaredFields[i].setAccessible(true);
    				Object o = declaredFields[i].get(bean);
    				paramList.add(o);
    			} catch (IllegalAccessException e) {
    				e.printStackTrace();
    			}
    		}
    		int size = paramList.size();
    		Object[] params = paramList.toArray(new Object[size]);
    
    		//传入sql语句模板和模板所需的参数,插入User
    		int num = jdbcTemplate.update(sql, params);
    		System.out.println(num);
    	}
    }

     

    UserDao

    package myJPA;
    
    public class UserDao extends BaseDao<User> {
    	@Override
    	public void add(User bean) {
    		super.add(bean);
    	}
    }

     

    测试类

    package myJPA;
    
    public class TestUserDao {
    	public static void main(String[] args) {
    		UserDao userDao = new UserDao();
    		User user = new User("hst", 20);
    		userDao.add(user);
    	}
    }

     

    测试结果

     

    桥多麻袋!这个和JPA有半毛钱关系啊!上一篇的注解都没用上!!

    不错,细心的朋友肯定已经发现,我的代码实现虽然不够完美,但是最让人蛋疼的还是:要求数据库表名和POJO的类名一致,不能忍...

    于是,我决定抄袭一下JPA的思路,给我们的User类加一个Table注解,用来告诉程序这个POJO和数据库哪张表对应:

     

    @Table注解

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Table {
    	String value();
    }

     

    新的User类(类名加了@Table注解)

    package myJPA;
    
    @Table("t_user")
    public class User {
    	private String name;
    	private Integer age;
    
    	public User(String name, Integer age) {
    		this.name = name;
    		this.age = age;
    	}
    
    	public String getName() {
    
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public Integer getAge() {
    		return age;
    	}
    
    	public void setAge(Integer age) {
    		this.age = age;
    	}
    
    }

     

    新的测试类

    package myJPA;
    
    import org.apache.commons.dbcp.BasicDataSource;
    import org.springframework.jdbc.core.JdbcTemplate;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.ParameterizedType;
    import java.util.ArrayList;
    
    public class BaseDao<T> {
    
    	private static BasicDataSource datasource = new BasicDataSource();
    
    	//静态代码块,设置连接数据库的参数
    	static{
    		datasource.setDriverClassName("com.mysql.jdbc.Driver");
    		datasource.setUrl("jdbc:mysql://localhost:3306/test");
    		datasource.setUsername("root");
    		datasource.setPassword("123456");
    	}
    
    	//得到jdbcTemplate
    	private JdbcTemplate jdbcTemplate = new JdbcTemplate(datasource);
    	//泛型参数的Class对象
    	private Class<T> beanClass;
    
    	public BaseDao() {
    		//得到泛型参数的Class对象,假设是User.class
    		beanClass = (Class) ((ParameterizedType) this.getClass()
    				.getGenericSuperclass())
    				.getActualTypeArguments()[0];
    	}
    
    	public void add(T bean) {
    		//得到User对象的所有字段
    		Field[] declaredFields = beanClass.getDeclaredFields();
    
    		//拼接sql语句,【表名从User类Table注解中获取】
    		String sql = "insert into "
    				+ beanClass.getAnnotation(Table.class).value() 
    				+ " values(";
    		for (int i = 0; i < declaredFields.length; i++) {
    			sql += "?";
    			if (i < declaredFields.length - 1) {
    				sql += ",";
    			}
    		}
    		sql += ")";
    
    		//获得bean字段的值(要插入的记录)
    		ArrayList<Object> paramList = new ArrayList<>();
    		for (int i = 0; i < declaredFields.length; i++) {
    			try {
    				declaredFields[i].setAccessible(true);
    				Object o = declaredFields[i].get(bean);
    				paramList.add(o);
    			} catch (IllegalAccessException e) {
    				e.printStackTrace();
    			}
    		}
    		int size = paramList.size();
    		Object[] params = paramList.toArray(new Object[size]);
    
    		//传入sql语句模板和模板所需的参数,插入User
    		int num = jdbcTemplate.update(sql, params);
    		System.out.println(num);
    	}
    }

    这下真的是山寨JPA了~

  • 相关阅读:
    Linux虚拟机的安装(使用Centos6.3)
    【转载】接口测试用例的设计原则
    Oracle PLSQL游标、游标变量的使用
    利用shell脚本将Oracle服务器中数据定时增量刷新到ftp服务器中
    源码安装rlwrap 0.43(为了方便使用linux下的sqlplus)
    Oracle自定义脱敏函数
    Oracle分析函数FIRST_VALUE、LAST_VALUE
    MYSQL性能测试工具SYSBENCH
    OEL7.6源码安装MYSQL5.7
    OEL7.6安装Oracle Database 19C(VERSION 19.3.0.0)
  • 原文地址:https://www.cnblogs.com/ym65536/p/12829814.html
Copyright © 2011-2022 走看看