日常开发杂记 - 2020/02/25、26、27 --- 未系统整理

用 gradle 的GroovyCompiler如何指定Java编译器选项?

这样指定:

	compileGroovy {
	    options.compilerArgs << "-Xlint:unchecked"
	}

Groovy 和 Java 混合使用的项目,如何让 Java 类也能访问到 Groovy 类?

在 gradle 中配置下面内容:

// 下面的配置会让gradle先编译groovy,后编译java,以便在 java 中使用 groovy 类
// ref: https://coderwall.com/p/wuqopq/compile-groovy-before-java-in-gradle-build
// 因为 groovy 插件默认会先编译 java 后编译 groovy,导致 java 找不到 groovy 的类。
sourceSets {
    main {
        groovy {
            // this makes the groovy-compiler compile groovy- as well
            // as java-files.
            // Needed, because java is normally compiled before groovy.
            // Since we are using groovy objects from java, we need it
            // the other way round.
            srcDirs = ['src/main/groovy', 'src/main/java']
        }
        java {
            srcDirs = [] // don't compile Java code twice
        }
    }
}

如何解决“未经检查的类型转换”编译警告?

警告如下:

public Token deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
                 ^
  返回类型需要从Token到T的未经检查的转换
  其中, T是类型变量:
    T扩展已在方法 deserialze(DefaultJSONParser,Type,Object)中声明的Object

没有找到解决方法彻底解决,但可以用注解告诉 javac 编译器不要显示 unchecked 警告。

@SuppressWarnings("unchecked")

其实这个接口的定义不需要定义一个泛型 T 吧 (fastjson项目的)?下面这段代码

public interface ObjectDeserializer {
    /**
     * fastjson invokes this call-back method during deserialization when it encounters a field of the
     * specified type.
     * 

In the implementation of this call-back method, you should consider invoking * {@link JSON#parseObject(String, Type, Feature[])} method to create objects * for any non-trivial field of the returned object. * * @param parser context DefaultJSONParser being deserialized * @param type The type of the Object to deserialize to * @param fieldName parent object field name * @return a deserialized object of the specified type which is a subclass of {@code T} */ <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName); int getFastMatchToken(); }

不清楚上面这样定义 T 有什么作用。

Groovy 类的属性(property)默认是 private级别的,如何设置属性是protected的,而get/set方法是public的?

看groovy的 traits(特征) 相关文档,就能知道了。
首先要能区分两个容易混淆的概念 “fields” 和 “property”。

  • field 字段:就是通常所说的 java class 中的 fields 字段,在 groovy 中定义时必须带 private, protected 或者 public 访问修饰词,groovy 就是靠是否有此修饰词来确定你的代码是在定义一个 field还是 property。
  • property 属性:它就是 Java Bean 规范中定义的 property,就是 fields 加上对应的 get / set 方法。访问 property 时必须通过 get、set 方法。

因此,我们从规则上就应该避免这样用,实在需要这样设计,那么属性对应的字段肯定是改不成 protected 了,直接使用 get、set 方法就好了。如果是 groovy 代码,那么使用 属性 和使用 字段 是没有区别的,都是用 obj.name 的方式就可以了。

如果是在 traits 中使用 property 就更需要注意了,因为实现类中不能重载traits属性的定义,设置值倒是可以的,如下面的代码示例:

trait IntCouple {
    int x = 1
    int y = 2
    int sum() { x+y }
}
class Elem implements IntCouple {
    int x = 3                                       
    int y = 4                                       
    int f() { sum() }                               
}
def elem = new Elem()
assert elem.f() == 3

结果是失败!因为 elem.f() 等于 7!而不是3,因为 traits 有一个独立的对象,这个对象的 field 并不会被实现类(Elem)所改变。
要想支持在实现类中改变traits的属性,只能在traits的定义中使用get、set方法,而不要直接使用字段,像下面这样:

trait IntCouple {
    int x = 1
    int y = 2
    int sum() { getX()+getY() }
}

就可以了,elem.f() 的值现在就是 7 了。或者用设置值的方式也行,如下:

