反射和注解总结及使用例子

前言:

姑且算作三月的温故而知新吧 hhhh,注解在我们日常工作使用中可谓是用的很多了,然而反射却很少,因为反射一般都是在写基本框架时用的比较多,如果能更好的理解这两快其实对于我们学习是很有帮助,这里就再次回顾一下。btw 在讲反射之前,我们可以先了解下什么是静态编译与动态编译,这有助于我们自然而然的引出反射。

 

静态编译与动态编译

静态编译:一次性编译,在编译的时候把所有的模块全部编译进去。其实项目中一般都是这种情况,因为动态编译在实际中很不可取,下面举个实例:

public class ReflectTest {
    // 在启动时即将该方法用到的所有类全部编译
    public static void main(String[] args) {
        StaticClassTest staticClassTest = new StaticClassTest();
        staticClassTest.say();
    }
}

public class StaticClassTest {

    public void say() {
        System.out.println("hello");
    }
}

如果把编译的StaticClassTest.class文件删除(在target目录下),再次运行即会报NoClassDefFoundError异常,只能重新编译再运行,下面看下动态编译的过程。

动态编译:按需编译,程序在运行的时候,需要哪个模块就编译哪个模块,下面先提前以反射的方式实现动态编译

    public static void main(String[] args) {
        System.out.println("请输入类的全路径,这里为了展现这种思想请填写指定的类型");
        Scanner scanner = new Scanner(System.in);
        String s = scanner.next();
        try {
            // 加载这个类
            Class c = Class.forName(s);
            // 获得这个类的实例化对象
            Object obj = c.newInstance();
            // 强制类型转换
            StaticClassTest staticClassTest = (StaticClassTest) obj;
            staticClassTest.say();
        } catch (Exception e) {

        }
    }

输出结果:反射和注解总结及使用例子_第1张图片

然后当你把StaticClassTest.class文件删除之后(这里注意备份该class文件):

1、运行该方法并填入指定信息,此时必然还是报NoClassDefFoundError异常,与静态编译类型

2、再次运行该方法,然后恢复class文件,填入指定类信息,此时编译通过~

这样我们就可以在程序运行的情况下,动态的加载一个类,这里充分的体现了java的动态性。到这里静态编译与动态编译的区别应该很明显啦,不过在项目中还是不要用的好,因为:

a、存在安全问题,属于"注入漏洞",只要上传一个恶意的java程序就能让我们的安全工作毁于一旦

b、在高性能项目中也不要用,毕竟动态编译需要一个编译过程,且比静态编译多了一个执行环节

 

反射

有了上面静态编译与动态编译的入门,这里对反射的概念应该很好理解了,即在程序运行的过程中,能知道任何一个类及它的属性和方法,并且能够任意调用它的属性和方法。这种在程序运行时的动态获取机制就是java的反射机制,上面动态编译的例子其实就是运用了反射,下面讲下反射的常用api及使用方式。

1、利用反射获取Class对象

在反射中,要获取一个类的属性及方法,首先我们得先获取其Class对象,大致上有三种,d存在先决条件,b和c两种方式适合在编译前就知道要操作的Class

a、使用Class.forName()方法,通过传入类的全路径以获取该Class对象,上面动态编译的例子即是采用这种方式

Class clz = Class.forName("service.StaticClassTest");

b、使用类的.class方法获取

Class clz = StaticClassTest.class; 

c、使用类对象的getClass()方法

StaticClassTest staticClassTest = new StaticClassTest();
Class clz = staticClassTest.getClass();

d、如果是基本类型的包装类,可以通过调用包装类的Type属性来获取其Class对象

Class clz = Integer.TYPE;

2、通过反射创建类对象

a、利用Class对象的newInstance()方法,上面动态编译的例子即是采用这种方式,调用的是该对象的默认无参构造方法

Class c = StaticClassTest.class;
StaticClassTest staticClassTest = (StaticClassTest) c.newInstance();

b、利用Constructor对象的newInstance()方法,好处是可以指定特定的构造方法创建类对象,看下面demo

Class clz = StaticClassTest.class;
Constructor constructor = clz.getConstructor(String.class, Integer.class);
StaticClassTest staticClassTest = (StaticClassTest) constructor.newInstance("小明", 25);

3、利用反射获取类的属性、方法及构造器

a、通过调用Class对象的getFields()获取class类的公有属性,但无法获取私有属性

@Data
public class StaticClassTest {

    private String id;
    private String name;
    private Integer age;
    public String address;

    public StaticClassTest(String id, Integer age) {
        this.id = id;
        this.age = age;
    }

