java学习(15)-注解与反射



说明

因为是个人复习java的总结,所以结构稍显杂乱,有些语句过于口语化.
接下来的学习会尽量增加代码的思考,光看理论终究是虚的.


JUnit测试

  具体的测试的知识就不深入了,因为之前已经学习过,下次有时间再考虑整理一下黑盒测试,白盒测试,版本控制.
  我使用的是eclipse,如果单纯进行JUnit测试可以直接在eclipse中创建JUnit Test Case来实现,eclipse自带JUnit3和4,会有提示导入.如果需要更高版本就需要去下jar包然后导入使用.

  使用中一般将源代码和测试代码分开,也就是分为main包和test包,
  JUnit最常使用的方法就是assertEquals(expect, actual),可以对传入的预期和实际进行判断,如果正确就通过,如果错误就报错.

  但是实际测试如果要每个案例都写程序去判断就太麻烦了,所以下面上传一个案例,使用maven来解析Json文件然后在JUnit中对获取的Json文件中的内容进行判断.这样可以有效减少代码书写.仅对初学者提供参考.
  maven的话eclipse也有自带,直接创建项目就可以使用,具体资源会上传在下面

              可以下载看一下,纯粹分享,没有积分要求

  JUnit中有@Before@After注解,其实就是一般用来保证资源获取和释放的.@Before标记之后会在测试运行之前运行,从而获取资源.@After则是在程序要结束时运行,不管JUnit是否通过所有断言,都会运行,用来释放资源.


反射

  这个概念在框架的设计中十分重要.其概念是将类的各个组成部分封装成其他对象.其实就是说在编程过程中,你通过类调用到其中的方法,通过类的对象调用到方法方法的这个过程中的实现原理.其实就是将类中的组成部分变成对象,然后通过对象实现类调用方法或者对象调用方法的功能.
  换种说法,在堆中的对象,里面存储的其实是其他的对象,里面的方法可能是一个数组对象,因为实际存的是地址,那么这些地址从概念上讲其实就是存在一个数组对象中.
  就是将类中抽象的方法具体到对象或者类中具体的对象上,就是反射机制.

  看图更容易理解,就是编译成字节码之后,类中内容被加载成一个个具体对象的过程,叫反射.

在这里插入图片描述


获取Class的类对象

  其实意思不是获取类的对象,而是说获取这个类对象.因为在反射中,可以把加载进jvm的这个类看作是一个对象,类对象中存储的是关于类的信息,而如何获取到这个类对象就有以下三种方式:

  1. 针对未加载的类使用,加载类并返回类对象
Class.forName(“全类名包括包名”)
  1. 针对已存在的类使用
类名.class.
  1. 通过对象获取类对象,getClass()方法继承自Object
对象.getClass()

  并且类其实就是加载在方法区,是唯一一个,所以上面三种方式获取到的类对象会是同一个对象,而这个对象不是实例化后堆中的那个对象.
  注意区分类的对象是在堆中的,这里类对象表达的其实相当于是方法区中类记录的自己的地址叫类对象.


Class中的基本方法

在获取类之后肯定需要使用类中的方法,那就可以使用以下的方法获取一些必要的信息.

  1. 获取成员变量的方法
Field[]  getFields()

  返回包含一个数组 Field对象反射由此表示的类或接口的所有可访问的public字段 类对象。获取之后一般使用foreach进行循环,然后操作.

Field  getField(String name)

  返回一个 Field对象,它反映此表示的类或接口的指定public成员字段 类对象。
  可以获取父类和子类的公有域

Field[]  getDeclaredFields()

  返回的数组 Field对象反映此表示的类或接口声明的所有字段 类对象。

Field  getDeclaredField(String name)

  返回一个 Field对象,它反映此表示的类或接口的指定已声明字段 类对象。
  不能获取父类的部分


总结方法的获取范围:

方法成员清单继承成员私有成员
getDeclaredFields()nonoyes
getField(String name)noyesno
getDeclaredField(String name)yesnoyes
getFields()yesyesno

  其实可以强行获取不是public修饰的成员变量,在访问这个变量或者说是字段前使用setAccessible(true),强行忽略权限修饰.这种方式也叫做暴力反射.

  1. 获取构造方法
Constructor<T>  getConstructor(class<?>... parameterTypes)
Constructor<?>[]  getConstructors()

根据获取的构造器不同,创建对象时调用的newInstance()方法传入的参数也不同.

