小言_互联网的博客

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

395人阅读  评论(0)



说明

因为是个人复习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(“全类名包括包名”)
  2. 针对已存在的类使用 类名.class.
  3. 通过对象获取类对象,getClass()方法继承自Object 对象.getClass()

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


Class中的基本方法

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

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

  返回包含一个数组 Field对象反射由此表示的类或接口的所有可访问的public字段 类对象。

Field  getField(String name)

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

Field[]  getDeclaredFields()

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

Field  getDeclaredField(String name)

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


总结:

方法 成员清单 继承成员 私有成员
getDeclaredFields() no no yes
getField(String name) no yes no
getDeclaredField(String name) yes no yes
getFields() yes yes no

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

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

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

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

  1. 获取类名
String geName()

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

  Class类中其实还有一个newInstance()方法,用来创建类的实例.但是这个方法在jdk9中就不推荐使用了.这个方法存在一些限制,这个方法只能调用类的无参构造器并且使用之前必须保证这个类已经加载.
  所以说这个方法只适合在放射机制中使用,如果在接口中使用,那么就必须加载接口的实现类,那就太麻烦.

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


.properties

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


针对上面反射内容的案例

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

  那么分析这个案例,首先需要通过配置文件.properties将可以调用的全类名和方法名写入,然后通过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来加载类,只是这个方法还可以指定一些参数.
  到这里简单的介绍就结束了,之后其实还有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           描述注解是否被子类继承


解析注解

  也就是上面提到的第三种的使用方式,下面通过上面提到的针对反射的案例进行解释
  需要写一个”框架”,在不改变”框架”内任何代码的情况下,使用这个”框架”创建任意类的对象,并执行其中的任意方法.

//注解
@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);
    }
    @Check
    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.flush();
        bufferedWriter.close();
    }
}

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



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


转载:https://blog.csdn.net/weixin_43913376/article/details/106130170
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场