美文网首页其他零散知识点
Java反射-2(技巧)

Java反射-2(技巧)

作者: 小胖学编程 | 来源:发表于2019-09-26 18:39 被阅读0次

    Java反射-1(理论)
    Java反射-2(技巧)

    Java语言是把Java源文件编译成后缀为class的字节码文件,然后再通过ClassLoader机制把这些文件加载到内存中,最后生成实例执行的,这是Java处理的基本机制。

    1.1 Class对象

    Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象。

    Class类是Java的反射入口,只有在获取一个类的描述对象后才能动态的加载、调用,一般来说,获取一个Class对象有三种途径。

    • 类属性方式:如String.class。
    • 对象的getClass方法:如new String().getClass()。
    • forName方法加载,如Class.forName("java.lang.String")。

    获取到Class对象后,就可以通过getAnnotations()获取注解,通过getMethods获取方法,通过getConstructors()获取构造函数等,为后续反射代码铺平道路。

    1.2 适当选择getDeclaredXXX和getXXX

    Java的Class类提供了很多的getDeclaredXXX方法和getXXX方法,例如getDeclaredMethod和getMethod成对出现,getDecaredConstructors和getConstructors也是成对出现,那么这两者的区别是哪些?

    public class Foo {
    
        void doStuff() {
    
        }
    
        public static void main(String[] args) throws NoSuchMethodException {
            String methodName="doStuff";
            Method declaredMethod = Foo.class.getDeclaredMethod(methodName);
            Method method = Foo.class.getMethod(methodName);
        }
    }
    

    返回结果是method没有找到doStuff结果。

    getXXX方法获取的是所有public访问级别的方法,包括在父类继承的方法,而getDeclaredXXX方法获取的是自身类的所有方法(不受限于访问权限)。

    1.3 反射访问属性或方法时,将Accessible设置为true

    Java中通过反射执行一个方法的过程如下:

    public class Foo {
        private void doStuff() {
            System.out.println("hello world");
        }
    }
    
    public class TestFoo {
    
        @Test
        public void test() throws Exception {
            String methodName = "doStuff";
            Method declaredMethod = Foo.class.getDeclaredMethod(methodName);
            //由开发者决定是否要逃避安全体系的检查(是否可以快速获取)
            System.out.println(declaredMethod.isAccessible());
            if(declaredMethod.isAccessible()){
                declaredMethod.setAccessible(true);
            }
            declaredMethod.invoke(new Foo());
        }
    }
    

    实际上,在通过反射执行方法时,必须在invoke之前检查Accessible属性。但是方法对象的Accessible属性并不是用来决定是否可以访问的。

    public class Foo {
    
        public void doStuff() {
            System.out.println("hello world");
        }
    
        public static void main(String[] args) throws NoSuchMethodException {
            String methodName = "doStuff";
            Method declaredMethod = Foo.class.getDeclaredMethod(methodName);
            System.out.println(declaredMethod.isAccessible());
        }
    }
    

    实际上,即使反射的方法访问修饰符为public,isAccessible()的最终结果依旧是false。而且即使为false,还是可以使用invoke()方法执行。

    故Accessible的属性并不是我们语法层次理解的访问权限,而是指是否更加容易获得,是否进行安全检查。

    我们知道,动态修改一个类或者执行方法都会受到java安全体系的制约,而安全处理是非常消耗资源的(性能非常低),因此对于运行期要执行的方法或要修改的属性提供了Accessible可选项:由开发者来决定是否逃避安全体系的检查。

    在源码java.lang.reflect.AccessibleObject#isAccessible中,该方法默认返回值为false。

    AccessibleObject是Field、Method、Constructor的父类,决定其是否可以快速访问而不进行访问控制检查。在AccessibleObject中是以override变量保存该值的,但是具体是否快速执行是在Method类的invoke方法中决定的。

        public Object invoke(Object obj, Object... args)
            throws IllegalAccessException, IllegalArgumentException,
               InvocationTargetException{
            //检查是否可以快速获取,其值是父类的AccessibleObject的override变量
            if (!override) {
                //不能快速获取,要进行安全检查
                if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                    Class<?> caller = Reflection.getCallerClass();
                    checkAccess(caller, clazz, obj, modifiers);
                }
            }
            MethodAccessor ma = methodAccessor;             // read volatile
            if (ma == null) {
                ma = acquireMethodAccessor();
            }
            //直接执行方法
            return ma.invoke(obj, args);
        }
    

    Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这样可以大幅度地提升系统性能(当然了,由于取消了安全检查,也可以运行private方法,获取private私有属性)。

    1.4 使用forName动态的加载类

    详见—Class.forName与动态加载

    • 静态加载:编译时期加载的类。
    • 动态加载:运行时期加载的类。

    一般new对象便是静态加载,静态加载的不足在于:在编译的时刻就会把所有的类都加载,无论该类是否使用。

    而forName可以看做是动态加载,在运行期间才会检查该类是否存在。

    Class c1= Class.forName("com.javatest.Excel");
    Officeable e=(Officeable)c1.newInstance(); 
    

    而加载一个类即表示要初始化该类的static变量,特别是static块,在这里我们可以做大量的工作,比如注册自己,初始化环境,这才是动态加载的意义所在。

    例如源码中:com.mysql.cj.jdbc.Driver

    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        //静态代码块
        static {
            try {
                //把自己注册到DriverManager中
                java.sql.DriverManager.registerDriver(new Driver());
            } catch (SQLException E) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    
        public Driver() throws SQLException {
            // Required for Class.forName().newInstance()
        }
    }
    

    而实际上我们一般使用Class.forName("com.mysql.jdbc.Driver")来将Diver类加载到内存中。

    需要注意的是,forName只是加载类,并不执行任何代码,也不保证由此产生一个实例对象。之所以运行static代码,那是由类加载机制决定的,而不是forName方法决定的。

    1.5 反射在动态代理中的使用

    源码导读-5分钟看懂-JDK动态代理源码

    1.6 反射在装饰模式中的使用

    装饰模式的定义是“动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更加灵活。”不过,使用Java的动态代理也可以实现装饰模式的效果,而且其更加灵活,适应性更强。

    装饰器模式.png

    装饰模式的特点是:功能增强,接口不变。

    构建类型:

    public interface Animal {
        void doStuff();
    }
    
    public class Rat implements Animal {
        @Override
        public void doStuff() {
            System.out.println("这是一只小老鼠!");
        }
    }
    

    装饰类型:

    //定义某种能力
    public interface Feature {
        //加载能力
        void load();
    }
    
    public class FlyFeature implements Feature {
        @Override
        public void load() {
            System.out.println("增加一只翅膀...");
        }
    }
    

    装饰者模式:实现了装饰类和被装饰类的解耦。

    public class DecorateAnimal implements Animal {
        //被包装的动作
        private Animal animal;
        //使用哪个包装器
        private Class<? extends Feature> clz;
        public DecorateAnimal(Animal animal, Class<? extends Feature> clz) {
            this.animal = animal;
            this.clz = clz;
        }
        @Override
        public void doStuff() {
            //代理模式
            Feature proxy = (Feature)Proxy.newProxyInstance(getClass().getClassLoader(), clz.getInterfaces(), (p, m, args) -> {
                //实现InvocationHandler接口
                Object obj = null;
                //设置增强方法(Feature的接口类型为public)
                if (Modifier.isPublic(m.getModifiers())) {
                    obj = m.invoke(clz.newInstance(), args);
                }
                //执行animal方法
                animal.doStuff();
                return obj;
            });
            //执行代码方法
            proxy.load();
        }
    }
    

    一个装饰类型必然是抽象构建(Component)的子类型,它必须实现doStuff,此处doStuff方法委托了动态代理执行。

    此处代码是一个通用的装饰模式,只需要定义被装饰的类以及装饰类即可,装饰行为又动态代理实现,实现了装饰类和被装饰类的完全解耦,提供了系统的可扩展性。

    1.7 反射在模板模式中的使用

    模式方法模式(Template Method Pattern)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。

    public abstract class AbsPopulator {
        public final void dataInitialing(){
            //TODO 父类共有逻辑
            //子类特有方法
            doInit();
        }
        //需要每个业务自己实现
        protected abstract void doInit();
    }
    

    而反射如何在模板方法模式中使用呢?

    若doInit()方法中逻辑比较繁杂(例如初始化一张User表需要很多的操作,比如先建表,然后筛选数据,之后插入,最后校验),但是将其全部放入doInit()方法非常庞大(即使提取出多个方法承担不同的责任,代码可读性依旧非常差)。

    可以使模板方法实现对一批固定规则的基本方法的调用。

    public abstract class AbsPopulator {
    
        public final void dataInitialing() {
    
            //TODO 父类共有逻辑
            Method[] methods = getClass().getMethods();
            Arrays.stream(methods).
                    filter(this::isInitDataMethod).
                    forEach(method -> {
                        try {
                            method.setAccessible(true);
                            method.invoke(this);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    });
    
        }
    
        private boolean isInitDataMethod(Method method) {
            return method.getName().startsWith("init")  //init开头
                    && Modifier.isPublic(method.getModifiers())  //public修饰
                    && method.getReturnType().equals(void.class) //返回值是void
                    && !method.isVarArgs()  //输入参数为空
                    && !Modifier.isAbstract(method.getModifiers());  //不是抽象方法
        }
    }
    

    子类方法

    public class UserPopulator extends AbsPopulator {
    
        public void initUser() {
            System.out.println("初始化User");
        }
    
        public void initPassword() {
            System.out.println("初始化Password");
        }
    
        public void initJob() {
            System.out.println("初始化job");
        }
    
        public static void main(String[] args) {
            AbsPopulator userPopulator = new UserPopulator();
            userPopulator.dataInitialing();
        }
    }
    

    UserPopulator类中的方法只要符合基本方法鉴别器条件即会被模板方法调用,方法的数据量也不再受父类的约束,实现了子类灵活定义基本方法,父类批量调用的功能,并且缩减了子类的代码量。

    相关文章

      网友评论

        本文标题:Java反射-2(技巧)

        本文链接:https://www.haomeiwen.com/subject/azuguctx.html