class Elem implements IntCouple {
    int f() { 
	      x = 3
	      y = 4
	      sum()
      }                               
}

Groovy 的 traits 功能非常强大,值得单独写一篇文章。

如何去掉 IDEA 的“unused” 警告?

在方法或类上添加 @SuppressWarnings(“unused”) 注解即可。

如何让 Groovy 不要调用 DefaultGroovyMethods.addAll() 方法,而是 JavaFX ObservableListWrapper 的 addAll() 方法?

TODO

Groovy 的 spread 操作符 ‘*’ 不支持多级展开

下面代码不成立

list*.staff*.status = newStatus

只能展开一次,并且只能在等号的右侧,如下

def staffList = list*.staff

GString 不能正确解析 property

如果 class 定义了 field,定义 get 函数,那么 GString 中的 this.name 会解析为 field 而不会作为property访问,去调用 get 函数。例如:

class TenantUser {
	private ObjectProperty<TenantUserStatus> status = new SimpleObjectProperty<>()
    TenantUserStatus getStatus() {
        return status.value
    }
    def test() {
    	"${this.status.name}"
    }
}
def user = new TenantUser()
// 编译会报错:找不到 类型 ObjectProperty 的 name 方法

说明,GString 中 ${this.status.name} 解析 this.status 为字段而不是属性了。
解决方法是在GString中使用 get 函数。

Groovy 中经常出现的 Slurper 单词如何理解和翻译?

比如 JsonSlurper, XMLSlurper 等等。
Slurper 是寂静岭3这款游戏中的怪物,头上就一张嘴,跳起来从头上啜食猎物。因此我们可以把 Slurper 翻译为“吃货”,对,就是吃货,如果翻译为“啜食兽”,又让人要去猜什么是“啜食”,不如“吃货”来得形象。
因此,JsonSlurper 就是 Json吃货,XMLSlurper 就是 XML 吃货。

如何将一个Groovy map 的key/value对一次性拷贝到另外一个对象的属性上?

就是将 map 的 key 对应 对象的属性名,value 赋给该属性。目标是实现将 json slurper 生成的 Groovy Object 转换为指定 class 的对象。
目标逻辑示例代码如下:

def jsonObject = jsonSlurper.parseText "{name:'abc'}"
User user = jsonObjectToUser(jsonObject)
assert user instanceof User

思路:
有帮助的一个方法是 InvokeHelper.setProperties( target, properties )

User user = User.findById('1')
User copyUser = new User()
InvokerHelper.setProperties(copyUser, user.properties)
copyUser.language = 'French'
copyUser.save(flush:true)

另外一个思路是用 强制转换 操作符 “as” ,参考 as 的文档。
下面是一个 as 的使用方法

class Identifiable {
    String name
}
class User {
    Long id
    String name
    def asType(Class target) {                                              
        if (target == Identifiable) {
            return new Identifiable(name: name)
        }
        throw new ClassCastException("User cannot be coerced into $target")
    }
}
def u = new User(name: 'Xavier')                                            
def p = u as Identifiable                                                   
assert p instanceof Identifiable                                            
assert !(p instanceof User)       

因为 JsonSlurper 解析 json 中的 object 为 Map,所以我们需要为 Map 动态地加上一个函数 asType()。
怎么实现呢?用 traits 吧。traits 可以为一个类动态地添加接口。 用traits使用起来比较麻烦,应该使用 Category 来实现。

下面的traits代码不工作,会抛出异常

trait MapToClass {
    /**
     * 支持将 Map 转换为 TenantStaff 类
     * @param target 要转换的目标类
     */
    def asType(Class target){
        switch (target) {
            case TenantStaff:
                TenantStaff targetObject = new TenantStaff()
                // 复制key/value到 targetObject 的属性上
                this.entrySet().collect {
                    targetObject.setProperty(it.key as String, it.value)   //  <1>
                }
                return targetObject
            case StringProperty:
                // 取 StringProperty 变成 json串的object后的 value 属性,因为这个对象的格式类似下面这样:
                // {value=老杨 (在线), bean=null, bound=true, name=, valueSafe=老杨 (在线)}
                return new SimpleStringProperty(this.value)
        }
        throw new ClassCastException("Unsupport coercion from Map to class $target")
    }
}