    public void say() {
        System.out.println("hello");
    }
}

    @Test
    public void test02() {
        try {
            Class clz = StaticClassTest.class;
            Field[] fields = clz.getFields();
            for (Field field : fields) {
                System.out.println(field); // public java.lang.String service.StaticClassTest.address
            }
        } catch (Exception e) {
            log.error("reflect error");
        }
    }

b、通过调用getDeclaredFields()获取class类的所有属性,包括私有属性在内

    @Test
    public void test02() {
        try {
            Class clz = StaticClassTest.class;
            Field[] fields = clz.getDeclaredFields();
            for (Field field : fields) {
                System.out.println(field);
            }
        } catch (Exception e) {
            log.error("reflect error");
        }
    }

输出如左图:反射和注解总结及使用例子_第2张图片

获取类的方法及构造器与获取属性类似,如果想要获取私有方法或私有构造器,则必须使用含有 declared 关键字的方法。

 

注解

之所以把注解和反射一起讲是因为在实际开发过程中,反射其实用的并不多,一般都是和注解联合使用,这里先把注解相关知识拎出来复习巩固下。

1、注解的概念

注解又叫元数据,提供了一种安全的类似注释的机制,是自jdk1.5以后所引入的一个作用在代码层面的说明,与类、接口、枚举是在同一个层次,以@interface关键词表示。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释,可以起到减少配置的成果,给程序起到辅助性的作用

2、注解的作用,其实很好理解,对应着注解的三种作用域分类,下面会讲~

a、标记作用,用于告诉编译器一些信息让编译器做一些基本的格式检查,如@Override,@Deprecated,@SuppressWarnings

插一句题外话,@SuppressWarnings最常抑制的两种警告类型分别是deprecation和unchecked。

b、编译时动态处理,动态生成代码,如Butter Knife、Dagger 2

c、运行时动态处理,获得注解信息,如Retrofit

3、注解的分类,有两种分法,第一种就是根据注解的类型分,一般可分为如下3中,另外一种分法是根据注解的作用域分,可分为源码时注解、编译时注解、运行时注解,对应于上面注解的三种作用,也对应于下面@Retention元注解所示的在哪种级别保存该注解信息

a、基本内置注解,是指Java自带的几个Annotation,如@Override、@Deprecated、@SuppressWarnings等

b、元注解(meta-annotation),是指负责注解其他注解的注解,JDK 1.5及以后版本定义了4个标准的元注解类型,如下:

1、@Target
2、@Retention
3、@Documented
4、@Inherited

c、自定义注解,根据需要可以自定义注解,自定义注解需要用到上面的元注解

4、元注解相关信息

a、@Target:指注解所修饰的对象范围,通过ElementType取值有8种,如下:

TYPE:类、接口(包括注解类型)或枚举
FIELD:属性
METHOD:方法
PARAMETER:参数
CONSTRUCTOR:构造函数
LOCAL_VARIABLE:局部变量
ANNOTATION_TYPE:注解类型
PACKAGE:包

b、@Retention:指注解被保留的时间长短,通过RetentionPolicy取值有3中,如下:

SOURCE:在源文件中有效(即源文件保留)  
CLASS:在class文件中有效(即class保留)  
RUNTIME:在运行时有效(即运行时保留)

c、@Documented:是一个标记注解,表示被标注的注解应该javadoc工具记录,默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了@Documented,则它会被 javadoc 之类的工具处理,,所以注解类型信息也会被包括在生成的文档中

d、@Inherited:也是一个标记注解,表示某个被标注的注解是可以被继承的

 

自定义注解的应用

注解几乎无处不在,有注解的地方就有反射调用,自定义注解最常用的地方应该为通过自定义注解实现aop把,这个例子以前我好像写过一篇,传送门:一个最简单的通过自定义注解形式实现AOP的例子

 

由反射+注解联想IOC+DI

其实看完反射+注解的记录后,到这里应该对Spring的控制反转和依赖注入有了更好的理解

IOC:将对象的创建权交给容器,我们无需在程序中主动去创建对象,spring的xml配置文件中的标签及一系列使用了全路径的类名,全部都是通过反射的方式(Class.forName())创建对象,这也算是反射在框架中的一个应用,不过用的更多的地方应该在动态代理中。

DI:简单来说就是由容器动态的将某个依赖关系注入到组件之中,spring的依赖注入大致分为xml配置注入(属性注入、构造函数注入、工厂方法注入)和注解方式注入(如@Autowired等),注解注入在我们日常工作中可谓是平常的不能再平常了,即使我们不使用第三方注入,日常代码中也有依赖注入的影子,如下:

public class Human {
    ...
    Father father;
    ...
    public Human(Father father) {
        this.father = father;
    }
}