参照上面模式还有两个方法,这里省略了.

  1. 获取成员方法
Method getMethod(String name,<?>... parameterTypes)
Method getMethods()

  1. 获取类名
String geName()

具体的方法在api中都有,这里就列举一下

  Class类中其实还有一个newInstance()方法,用来创建类的实例.但是这个方法在jdk9中就不推荐使用了.这个方法存在一些限制,这个方法只能调用类的无参构造器并且使用之前必须保证这个类已经加载.区别于上面构造器的newInstance(),构造器的是可以根据获取的构造器来使用的.

  对于上面的内容实际使用的时候肯定不可能每个类名或者说每个方法名都是在程序中写,那就可以考虑上面提到的Json实现JUnit测试的方式,可以使用java中的.properties配置文件来实现读取文件中的类名和方法名.


.properties

  是java中的配置文件,格式为文本文件,其中的内容是使用”键=值”的格式来书写的.可以使用#作为注释符号.


针对上面反射内容的案例

  需要写一个”框架”,在不改变”框架”内任何代码的情况下,使用这个”框架”创建任意类的对象,并执行其中的任意方法.

  那么分析这个案例,首先需要通过配置文件.properties将可以调用的全类名和方法名写入,然后通过properties类用流将配置文件读入,然后根据反射读取选定的类,最后创建对象并执行方法.

  实现如下

public static void main(String[] args) throws Exception {
	Properties properties = new Properties();
	//获取本类的类加载器
	ClassLoader classLoader = Test.class.getClassLoader() ;
	//通过类加载器将文件转换为输入流
	InputStream is = classLoader.getResourceAsStream("pro.properties");
	//将输入流以键值对形式加载
	properties.load(is);
	
	String className = properties.getProperty("className");			//全类名
	String methodName = properties.getProperty("methodName");		//方法名
	
	//找到类创建对象并使用方法
	Class class1 = Class.forName(className);
	Object obj = class1.newInstance();
	Method m = class1.getMethod(methodName);
	m.invoke(obj);
	
}

  通过案例可以再巩固一下对于反射的理解和使用,但是真实的框架不可能这么简单,但是可以了解大概,将实现功能的代码进行封装,只对外留下简单的使用.


ClassLoader

  上面内容中用到了这个类加载器,这里进行简单的说明.其实是一个抽象类用来加载类,也就是用来解析.class文件形成jvm中的类对象.

  jvm中有三个重要的类加载器BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader.

  BootstrapClassLoader
  负责加载 JVM 的核心类

  ExtensionClassLoader
  负责加载 JVM 的扩展类

  AppClassLoader
   是直接面向我们编程的加载器,我们写的和配置的其实都是通过这个类加载器来加载的

  看到这里突然想起来数据库加载的时候使用的是Class.forName(),然后查了一下,发现forName()方法其实也是使用ClassLoader来加载类,只是Class.forName()方法还可以指定一些参数.

  到这里简单的介绍就结束了,之后其实还有ClassLoader的原理还有三种类加载器之间的分工合作.这些深入内容暂时不提,等虚拟机更深入学习后再来深入.


注解

其实就是提供给计算机的程序说明,基本上分为以下几类功能的注释

  1. 编译检查,之前提到过的比如继承的@Override还有检查函数式接口的
  2. 编写文档,其实很常见就是注释中经常自动补的@param @return @author @version @since,这些注释在程序运行中不会有效果,但是在使用javadoc命令中可以将这些注释抽取成一个文档,最终形成的样式和jdk文档一样.
    上面个两类基本都是预编译好的,我们无法修改,我们主要使用的是下面这种方式
  3. 代码分析:可以跟踪代码依赖性实现代替配置文件的功能,现在Spring中使用的很多


基本的预定义注解

  @Override
  检测是否是继承自父类或接口

  @Deprecated
  标识内容已过时,主要如果修改就可能影响整个类,只能提示一下使用者

  @SuppressWarnings(“all”)
  用来压制警告信息,一般传递all来压制所有警告


自定义注解

  那肯定是看源代码先了解一下格式,下面是@Override的源码

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

  总结格式如下

public @interface 注解名{
}

  接下来了解注解的实质,其实就是一个接口,一个继承了注解类的接口

public interface 注解名 extends java.lang.annotation.Annotation{}