<1> 在这里会抛出异常,说无法将 Map cast 成 StringProperty。这个原因是groovy在设置 property 值之前,做了参数的 cast 动作,而我们的参数是一个复杂对象即 StringProperty,它被序列化成 json 的 object,当反序列化的时候,就会被 JsonSlurper 读作 Map 对象,从而引发上面的 cast 异常。下面我们用 category 来实现也是一样会抛异常的,因为都会调用 setProperty() 方法。

换用 Category 来实现还是不行,抛异常。

看来要用 ExpandoMetaClass 才能实现了。

抛异常的地方:

//----------------------------------------------------------------------
// executing the getter method
//----------------------------------------------------------------------
if (method != null) {
    if (arguments.length == 1) {
        newValue = DefaultTypeTransformation.castToType(
                newValue,
                method.getParameterTypes()[0].getTheClass());       <1>
        arguments[0] = newValue;
    } else {
        newValue = DefaultTypeTransformation.castToType(
                newValue,
                method.getParameterTypes()[1].getTheClass());
        arguments[1] = newValue;
    }
    method.doMethodInvoke(object, arguments);
    return;
}
        
private static Object continueCastOnSAM(Object object, Class type) {
...
    return InvokerHelper.invokeConstructorOf(type, args);       <2> 
...
}

<1> 这个地方导致的异常了。
<2> 这个地方最终抛出的异常。

所以还要研究如何给一个 Class 动态添加构造函数才行。

主要是因为属性的类型是 javafx 的 StringProperty 类 class,它有已经有 default constructor。

如果把它换成具体的实现类,比如SimpleStringProperty则会出现另外的异常,即无法cast参数值的异常。

最后,用了一个解决方法,就是为每个特殊的属性编写转换逻辑:

    /**
     * 支持将 Map 转换为 TenantStaff 类
     * @param target 要转换的目标类
     */
    static def asType(Map self, Class target) {
        switch (target) {
            case TenantStaff:
                TenantStaff targetObject = new TenantStaff()
                // 复制key/value到 targetObject 的属性上
                self.entrySet().forEach {
                    switch (it.key) {
                        case "elementNameProperty":
                            targetObject.elementNameProperty.value = it.value.value
                            break
                        case "statusProperty":
                            targetObject.statusProperty.value = it.value.value as TenantUserStatus
                            break
                        case "children":
                            targetObject.children.addAll(it.value as List)
                            break
                        default:
                            targetObject.setProperty(it.key as String, it.value)
                    }
                }
                return targetObject
        }
        throw new ClassCastException("Unsupport coercion from Map to class $target")
    }

小结:Groovy 的 动态 setProperty 方法要求属性的类型是可以正常实例化的,并且要设置的值对象必须能直接转换为属性类型,而不能通过额外的其他强制转换方式实现。这也是目前Groovy能力的局限吧。

为什么 Groovy 对 Date 的扩展函数没有了?

今天在开发时,需要用 Date.parse(“yyyy-MM-dd HH:mm:ss”, “2020-02-28 12:30:00”) 这个方法,却发现找不到了,编译器报告说 parse() 函数已经过时。

原来是缺少 groovy-dateutil JAR 的依赖,添加依赖即可,如下

implementation 'org.codehaus.groovy:groovy-all:2.5.9'
implementation 'org.codehaus.groovy:groovy-dateutil:2.5.9'

另外 LocalDate 的扩展也可以使用,如下:

org.apache.groovy.datetime.extensions.DateTimeStaticExtensions#parse(java.time.LocalDate, java.lang.CharSequence, java.lang.String)

所以我们要这样写,注意 pattern 参数放到最后面了:

LocalDate.parse("2020-02-28 12:30:00", "yyyy-MM-dd HH:mm:ss")

一些有用的Groovy相关资源链接

  • Groovy 文档的中文翻译,尽管翻译得不是太好
  • Groovy 的深度技术文章,的确是使劲用过的人写的精华:
    • Groovy与Java集成常见的坑
    • Groovy深入探索——Groovy的ClassLoader体系
    • Groovy引发的PermGen区爆满问题定位与解决
    • 《深入理解Java虚拟机》和《Java性能优化权威指南》
    • 《Groovy in Action》