上面代码中,我们将 father 对象作为构造函数的一个参数传入,在调用 Human 的构造方法之前外部就已经初始化好了 Father 对象。像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入。

 

反射+注解实际使用

其实反射在实际开发中用的并不算多,甚至能避免就避免,因为好多人都吐槽反射的效率不高代码可读性差等等balabala,但其实如果在某些动态的业务需求下使用反射+注解也是可行的,其带来的所谓性能问题可以忽略不计,

a、在方法上使用注解,适合在同一个类下多种实现

下面我们模拟第一个需求场景,练习一下反射+注解知识,以分页查询为例,根据前端传来的某个查询请求参数(例如只想要分页列表参数page,某种类型数据总数count),根据注解+反射在service层动态调用logic层接口,这样可以做到针对不同的查询方式,service层只需要一个接口就够了,逻辑可以都放在logic层,先讲下大致思路:

在进程启动时作一个初始化操作,将那些被注解所标识的接口方法全部放到一个指定map中,然后在所需要的地方直接通过反射调用,下面看下代码实现:

1) 先看自定义的注解

@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Target(ElementType.METHOD) // 作用在方法
@Documented // 被javadoc工具所记录
public @interface ScoreAno {
    /**
     * 查询方式枚举
     */
    enum QueryType{COUNT, PAGE}

    /**
     * 查询类型,根据业务来定
     * @return
     */
    String filterType();

    /**
     * 查询方式,这里为分页和总数
     * @return
     */
    QueryType queryType();
}

2) logic层实现类中的方法,即所需要扫描的类

    // 该类中可以存放针对多种场景下的不同接口形式
    @ScoreAno(filterType = "test", queryType = ScoreAno.QueryType.PAGE)
    // PageResult一般为封装好的分页专用类,包含list,total,pageNum,pageSize
    public PageResult query (String id, String name) {
        // 这里为你所需要的业务逻辑
        PageResult pageResult = new PageResult<>();
        pageResult.setList(Lists.newArrayList(new User(id, name)));
        pageResult.setTotal(1);
        return pageResult;
    }

3) 注解管理类中特定属性及方法,以及模拟service层调用逻辑

    /**
     * 存放指定类下的方法集
     */
    private static Map methodMap = new HashMap<>();
    /**
     * 需要扫面的类数组
     */
    private static Class[] classes = new Class[] {ScanTest01.class};
        
        // 上文所说的在进程启动时调用可用@PostConstruct注解实现
        // 扫描指定class下所有被指定注解所标识的方法
        for (Class clz : classes) {
            Method[] methods = clz.getMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(ScoreAno.class)) {
                    Annotation[] annotations = method.getAnnotations();
                    for (Annotation annotation : annotations) {
                        if (!(annotation instanceof ScoreAno))
                            continue;
                        ScoreAno scoreAno = (ScoreAno) annotation;
                        QueryType queryType = new QueryType(scoreAno.filterType(), scoreAno.queryType());
                        methodMap.put(queryType, method);
                    }
                }
            }
        }

        // 这里演示在service层通过反射调用
        Method method = getMethod("test", ScoreAno.QueryType.PAGE);
        try {
            PageResult result = (PageResult) method.invoke("123", "小明");

        } catch (IllegalAccessException | InvocationTargetException e) {

        }


    // 封装获取指定方法
    public Method getMethod(String fielterType, ScoreAno.QueryType queryType) {
        return methodMap.get(new QueryType(fielterType, queryType));
    }

    /**
     * 内部类
     */
    private class QueryType {

        private String fielterType;
        private ScoreAno.QueryType queryType;

        public QueryType(String fielterType, ScoreAno.QueryType queryType) {
            this.fielterType = fielterType;
            this.queryType = queryType;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof QueryType)) return false;
            QueryType queryType1 = (QueryType) o;
            return Objects.equals(fielterType, queryType1.fielterType) &&
                    queryType == queryType1.queryType;
        }

        @Override
        public int hashCode() {
            return (fielterType + "_" + queryType.toString()).hashCode();
        }
    }