注解有两点注意事项:

  1. 注解中可以定义抽象方法又称为属性,但是抽象方法的返回值只能是基本数据类型,String,枚举,注解,或者上面这些类型的数组
  2. 如果定义了属性,那么使用的时候需要给属性赋值,如果只有一个属性且为value,可以直接在注解后面传入参数,上面@ SuppressWarnings(“all”)就是.但是如果不满足前面的条件就需要使用属性名=属性值的方式给注解赋值.除非定义属性是跟了default 默认值,不然每个属性都需要赋值.


枚举

  实质上一个特殊类,其中的常量都是public static final的
  主要用处就是当需要只能在一堆量中选择一个使用的时候,可以使用这种方式定义.

enum Color {
  RED, BLUE, GREEN;
}

  因为本质是一个类,其中也可以定义方法



元注解

  其实就是用来描述注解的注解
  @Target(ElementType)
  描述注能够作用的位置

  @Retention(Rententionpolicy)
  描述注解被保留的阶段

  @Documented()
  描述注解是否被抽取到api中

  @Inherited
  描述注解是否被子类继承

  其中具体的属性可以到api中查询对应的枚举


解析注解

  也就是上面提到的第三种的使用方式,下面通过上面提到的针对反射的案例进行解释

  需求是写一个”框架”,在不改变”框架”内任何代码的情况下,使用这个”框架”创建任意类的对象,并执行其中的任意方法.

//注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro {
	String className();
	String methodName();
}

//测试类
//这里不要关注我的包名,随便找了个包做的实验
@Pro(className = "image_edge.Human", methodName = "sayHello")
public class Test {
	public static void main(String[] args) throws Exception {
		Class<Test> class1 = Test.class;
		//这里其实是创建了一个Pro的实现类,重写了其中的方法
		Pro an = class1.getAnnotation(Pro.class);
		
		String className = an.className();
		String methodName = an.methodName();
		
		Class class2 = Class.forName(className);
		Method method = class2.getMethod(methodName);
		Object obj = class2.newInstance();
		method.invoke(obj);
	}
}

总结地说就是通过以下几个步骤来实现获取注解的属性值,也就是替代配置

  1. 获取注解定义的位置的对象,上面案例中就是当前的class
  2. 获取指定注解的实现类getAnnotation()方法
  3. 调用实现类中重写的抽象方法获取需要的信息


下面再用一个测试框架的案例巩固一下

  需求就是使用注解来对某个类中的方法进行测试.具体实现如下:

//需要测试的类
public class Caculator {
    @Check
    public void add(){
        System.out.println(1+0);
    }
    @Check

    public void sub(){
        System.out.println(1-0);
    }
    @Check
    public void mul(){
        System.out.println(1*0);
    }
    @Check
    public void div(){
        System.out.println(1/0);
    }

    public void show(){
        System.out.println("end");
    }
}
//自定义注解
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)	//定义注解保留阶段为运行时
@Target(ElementType.METHOD)			//定义注解效用位置为方法
public @interface Check {

}
//测试类
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;

public class Test {
    public static void main(String[] args) throws IOException {
    	//获取计算类对象用于调用方法
        Caculator caculator = new Caculator();
        //获取类对象
        Class cla = caculator.getClass();
        //获取方法列表
        Method[] methods = cla.getMethods();
        //出现异常次数记录
        int wrongNum = 0;
        //使用流将异常信息输入到文件中
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("bug.txt"));
        //循环方法列表
        for (Method method : methods) {
        	//判断是否有Check注解标记
            if(method.isAnnotationPresent(Check.class)){
                try {
                	//调用方法
                    method.invoke(caculator);
                //捕获异常
                }catch (Exception e){
                	//异常信息记录
                    wrongNum++;
                    bufferedWriter.write(method.getName()+"方法出错");
                    bufferedWriter.newLine();
                    bufferedWriter.write("异常名称为"+e.getCause().getClass().getSimpleName());
                    bufferedWriter.newLine();
                    bufferedWriter.write("异常原因"+e.getCause());

                }
            }
        }
        
        bufferedWriter.write("一共出现"+wrongNum+"次异常");
        
        bufferedWriter.close();
    }
}

  其中主要的部分就是获取方法并判断是否存在注解标识,然后再对注解标识的方法进行测试,其实也就是使用注解分析.

  但是仔细深究又会发现,这个案例如果直接用对象去试方法也差不多,但是要考虑这只是简单的案例,如果方法增多,使用反射获取其中的方法列表然后进行测试是很有必要的.主要加深对于反射和注解的理解,具体使用需要在编程中体会.



如有错误欢迎读者批评指正!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值