关于 WebSocket

  • 介绍 websock的基本概念和开发技术;这篇博客介绍了更多技术细节,如数据帧、API。
  • grails 有 websocket plugin
  • WebSocket 协议
    • 其中关于"重连" 的描述,解决了我的疑惑。参看 7.2.3. Recovering from Abnormal Closure
  • spring 中关于 websocket 的用法
  • websocket 的 java client libarary
    • vert.x for Groovy,可以用来实现websocket客户端

关于 Vert.x 框架

目前我的理解,Vert.x 就是 Java 中的 Node.js,一切调用都是异步的。
和 node.js 一样,都使用 Reactor Pattern 进行编程。而和 node.js 不同的是,Vert.x 使用了多个 event-loop thread,这样可以用一个 Vert.x 实例运行在多个 CPU 核心上,这种模式叫做 Multi-Reactor Pattern.

这种编程模式,对于程序员最重要的要求是“不能阻塞Event Loop Thread” !例如不能在你的 Handler 中 wait、sleep等。

对于会阻塞一小会儿(不超过几秒)的任务,可以用 executeBlocking()方法来执行。但是更长时间的任务,就必须由应用程序自己提供专门的线程来执行,然后用 event-bus 或者 runOnContext 来和 vert.x 交互了。

这个看起来和 JavaFX 的UI线程模式怎么这么相像

vertx.executeBlocking({ promise ->
  // Call some blocking API that takes a significant amount of time to return
  def result = someAPI.blockingMethod("hello")
  promise.complete(result)
}, { res ->
  println("The result is: ${res.result()}")
})

到这里,并没有觉得 Vert.x 的优点,是不是使用线程池来开发也是一样的呢。其实还是有一点细微的差别的,这个细微的差别就是 Vert.x 使用的 Reactor-Pattern,就像 Node.js 那样,处处使用、甚至是强迫程序员使用 Reactor-Pattern,可以将CPU因为等待IO而浪费的时间大大减小,这样就做到了提高服务器使用效率,也就是单台服务器能处理更多的并发请求了。

另外一个好处就是下面的各种“异步协调”,这种模式可以方便地组合异步任务的结果。

Async coordination 异步协调

就是将多个异步执行的“未来”对象进行各种组合,得到一个新的“未来”对象。

  • CompositeFuture.all 全部成功才算成功,有一个失败都算失败,并且不等所有的 future 结束
  • CompositeFuture.join 和 all 一样,只是会等待所有的 future 结束
  • compose 顺序执行

Vert.x 前景预测

因为 Node.js 已经在世界各大电商、流量大户广泛使用,其中一个原因是由于异步开发模式带来的服务器执行效率,一个原因是 javascript 完成了全栈的开发,开发效率高。
因此想到了 Java 程序员们,Groovy 是 Java 中的 Javascript,那么 Vert.x 就是 Java 中的 Node.js 了,如果 Groovy 语言能在 Java 世界中普及、流行,那么 Vert.x 也有望在 Java 世界中普及。但是因为 Vert.x 不像 Node.js 对于 Javascript 世界那么重要,因为 Node.js 直接打通了前后台开发,Vert.x 并不能渗透到前端开发,再加上 Vert.x 是Node.js 的后来者,Vert.x 最多在服务器端,把异步编程带来的服务器性能发挥到极致。

结论就是 Vert.x 的流行程度会和 Groovy 语言的流行程度成正比,能在服务器端更好地完成“高并发、大流量”的计算任务,短时间内的成功取决于世界级的电商大厂、中型互联网公司的采纳和拥抱,但长期来看,Java世界要拥抱 “异步编程模式”(所谓的 Reactive Programming)就必须仰仗 Vert.x、Spring5+,或者Akka了。

参考资料

  • Vert.x 的Groovy文档
  • Going Reactive With Vert.x and RxJava

你可能感兴趣的:(技术笔记,日常杂记,java,gradle,groovy)