文章目录
  1. 1. iOS上的元数据运用
    1. 1.0.0.1. Runtime Programming
    2. 1.0.0.2. Metamarco
  • 2. Java上的元数据运用
    1. 2.0.0.1. 反射
    2. 2.0.0.2. AnnotationProcessor
  • 3. Groovy元数据编程
  • 4. 最后说一句
  • 我比较推崇通过尽量利用编程中的元数据配合一些规约来进行功能实现.这里的元数据包括字段名,字段类型,方法名等等等等,任何你在编程的时候输入的字符实际上都可以作为元数据的一种,所以这篇文章的中心思想就是让你输入的每个字符都有含义,并且利用这些含义 .那么通过利用每个字符的含义可以大大节省代码量从而提高编码效率 .下面就是通过一些例子和技巧来展示这个思想.

    这里可以通过Objective-C/Java/Groovy上的一些应用来展示,我们先说一下iOS上的开发

    iOS上的元数据运用

    Objective-C(下面简称OC)这门语言是个挺有意思,它是基于C之上来构建的一门语言,所以它可以使用C中的一切,但是作为一门强静态语言,他的运用时又极其的动态,所以在iOS之上进行元数据编程就很有乐趣

    Runtime Programming

    OC的Reference里面专门有一章是讲 Runtime Programming 的,具体里面的原理大家可以看文档,这里就讲一下一些具体的实例.

    首先要做到元数据编程就需要能先拿到元数据,OC提供的库里面通过 #import <objc/runtime.h> 就能找到大部分的方法去拿到元数据.包括获取Class,Protocol,objc_property_t,Ivar,Method,Category,SEL,IMP,objc_method_description,objc_cache,objc_property_attribute_t
    基本都能拿到和进行操作,如果大家对上面的定义每个含义都能很清楚的了解那么对OC的整个语言实现体系,也能有一个大概的认识了.

    现在先看一个例子来说明元数据编程的作用, 最近凌飞推荐了一个Json的库JsonModel 在上面的例子里看到了一个对象的定义

    @protocol ProductModel
    @end
    
    @interface ProductModel : JSONModel
    @property (assign, nonatomic) int id;
    @property (strong, nonatomic) NSString* name;
    @property (assign, nonatomic) float price;
    @end
    
    @implementation ProductModel
    @end
    
    @interface OrderModel : JSONModel
    @property (assign, nonatomic) int order_id;
    @property (assign, nonatomic) float total_price;
    @property (strong, nonatomic) NSArray<ProductModel>* products;
    @end
    
    @implementation OrderModel
    @end
    

    可以看到其中最有意思的一行@property (strong, nonatomic) NSArray<ProductModel, ConvertOnDemand>* products; 这一行表示定义了一个属性products 它的类型是一个NSArray,而且让它被反序列化的时候这个NSArray里面被放入的对象是ProductModel这个类型. 这个描述是不是和Java的泛型很像啊,但是OC这门语言实际上是不支持泛型的,那它是怎么做到的呢.

    我们看NSArray<ProductModel> 这个写法,这是一个Protocol的用法,也就是说ProductModel是一个Protocol,所以我们看到是有

    @protocol ProductModel
    @end
    

    这样的定义在上面的.那么NSArray<ProductModel>中的ProductModel实际上是指@protocol ProductModel,而不是@interface ProductModel,那就是说JsonModel的作者只是利用的一个技巧,将和类相同名字的Protocol作为其元数据的传递被利用起来,做到代码的美观和语义的丰满.

    现在我们看到里元数据的利用,我们再来看具体实现是怎么把这个元数据利用起来的.首先我们要拿到这个属性的元数据,我们通过objc_property_t *properties = class_copyPropertyList(class, &propertyCount); 这个方法拿到这个类下面所有的属性, 然后再通过const char *attrs = property_getAttributes(property);拿到属性的属性.可以看到属性的属性实际上是一个字符串 那么@property (strong, nonatomic) NSArray<ProductModel>* products;这个属性的属性被取出来是这样子的:T@"NSArray<ProductModel>",&,N,V_products 通过分析这个字符串我们就能拿到这个属性字段的类型包括描述它的那个Protocol,那么利用这个Protocol和类相同名字的这个规约,我们就能拿到,我们约定的这个属性NSArray需要包含的数据的类型了,然后在反序列化Json的时候就能准确的知道需要反序列化的类型,而不再是单纯的NSDictionary了.

    上面的那个例子就可以看到利用元数据可以帮助实现一些很技巧却也带来很有效的节省工作量的做法,就是利用元数据将原先需要重复手工ObjectMapping的步骤通过元数据被抽象到框架上层进行封装,而对使用者透明,从而提高了整个编码效率.

    Metamarco

    由于OC是基于C的,而C有一个很有意思的东西就是宏(marco),利用marco我们也能进行针对macro的元数据编程,来达到一些很不可思议的效果.

    我推荐看这个实现metamacros.h, 这是一个libextobjc下的一个针对marco进行元数据操纵的头文件,里面提供了很多利用marco进行元数据编程的define, 可以进行计算,判断,拼接的功能.

    由于marco是在编译器被执行的,所以marco可以做到在编译期做强制检查的功能,在编译期就能讲错误暴露出来.

    这里举一个例子来看,见EXTKeyPathCoding.h

    **
     * \@keypath allows compile-time verification of key paths. Given a real object
     * receiver and key path:
     *
     * @code
    
    NSString *UTF8StringPath = @keypath(str.lowercaseString.UTF8String);
    // => @"lowercaseString.UTF8String"
    
    NSString *versionPath = @keypath(NSObject, version);
    // => @"version"
    
    NSString *lowercaseStringPath = @keypath(NSString.new, lowercaseString);
    // => @"lowercaseString"
    
     * @endcode
     *
     * ... the macro returns an \c NSString containing all but the first path
     * component or argument (e.g., @"lowercaseString.UTF8String", @"version").
     *
     * In addition to simply creating a key path, this macro ensures that the key
     * path is valid at compile-time (causing a syntax error if not), and supports
     * refactoring, such that changing the name of the property will also update any
     * uses of \@keypath.
     */
    #define keypath(...) \
        metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
    
    #define keypath1(PATH) \
        (((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
    
    #define keypath2(OBJ, PATH) \
        (((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
    

    我们知道OC中的keypath在运行时是以字符串的形式存在的,那么如果开发者改了字段名,而没有改使用中的keypath的话,那么在运行时就会出现错误,且不容易被发现,但是如果使用上面定义的@keypath的宏来获取keypath那么他在编译器就会做一次检查这个字段名是否存在,提早报出错误.

    metamarco的使用场景还有很多,比如根据规约生成有关联的方法名(见TBMBBind.hTBMBWhenThisKeyPathChange的使用)便于自动注册等等,等待大家的挖掘.

    Java上的元数据运用

    Java有着很强大的反射系统(不过感觉还是没有OC强,不过用起来比OC方便多了),可以很容易的获取到元数据,而且配合Annotation可以衍生出很多元数据类型来,同样的在Java5之后,提供了AnnotationProcessor这等神器,也让Java有了预编译这个阶段,可以发挥很多作用出来.

    反射

    通过反射我们能拿到最大多数元数据,包括Type(Class是Type的一种),Field,Method,Constructor,Package,Annotation等,这里我还是通过举一些例子来看怎么利用起来.

    看一个例子:Filter.java

    其中看它的字段定义

    public class Filter implements Request {
        private String _field;
    
        private int _____flag;
    
        private String _value;
    

    可以看到字段名被以下划线作为开始.为什么要这么做呢.tdhs-java-client 是一个网络客户端,里面涉及到做序列化,这里因为性能考虑使用了TLV这种很简单的序列化协议,而这边由于考虑到协议的变换,将序列化功能单独抽象出来到TDHSProtocolBinary.java,那么就需要被序列化的字段的元数据由字段本身来维护.所以这里使用了一下几个元数据

    1. 字段名: 以下划线为字段名开始的字段表示这个字段是需要被序列化的,如果是4个下划线开始的字段表示这个int是以32位int被序列化否则是8位的大小被序列化.
    2. 字段书写顺序: 字段被书写时的顺序,也被用作序列化成TLV时的字段顺序.

    从这个例子可以看出利用这些元数据,就能减少很多编码,如果没用可能每个对象都需要实现一个encode方法来实现编码.而现在就能被直接抽象到一个专门的Encode类中来进行序列化了,后续有新的对象也能很透明的被加进来.

    当然从优雅的角度而言,用Annotation来表明这些属性是最好的,但是不幸的是 java.lang.reflect.Field#getAnnotation 的性能比较差,在高并发下很耗CPU(主要是getAnnotation没有做cache),最后选择了直接使用字段名来做利用.


    当然上面的例子是一个比较极端的例子,一般情况下Java是善于使用Annotation来元数据传递的.举一个Modulet的例子

    @Component
    @Desc("发布Feed")
    @NeedUserAuth
    @MtopConfig(dataToParameter = true, hsfMethodName = "publishFeed")
    public class PublishFeedModulet implements Modulet<FeedParam, FeedId> 
    

    这段代码对PublishFeedModulet这个类,有4个Annotation进行描述,我们这里就说下@NeedUserAuth这个Annotation,打上这个Annotation的Modulet表示调用到Modulet的时候是需要用户验证的,说白了就是需要登录的,但是判断登录的代码并不会写在这个Modulet里面,而是在调用链上的一个拦截器上被判断,那么判断是否登录和获取登录信息的逻辑就被抽象到拦截器上,而通过获取具体Modulet上是否有@NeedUserAuth这个元数据,将用户验证逻辑直接复用到Modulet上来.


    还有一个很容易被忽略的元数据就是泛型.泛型不单单只能被用来做类型限制,也能在运用时被取出来做一些处理

    还是上面的Modulet,我们可以看到有Modulet<FeedParam, FeedId> 这里表面了Modulet的输入和输出的类型FeedParam 和 FeedId, 那么在运行时可以通过Type[] genericInterfaces = aClass.getGenericInterfaces(); 拿到Type然后在这个Type(实际上是ParameterizedType)里拿到泛型的类型,然后通过这个泛型的具体类型 再去做具体的反序列化到这个类型是实例然后传递进来.

    还有一个很好的例子就是Generic Data Access Objects 通过利用泛型,将可复用的逻辑很容易的集中起来.

    AnnotationProcessor

    对于AnnotationProcessor的利用,最有说服力的就是lombok了,他通过Annotation或者直接的字段名等元数据信息在编译期直接生成代码的方式,让你大大节省一些重复的编码,比如说生成Getter/Setter/ToString等等,但是debug的时候就有的苦逼了,嘿嘿

    但是道理和上面的反射是一样的,AnnotationProcessor提供了一种在编译期获取元数据的方式,并且可以让我们能利用这些元数据做一些事情.只不过利用这个元数据的过程是在编译期而不是运行时.

    Groovy元数据编程

    Groovy是一个很有意思的语言,首先他是一个动态语言,所以在它上面做元数据编程是一件很容易的事情,所以衍生出来的做一级DSL也会很容易.

    网上关于Groovy Metaprogramming的文章有很多大家可以自己去搜索查看.这里就不多写了.

    推荐一篇写的比较全面介绍的文章METAPROGRAMMING AND THE GROOVY MOP

    主要是写的累了,休息一下

    最后说一句

    元数据编程本身是一种高度抽象,很多时候表现和实现是高度分离的,基本都是靠一些规约维系其中的关系.所以使用过程中对于后续的维护者还是review者都有很大的学习成本.所以也要节制的去使用,因为爱是节制~

    文章目录
    1. 1. iOS上的元数据运用
      1. 1.0.0.1. Runtime Programming
      2. 1.0.0.2. Metamarco
  • 2. Java上的元数据运用
    1. 2.0.0.1. 反射
    2. 2.0.0.2. AnnotationProcessor
  • 3. Groovy元数据编程
  • 4. 最后说一句