b、在类上使用注解,一般适用在某个类只做特定的事情,比如某个类只做发送短信,某个类只做restful调用等等,思路其实与上面差不多,只是获取方式不同。这里需要将特定的类放在某个指定的包路径下,然后将该路径定义在注解管理类中,通过init初始化方法扫描该包下所有的class类,后面的操作与上面几乎一样,只是最后在map中存的不是方法而是class对象了,毕竟该类只做同一件事嘛~ 下面贴下我在测试时所找到的扫面某个包路径下的所有class工具方法:

    /**
     * @param intface 需要继承的接口类
     * @param packageName 包名
     * @param scanSubPackage 是否扫描子包
     */
    public static List getAllClassByInterface(Class intface, String packageName, boolean scanSubPackage) {
        List returnClassList = new ArrayList<>();
        if(!intface.isInterface()){
            throw new IllegalArgumentException("class must interface.");
        }

        List> allClass = getClasses(packageName, scanSubPackage);
        for (Class allClas : allClass) {
            if (intface.isAssignableFrom(allClas)) {
                if (!intface.equals(allClas)) {
                    returnClassList.add(allClas);
                }
            }
        }
        return returnClassList;
    }

    public static List> getClasses(String packageName, boolean scanSubPackage) {
        //第一个class类的集合
        List> classes = new ArrayList<>();
        //获取包的名字 并进行替换
        String packageDirName = packageName.replace('.', '/');
        //定义一个枚举的集合 并进行循环来处理这个目录下的things
        Enumeration dirs;
        try {
            dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
        } catch (IOException e) {
            throw new IllegalArgumentException("packageName is illegal.", e);
        }
        //循环迭代下去
        while (dirs.hasMoreElements()) {
            //获取下一个元素
            URL url = dirs.nextElement();
            //得到协议的名称
            String protocol = url.getProtocol();
            //如果是以文件的形式保存在服务器上
            if ("file".equals(protocol)) {
                //获取包的物理路径
                String filePath = null;
                try {
                    filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                //以文件的方式扫描整个包下的文件 并添加到集合中
                findAndAddClassesInPackageByFile(packageName, filePath, scanSubPackage, classes);
            } else if ("jar".equals(protocol)) {
                //如果是jar包文件
                //定义一个JarFile
                JarFile jar = null;
                try {
                    //获取jar
                    jar = ((JarURLConnection) url.openConnection()).getJarFile();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                //从此jar包 得到一个枚举类
                Enumeration entries = jar.entries();
                //同样的进行循环迭代
                while (entries.hasMoreElements()) {
                    //获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
                    JarEntry entry = entries.nextElement();
                    String name = entry.getName();
                    //如果是以/开头的
                    if (name.charAt(0) == '/') {
                        //获取后面的字符串
                        name = name.substring(1);
                    }
                    //如果前半部分和定义的包名相同
                    if (name.startsWith(packageDirName)) {
                        int idx = name.lastIndexOf('/');
                        //如果以"/"结尾 是一个包
                        if (idx != -1) {
                            //获取包名 把"/"替换成"."
                            packageName = name.substring(0, idx).replace('/', '.');
                        }
                        //如果可以迭代下去 并且是一个包
                        if ((idx != -1) || scanSubPackage) {
                            //如果是一个.class文件 而且不是目录
                            if (name.endsWith(".class") && !entry.isDirectory()) {
                                //去掉后面的".class" 获取真正的类名
                                String className = name.substring(packageName.length() + 1, name.length() - 6);
                                try {
                                    //添加到classes
                                    classes.add(Class.forName(packageName + '.' + className));
                                } catch (ClassNotFoundException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                }
            }
        }
        return classes;
    }

    private static void findAndAddClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, List> classes) {
        //获取此包的目录 建立一个File
        File dir = new File(packagePath);
        //如果不存在或者 也不是目录就直接返回
        if (!dir.exists() || !dir.isDirectory()) {
            return;
        }
        //如果存在 就获取包下的所有文件 包括目录
        File[] dirfiles = dir.listFiles(new FileFilter() {
            //自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
            public boolean accept(File file) {
                return (recursive && file.isDirectory()) || (file.getName().endsWith(".class"));
            }
        });
        //循环所有文件
        for (File file : dirfiles) {
            //如果是目录 则继续扫描
            if (file.isDirectory()) {
                try {
                    findAndAddClassesInPackageByFile(packageName + "." + file.getName(),
                            file.getCanonicalPath(),
                            recursive,
                            classes);
                } catch (IOException e) {
                    log.warn("get folder error");
                }
            }
            else {
                //如果是java类文件 去掉后面的.class 只留下类名
                String className = file.getName().substring(0, file.getName().length() - 6);
                try {
                    //添加到集合中去
                    classes.add(Class.forName(packageName + '.' + className));
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
    }

 

反射在动态代理中的应用

其实本质上还是对method.invoke()方法的调用,如果目标对象没有实现接口则需使用Cglib动态代理,反之则可以使用JDK动态代理,根据项目中实际需求及场景选择不同的实现方式即可。关于代理模式日后可能会单独再总结一篇 ~

 

后话

更好的理解反射+注解这一块对于看源码是有很大的帮助的,对于学习新的框架也能更快一步

你可能感兴趣的:(java)