Java反射-2(技巧)

Java反射-1(理论)
Java反射-2(技巧)
JAVA反射-3(性能)

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容