zoukankan      html  css  js  c++  java
  • 编写高质量代码:改善Java程序的151个建议(第7章:泛型和反射___建议106~109)

    建议106:动态代理可以使代理模式更加灵活

      Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发。我们知道一个静态代理是通过主题角色(Proxy)和具体主题角色(Real Subject)共同实现主题角色(Subject)的逻辑的,只是代理角色把相关的执行逻辑委托给了具体角色而已,一个简单的静态代理如下所示:

    interface Subject {
        // 定义一个方法
        public void request();
    }
    
    // 具体主题角色
    class RealSubject implements Subject {
        // 实现方法
        @Override
        public void request() {
            // 实现具体业务逻辑
        }
    
    }
    
    class Proxy implements Subject {
        // 要代理那个实现类
        private Subject subject = null;
    
        // 默认被代理者
        public Proxy() {
            subject = new RealSubject();
        }
    
        // 通过构造函数传递被代理者
        public Proxy(Subject _subject) {
            subject = _subject;
        }
    
        @Override
        public void request() {
            before();
            subject.request();
            after();
        }
    
        // 预处理
        private void after() {
            // doSomething
        }
    
        // 善后处理
        private void before() {
            // doSomething
        }
    }

      这是一个简单的静态代理。Java还提供了java.lang.reflect.Proxy用于实现动态代理:只要提供一个抽象主题角色和具体主题角色,就可以动态实现其逻辑的,其实例代码如下:

    interface Subject {
        // 定义一个方法
        public void request();
    }
    
    // 具体主题角色
    class RealSubject implements Subject {
        // 实现方法
        @Override
        public void request() {
            // 实现具体业务逻辑
        }
    
    }
    
    class SubjectHandler implements InvocationHandler {
        // 被代理的对象
        private Subject subject;
    
        public SubjectHandler(Subject _subject) {
            subject = _subject;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            // 预处理
            System.out.println("预处理...");
            //直接调用被代理的方法
            Object obj = method.invoke(subject, args);
            // 后处理
            System.out.println("后处理...");
            return obj;
        }
    
    }

      注意这里没有代理主题角色,取而代之的是SubjectHandler 作为主要的逻辑委托处理,其中invoke方法是接口InvocationHandler定义必须实现的,它完成了对真实方法的调用。

      我们来详细解释一下InvocationHandler接口,动态代理是根据被代理的接口生成的所有方法的,也就是说给定一个或多个接口,动态代理会宣称“我已经实现该接口下的所有方法了”,那大家想想看,动态代理是怎么才能实现接口中的方法呢?在默认情况下所有方法的返回值都是空的,是的,虽然代理已经实现了它,但是没有任何的逻辑含义,那怎么办?好办,通过InvocationHandler接口的实现类来实现,所有的方法都是由该Handler进行处理的,即所有被代理的方法都由InvocationHandler接管实际的处理任务。

      我们开看看动态代理的场景,代码如下: 

    public static void main(String[] args) {
            //具体主题角色,也就是被代理类
            Subject subject = new RealSubject();
            //代理实例的处理Handler
            InvocationHandler handler =new SubjectHandler(subject);
            //当前加载器
            ClassLoader cl = subject.getClass().getClassLoader();
            //动态代理
            Subject proxy = (Subject) Proxy.newProxyInstance(cl,subject.getClass().getInterfaces(),handler);
            //执行具体主题角色方法
            proxy.request();
        }

      此时就实现了,不用显式创建代理类即实现代理的功能,例如可以在被代理的角色执行前进行权限判断,或者执行后进行数据校验。

      动态代理很容易实现通用的代理类,只要在InvocationHandler的invoke方法中读取持久化的数据即可实现,而且还能实现动态切入的效果,这也是AOP(Aspect Oriented Programming)变成理念。

    建议107:使用反射增加装饰模式的普适性

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

      我们以卡通片《猫和老鼠》(Tom and Jerry)为例,看看如何包装小Jerry让它更强大。首先定义Jerry的类:老鼠(Rat类),代码如下: 

    interface Animal{
        public void doStuff();
    }
    
    class Rat implements Animal{
        @Override
        public void doStuff() {
            System.out.println("Jerry will play with Tom ......");
        }
        
    }

      接下来,我们要给Jerry增加一些能力,比如飞行,钻地等能力,当然使用继承也很容易实现,但我们这里只是临时的为Rat类增加这些能力,使用装饰模式更符合此处的场景,首先定义装饰类,代码如下:

    //定义某种能力
    interface Feature{
        //加载特性
        public void load();
    }
    //飞行能力
    class FlyFeature implements Feature{
    
        @Override
        public void load() {
            System.out.println("增加一对翅膀...");
        }
    }
    //钻地能力
    class DigFeature implements Feature{
        @Override
        public void load() {
            System.out.println("增加钻地能力...");
        }
        
    }

      此处定义了两种能力:一种是飞行,另一种是钻地,我们如果把这两种属性赋予到Jerry身上,那就需要一个包装动作类了,代码如下: 

    class DecorateAnimal implements Animal {
        // 被包装的动物
        private Animal animal;
        // 使用哪一个包装器
        private Class<? extends Feature> clz;
    
        public DecorateAnimal(Animal _animal, Class<? extends Feature> _clz) {
            animal = _animal;
            clz = _clz;
        }
    
        @Override
        public void doStuff() {
            InvocationHandler handler = new InvocationHandler() {
                // 具体包装行为
                @Override
                public Object invoke(Object proxy, Method method, Object[] args)
                        throws Throwable {
                    Object obj = null;
                    if (Modifier.isPublic(method.getModifiers())) {
                        obj = method.invoke(clz.newInstance(), args);
                    }
                    animal.doStuff();
                    return obj;
                }
            };
            //当前加载器
            ClassLoader cl = getClass().getClassLoader();
            //动态代理,又handler决定如何包装
            Feature proxy = (Feature) Proxy.newProxyInstance(cl, clz.getInterfaces(), handler);
            proxy.load();
        }
    
    }

      注意看doStuff方法,一个装饰类型必然是抽象构建(Component)的子类型,它必须实现doStuff方法,此处的doStuff方法委托给了动态代理执行,并且在动态代理的控制器Handler中还设置了决定装饰方式和行为的条件(即代码中InvocationHandler匿名类中的if判断语句),当然,此处也可以通过读取持久化数据的方式进行判断,这样就更加灵活了。

      抽象构建有了,装饰类也有了,装饰动作类也完成了,那我们就可以编写客户端进行调用了,代码如下: 

    public static void main(String[] args) {
            //定义Jerry这只老鼠
            Animal jerry = new Rat();
            //为Jerry增加飞行能力
            jerry = new DecorateAnimal(jerry, FlyFeature.class);
            //jerry增加挖掘能力
            jerry = new DecorateAnimal(jerry, DigFeature.class);
            //Jerry开始戏弄毛了
            jerry.doStuff();
        }

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

    建议108:反射让模板方法模式更强大

      模板方法模式(Template Method Pattern)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。简单的说,就是父类定义抽象模板作为骨架,其中包括基本方法(是由子类实现的方法,并且在模板方法中被调用)和模板方法(实现对基本方法的调度,完成固定的逻辑),它是用了简单的继承和覆写机制,我么来看一个基本的例子。

      我们经常会开发一些测试或演示程序,期望系统在启动时自动初始化,以方便测试或讲解,一般的做法是写一个SQL文件,在系统启动前自动导入,不过,这样不仅麻烦而且容易出错,于是我们就手写了一个自动初始化数据的框架:在系统(或容器)自动启动时自行初始化数据。但问题是每个应用程序要初始化的内容我们并不知道,只能由实现者自行编写,那我们就必须给作者预留接口,此时就得考虑使用模板方法模式了,代码如下:

     1 public abstract class AbsPopulator {
     2     // 模板方法
     3     public final void dataInitialing() throws Exception {
     4         // 调用基本方法
     5         doInit();
     6     }
     7 
     8     // 基本方法
     9     protected abstract void doInit();
    10 }

      这里定义了一个抽象模板类AbsPopulator,它负责数据初始化,但是具体要初始化哪些数据则是由doInit方法决定的,这是一个抽象方法,子类必须实现,我们来看一个用户表数据的加载:  

    public class UserPopulator extends AbsPopulator{
        @Override
        protected void doInit() {
            //初始化用户表,如创建、加载数据等
        }
    
    }

      该系统在启动时查找所有的AbsPopulator实现类,然后dataInitialing实现数据的初始化。那大家可能要想了,怎么让容器指导这个AbsPopulator类呢?很简单,如果是使用Spring作为Ioc容器的项目,直接在dataInitialing方法上加上@PostConstruct注解,Spring容器启动完毕后自动运行dataInitialing方法。具体大家看spring的相关知识,这里不再赘述。

      现在问题是:初始化一张User表需要非常多的操作,比如先建表,然后筛选数据,之后插入,最后校验,如果把这些都放入到一个doInit方法里会非常庞大(即使提炼出多个方法承担不同的责任,代码的可读性依然很差),那该如何做呢?又或者doInit是没有任何的也无意义的,是否可以起一个优雅而又动听的名字呢?

      答案是我们可以使用反射增强模板方法模式,使模板方法实现对一批固定的规则的基本方法的调用。代码是最好的交流语言,我们看看怎么改造AbsPopulator类,代码如下:

    public abstract class AbsPopulator {
        // 模板方法
        public final void dataInitialing() throws Exception {
            // 获得所有的public方法
            Method[] methods = getClass().getMethods();
            for (Method m : methods) {
                // 判断是否是数据初始化方法
                if (isInitDataMethod(m)) {
                    m.invoke(this);
                }
            }
        }
    
        // 判断是否是数据初始化方法,基本方法鉴定器
        private boolean isInitDataMethod(Method m) {
            return m.getName().startsWith("init")// init开始
                    && Modifier.isPublic(m.getModifiers())// 公开方法
                    && m.getReturnType().equals(Void.TYPE)// 返回值是void
                    && !m.isVarArgs()// 输出参数为空
                    && !Modifier.isAbstract(m.getModifiers());// 不能是抽象方法
        }
    }

      在一般的模板方法模式中,抽象模板(这里是AbsPopulator类)需要定义一系列的基本方法,一般都是protected访问级别的,并且是抽象方法,这标志着子类必须实现这些基本方法,这对子类来说既是一个约束也是一个负担。但是使用了反射后,不需要定义任何抽象方法,只需要定义一个基本方法鉴定器(例子中的isInitDataMethod)即可加载符合规则的基本方法。鉴别器在此处的作用是鉴别子类方法中哪些是基本方法,模板方法(例子中的dataInitaling)则需要基本方法鉴定器返回的结果通过反射执行相应的方法。

      此时,如果需要进行大量的初始化工作,子类的实现就非常简单了,代码如下:

    public class UserPopulator extends AbsPopulator {
    
        public void initUser() {
            /* 初始化用户表,如创建、加载数据等 */
        }
    
        public void initPassword() {
            /* 初始化密码 */
        }
    
        public void initJobs() {
            /* 初始化工作任务 */
        }
    }

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

      如果大家熟悉JUnit的话,就会看出此处的实现与JUnit非常相似,JUnit4之前要求测试的方法名必须是以test开头的,并且无返回值、无参数,而且是public修饰,其实现的原理与此非常类似,大家有兴趣可以看看Junit的源码。

    建议109:不需要太多关注反射效率

      反射的效率是一个老生常谈的问题,有"经验" 的开发人员经常会使用这句话恐吓新人:反射的效率是非常低的,不到万不得已就不要使用。事实上,这句话前半句是对的,后半句是错的。

      反射的效率相对于正常的代码执行确实低很多,但它是一个非常有效的运行期工具类,只要代码结构清晰、可读性好那就先开发起来,等到进行性能测试时证明此处性能确实有问题再修改也不迟(一般情况下,反射并不是性能的终极杀手,而代码结构混乱、可读性差则可能会埋下性能隐患)。我们看这样一个例子,在运行期获得泛型类的泛型,代码如下: 

    class Utils {
        // 获得一个泛型类的实际泛型类型
        public static <T> Class<T> getGenricClassType(Class clz) {
            Type type = clz.getGenericSuperclass();
            if (type instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType) type;
                Type[] types = pt.getActualTypeArguments();
                if (types.length > 0 && types[0] instanceof Class) {
                    // 若有多个泛型参数,依据位置索引返回
                    return (Class<T>) types[0];
                }
            }
            return (Class<T>) Object.class;
        }
    }

      前面我们讲过,Java泛型只存在于编译器,那为什么这个工具类可以取得运行期的泛型类型呢?那是因为该工具只支持继承的泛型类,如果是在Java编译时已经确定了泛型类的类型参数,那当然可以通过泛型类获得了。例如有这样一个泛型类: 

    abstract class BaseDao<T>{
        //获得T运行期的类型
        private Class<T> clz = Utils.getGenricClassType(getClass());
        //根据主键获得一条记录
        public void get(long id){
            session.get(clz,id);
        }
    }
    //操作user表
    class UserDao extends BaseDao<String>{
        
    }

      对于UserDao类,编译器编译时已经明确了其参数类型是String,因此可以通过反射的方式来获取其类型,这也是getGenricClassType方法使用的场景。

      BaseDao和UserDao是ORM中的常客,BaseDao实现对数据库的基本操作,比如增删改查,而UserDao则是一个比较具体的数据库操作,其作用是对User表进行操作,如果BaseDao能够提供足够多的基本方法,比如单表的增删改查,哪些与UserDao类似的BaseDao子类就可以省却大量的开发工作。但问题是持久层的session对象(这里模拟的是Hibernate  Session)需要明确一个具体的类型才能操作,比如get查询,需要获得两个参数:实体类类型(用于确定映射的数据表)和主键,主键好办,问题是实体类类型怎么获得呢?

      子类进行传递?麻烦,而且也容易产生错误。

      读取配置问题?可行,但效率不高。

      最好的办法就是父类泛型化,子类明确泛型参数,然后通过反射读取相应的类型即可,于是就有了我们代码中clz变量:通过反射获得泛型类型。如此实现后,UserDao可不用定义任何方法,继承过来的父类操作方法已经满足基本需求了,这样的代码结构清晰,可读性又好。

      想想看,如果考虑反射效率问题,没有clz变量,不使用反射,每个BaseDao的子类都要实现一个查询操作,代码将会大量重复,违反了"  Don't  Repeat Yourself " 这条最基本的编码规则,这会致使项目重构、优化难度加大,代码的复杂度也会提高很多。

          对于反射效率的问题,不要做任何的提前优化和预期,这基本上是杞人忧天,很少有项目是因为反射问题引起系统效率故障的(除非是拷贝的垃圾代码),而且根据二八原则,80%的性能消耗在20%的代码上,这20%的代码才是我们关注的重点,不要单单把反射作为重点关注对象。

      注意:反射效率低是个真命题,但因为这一点而不使用它就是个假命题。

  • 相关阅读:
    MyCAT常用分片规则之分片枚举
    MySQL参数
    CentOS 7下MySQL服务启动失败的解决思路
    ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
    CHECK_NRPE: Received 0 bytes from daemon. Check the remote server logs for error messages.
    MyCAT实现MySQL的读写分离
    搭建Spark的单机版集群
    如何部署Icinga服务端
    MyCAT简易入门
    如何部署Icinga客户端
  • 原文地址:https://www.cnblogs.com/selene/p/5941809.html
Copyright © 2011-2022 走看看