YON CISE

MyBatis 记录二: lazy loading

基于 v3.4.1

为了映射对象中复杂的关联对象, 我们在 ResultMap 中可以配置 association 和 collection. 这两者的实现方式可以通过

  1. 再请求一次 select (Nested Select)
  2. 通过 join 将所有的属性读取出来 (Nested Results).

但有时候我们并不会使用到对象中所有的属性, 所以这些额外的从数据库拉来的数据就浪费了. 对于通过 select 实现的方式我们可以使用懒加载来提升效率.

配置

lazyLoadingEnabled

默认为 false, 也就是不使用懒加载. 所以如果 association 和 collection 使用了 select, 那么 MyBatis 会一次性执行所有的查询. 如果 accociation 和 collection 中的 fetchType 指定为 lazy, 那么即使 lazyLoadingEnabled 为 false, MyBatis 也会使用懒加载.

Java 配置中 @One@ManyfetchType 支持三个值: LAZY, EAGER, DEFAULT, 其中 DEFAULT 的意思是跟随全局设置即 lazyLoadingEnabled.

aggressiveLazyLoading

默认为 true, 也就是说当你开启了懒加载之后, 只要调用返回的对象中的 任何一个方法, 那么 MyBatis 就会加载所有的懒加载的属性, 即执行你配置的 select 语句.

lazyLoadTriggerMethods

默认值为 equals,clone,hashCode,toString, 当你调用这几个方法时, MyBatis 会加载所有懒加载的属性.

proxyFactory

默认为 JAVASSIST (MyBatis 3.3 or above). 懒加载是通过字节码增强实现的, 3.3 以前是通过 cglib 实现的, 3.3 之后包括 3.3 是使用 javassist 实现的.

源码分析

前面提到了懒加载是通过字节码增强实现的, 所以 MyBatis 会动态代理你的类, 然后根据调用的方法名来判断是否需要加载属性.

相关类的实现有两个, 分别对应 javassist 和 cglib 的版本:

  • javassist: org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory
  • cglib: org.apache.ibatis.executor.loader.cglib.CglibProxyFactory

两个类在方法拦截时的处理逻辑是一样的, 我们挑其中一个来看 (javassist 的):

@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
  final String methodName = method.getName();
  try {
    synchronized (lazyLoader) {
      if (WRITE_REPLACE_METHOD.equals(methodName)) { // 这段不懂是处理什么情况的, 没细看
        Object original = null;
        if (constructorArgTypes.isEmpty()) {
          original = objectFactory.create(type);
        } else {
          original = objectFactory.create(type, constructorArgTypes, constructorArgs);
        }
        PropertyCopier.copyBeanProperties(type, enhanced, original);
        if (lazyLoader.size() > 0) {
          return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
        } else {
          return original;
        }
      } else { // 这段是重点
        if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
          if (aggressive || lazyLoadTriggerMethods.contains(methodName)) { 
            // 如果开启了 aggressive 或者调用的是 lazyLoadTriggerMethods 中设置的方法, 则加载所有属性
            lazyLoader.loadAll();
          } else if (PropertyNamer.isProperty(methodName)) { // 判断方法是否是以 get, set, is 开头
            final String property = PropertyNamer.methodToProperty(methodName); // 方法名转换成属性名
            if (lazyLoader.hasLoader(property)) {
              lazyLoader.load(property); // 加载
            }
          }
        }
      }
    }
    return methodProxy.invoke(enhanced, args);
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

注意事项

aggressiveLazyLoading

项目中开启了懒加载后, 准备测试下是否真的启用了懒加载, 于是打印日志看了下对应的属性在调用相应的方法前是否是 null. 结果发现属性每次都被加载了, 以为 MyBatis 能拦截属性的直接访问或者生成代理类的时候会分析相应字节码, 如果发现字节码中有属性的访问就在访问该方法时加载属性, 查了下 cglib 和 javassist 文档感觉并没有相应的功能啊.

最后发现原来是因为 aggressiveLazyLoading 默认是开启的, 因为我访问了对象的其他方法所以属性被加载了.

IDEA

千万不要用下断点的方式查看对应的属性有没有被加载, 可能 是因为 IDEA 在 debug 的时候会调用 lazyLoadTriggerMethods 中的方法的, 所以导致属性被加载.

Jackson

MyBatis 生成的代理类会多出一个 handler 的属性, 从而导致 Jackson 序列化失败, 可以通过在类上添加注解来忽略该属性:

@JsonIgnoreProperties("handler")
public class MyDO {
}

Configuration

Mapper XML Files

Java API

MyBatis 记录一: 缓存

基于 v3.4.1

MyBatis 缓存分为 local cache 和 2nd level cache. 前者是在 SqlSession 级别缓存, 后者是在 Maprer 级别进行缓存. 是否开启缓存可以通过在配置中设置 cacheEnabled (默认是开启的).

Local Cache

如果开启了缓存, 那么默认使用的是 local cache, 作用域是 SESSION, 即同一个 session 中 MyBatis 会对所有的查询缓存. SELECT, UPDATE, INSERT, DELETE 语句都支持 flushCache 配置, 如果为 true, 那么语句执行完毕 MyBatis 会清空缓存. SELECTflushCache 默认关闭, 其他语句都默认打开.

作用域可以通过配置 localCacheScope 来调整, 另一个作用域是 STATEMENT. 在这一作用域下, MyBatis 只会为每一次的调用使用缓存. 难道一次调用不就是一条 SQL 语句么, 那缓存有什么用? MyBatis 的一次调用可能不止一条 SQL 语句的, 因为 ResultMap 支持 association 和 collection, 这两者都可能触发多次 SQL.

2nd Level Cache

2nd level cache 的使用和 local cache 是一样的, 只是它默认是关闭. 需要手动开启, 在 Mapper.xml 中添加:

<cache />

如果是 Java, 可以使用 @CacheNamespace 来开启:

@CacheNamespace
public interface Mapper {
}

被缓存的对象需要实现 Serializable 接口.

2nd level cache 还可以配置替换算法 (eviction), 刷新时间 (flushInterval, 毫秒为单位) 等属性

通过 cache-ref 可以多个 mapper 使用同一个缓存.

ps. 测试缓存是否生效可以在一次请求之后修改下数据库中的数据, 再请求下看下数据有没有变化, 如果没变就说明缓存生效了.

Configuration

Mapper XML Files

Java API

SSH 远程端口转发

今天想调试下新版本服务端支付宝支付确认消息的处理流程, 但是因为本机没有公网 IP, 所以想到利用远程服务器做端口转发. 于是执行下面的命令:

ssh -R 8080:localhost:8080 <remote>

然后在浏览器里访问 <remote>:8080, 提示连接被拒绝. 于是登录到远程服务器执行:

curl localhost:8080

发现访问正常, 所以连接被拒绝应该是因为 ssh 只绑定了本地的网口. 于是修改了下 ssh 的命令:

ssh -R 0.0.0.0:8080:localhost:8080 <remote>

测试了下发现还是拒绝. 到网上搜了好久才找到答案, 原来 ssh 默认只转发发往本地网口的请求. 要转发所有请求需要将 GatewayPorts 设置成 yes (在 /etc/ssh/sshd_config 中设置)

其实这问题之前遇到过, 也搜了好久, 所以还是要记下来才行!

How to make ssh tunnel open to public?

I/O 模型

Overview

I/O 模型是 操作系统 抽象出来的概念, 方便操作系统统一的管理. 各种各样的模型之间到底有什么区别呢? 阻塞 (blocking) 和 同步 (synchronous) 是什么关系?

I/O 过程

简单来讲, I/O 的过程分为两个部分 (以读取为例):

  1. 等待数据就绪
  2. 把数据从内核拷贝到用户进程空间

用 socket 来举例, 第一个步骤就是等待网卡接收到数据, 当接收到数据, 处理器把数据拷贝到内核的缓冲区. 第二个步骤就是把数据从内核的缓冲区拷贝到用户程序的缓冲区.

第一个步骤还是很直观的, 但是为什么要有第二个步骤呢? 数据拷来拷去不是很没效率么? 这牵扯到处理器的特权级别了. 现代的处理器在运行时是分为不同的级别的. 低级别程序会受到一定的限制, 比如只能访问一部分内存, 只能执行特定的指令. 所以需要将内核的数据拷贝到用户进程能访问的空间去.

五种 I/O 模型

下面的讨论主要基于 UNIX 操作系统.

Blocking I/O

最常见的 I/O 模型就是阻塞型的. 以 socket 来举例, 当我们调用 recv(), send(), connect(), accept() 这四个方法时, 线程都会阻塞住. 也就是说从等待数据就绪到数据拷贝到用户进程都是阻塞的.

Nonblocking I/O

当我们把 socket 设置成 nonblocking 的, 这时候调用 recv(), send(), connect(), accept(), 如果没有数据会返回 EWOULDBLOCK 错误而不是阻塞住, 如果有数据那么就会把数据从内核拷贝到用户进程. 虽然叫 nonblocking 但是在把数据从内核拷贝到用户进程 (写数据是从用户进程到内核) 时还是会阻塞住.

I/O multiplexing

在这种模型下, 我们一个线程能 同时 处理多个 I/O. 还是以 socket 为例, 我们将一个 socket 的集合传给 select() 函数, 调用 select() 后线程会阻塞住, 当传入的集合中有一个 socket 有数据了, select() 就会返回, 然后通过相应的方法将数据读取, 同样的, 在读取数据时线程会被阻塞住. 传给 select() 的 socket 可以是 blocking 的. 因为阻塞与否影响的只是 recv(), send(), connect(), accept() 这四个函数. 但是最好是 nonblocking 的, 因为即使 select() 返回了, socket 也可能在调用那四个函数时被阻塞. (见 select with blocking and non-blocking socket)

select, poll, epoll

这三个函数是 I/O 多路复用模型常用的函数, 各自实现的功能差不多.

select 传入的文件描述符的集合的大小是有限制的, 通常是 1024. poll 和 epoll 没有限制 (主要的限制是你的硬件资源了). select 和 poll 都会随着传入的文件描述符的数量增多而效率变低, epoll 的效率只和活跃的文件描述符数量有关. epoll 使用消息驱动来通知用户进程数据就绪.

epoll 怎么实现的我还蛮好奇的, 为什么 epoll 通常情况下效率要高呢? epoll 比 select 高效的原因

Signal-Driven I/O

在这个模型里, 我们向操作系统注册 handler (通过 sigaction 系统调用), 当数据就绪时, handler 会被操作系统调用, 这时候我们再去调用相应的方法获取数据 (会阻塞住).

Asynchronous I/O

和 Signal-Driven 类似, 我们需要向操作系统注册一个 handler. 这个模型和 Signal-Driven 最大的不同是, 当操作系统调用我们的 handler 时, 数据已经从内核态拷贝到用户进程了 (显然我们在注册 handler 的时候需要让操作系统知道把数据拷贝到哪里).

Synchronous I/O versus Asynchronous I/O

POSIX (Portable Operating System Interface) 其实只定义了两种类型的 I/O:

  • 同步的: 进行 I/O 操作时, 之前提到的两个过程中只要有一个会阻塞就算同步的
  • 异步的: 进行 I/O 操作时, 之前提到的两个过程都不会阻塞就是异步的

所以, 上面提到的五种 I/O 模型, 只有 Asynchronous I/O 是属于异步的, 其他的都属于同步的, 因为他们在把数据从内核拷贝到用户进程时都会阻塞住.

举个例子

找资料过程中发现用钓鱼来类比上面的几个模型是很合适的. 因为钓鱼成两个步骤, 等鱼儿上钩 (数据就绪), 把鱼儿放进桶里 (把数据从内核拷贝到用户进程)

  • Blocking: 下饵之后就一直等着直到鱼儿上钩, 再把鱼儿放到桶里.
  • Nonblocking: 下饵之后隔一会就来检查下有没有鱼儿上钩, 没有就做其他事情, 有就把鱼儿放进桶里.
  • I/O multiplexing: 同时用很多鱼竿, 然后在旁边看 (不能干其他事情), 哪个鱼竿有鱼了就捞上来放进桶里.
  • Signal-Driven: 用的是比较高级的鱼竿, 当有鱼上钩时会发出声音提醒 (可以专心做其它事情, 不用像 nonblocking 一样亲自来检查), 听到声音提醒之后把鱼儿捞起来放进桶里.
  • Asynchronous: 直接雇了个人 (操作系统), 当鱼上钩后捞起来放进桶里, 然后被雇佣的人会过来告诉你鱼钓到了.

6.2 I/O Models @ UNIX Network Programming

poll vs select vs event-based

Linux IO模式及 select、poll、epoll详解

看懂 Gradle

Gradle 的脚本第一眼看上去很难把它和一门编程语言联系起来. 但其实 Gradle 的脚本是基于 Groovy 语言的. 只不过 Gradle 利用了很多 Groovy 的特性, 或者说是语法糖. 今天我们就来分析下下面这段 Gradle 脚本.

apply plugin: 'java'

task 'myTask' {
    group = 'myTasks'
    description 'Prints $description and ${group}'
} << {
    println "$description"
    println "$group"
}

简单的语法糖

Groovy 支持很多比较常见的语法糖, 比如语句后面的分号可以省略.

字符串插值

Groovy 中字符串可以使用单引号和双引号. 区别在于双引号是支持插值的:

def name = 'Guillaume' // a plain string
def greeting = "Hello ${name}"

assert greeting.toString() == 'Hello Guillaume'

我们可以使用 ${}$ 来表示插值. ${}{} 中可以是任何表达式, 而 $ 的后面只可以接带 . 的表达式:

def sum = "The sum of 2 and 3 equals ${2 + 3}"
assert sum.toString() == 'The sum of 2 and 3 equals 5'

def sum12 = "The sum of 1 and 2 is equal to ${def a = 1; def b = 2; a + b}"
assert sum12.toString() == 'The sum of 1 and 2 is equal to 3'

def person = [name: 'Guillaume', age: 36]
assert "$person.name is $person.age years old" == 'Guillaume is 36 years old'

对于 $ 只有带点的表达式是可以的, 所以 "$number.toString()" 会被解释成 "${number.toString}()"

字面表示

Groovy 中对于常见的数据结构都有字面表示方法:

def list = [1, 4, 6, 9]

// by default, keys are Strings, no need to quote them
// you can wrap keys with () like [(variableStateAcronym): stateName] to insert a variable or object as a key.
def map = [CA: 'California', 'MI': 'Michigan']

def range = 10..20
def pattern = ~/fo*/

Groovy 函数支持 named argument, 但其实 Groovy 会把他们转换成 Map 然后传给函数. 所以函数声明的参数的类型是个 Map:

apply(plugin: 'java') // 等于 apply([plugin: 'java'])

def foo(Map m, s) { // Map 需要放到第一个
    println m
    println s
}

foo(name: 'John', 'ugly', age: 24) // 等于 foo([name: 'John', age: 24], 'ugly')

括号

Groovy 允许我们在 top-level 语句中省略掉函数调用的括号. 例如:

println "Hello"
method a, b // 注意中间的逗号

vs:

println("Hello")
method(a, b)

但是对于没有参数的函数的调用是不能省略括号的. 同时, 不是 top-level 语句的函数调用的括号也是不能省略的. 下面两种情况都是不可以的省略括号的:

def foo(n) { n }
def bar() { println 'bar' }

// 下面的语句都不是合法的函数调用
println foo 1 // won't work
def m = foo 1
bar

Command Chains

写代码时我们经常会使用链式调用 (a(b).c(d)), 利用 Groovy 可以省略括号的特性, 我们可以非常简洁的进行链式调用 (a b c d), 不单单括号去掉了连中间的 . 都省略了.

// equivalent to: turn(left).then(right)
turn left then right

如果链式调用中的某个方法没有参数, 那么这个括号是不可以省略的:

// equivalent to: select(all).unique().from(names)
select all unique() from names

如果链式调用中的元素的个数是奇数, 那么最后一个元素将会是获取属性:

// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies

所以文章最开头的脚本, 补上括号后是这样的:

apply([plugin: 'java'])

task('myTask') {
    group = 'myTasks'
    description('Prints $description and $group')
} << {
    println("$description")
    println("$group")
}

等一下, task('myTask')<< 后面的 {} 是什么鬼? 他们是我们下面要介绍的 Closure.

Closure

Closure 其实就是匿名函数. 语法定义是这样的:

{ [closureParameters -> ] statements }

下面是一些合法的 Closure 定义:

{ item++ }                                          

{ -> item++ }                                       

{ println it }                                      

{ it -> println it }                                

{ name -> println name }                            

{ String x, int y ->                                
    println "hey ${x} the value is ${y}"
}

{ reader ->                                         
    def line = reader.readLine()
    line.trim()
}

如果函数的最后一个参数是 Closure, 那么这个 Closure 是可以拿到括号外面的.

所以 task('myTask', {}) 可以写成 task('myTask') {}.

Delegate

Closure 是有自己的上下文 (Context) 的, 大多数语言的 Closure 的上下文就是 Closure 定义的时候所在的上下文. Groovy 的 Closure 比较不同的地方在于, 它可以重新定义 Closure 的上下文即 Delegate.

def myVar = 'Hello World!'
def myClosure = { println myVar }

class MyClass {
    def myVar = 'Hello from MyClass!'
}

MyClass m = new MyClass()
myClosure.setDelegate(m)
myClosure() // 输出: Hello from MyClass!

下面脚本中的 Closure 就被 Gradle 重新设置了上下文, 所以我们才可以使用 groupdescription.

task 'myTask' {
    group = 'myTasks'
    description 'Prints $description and ${group}'
}

隐式 Getter / Setter

在 Groovy 中访问和修改属性都是会默认调用 Getter 和 Setter 方法的. 所以 group = 'myTasks' 等价于 setGroup('myTasks'). 可以使用 obj.@prop 来跳过 Getter 和 Setter 方法.

不过 description '...' 可不等价于 setDescription('...'), 而是等价于我们之前说的方法调用 description('...'), 可是查文档我们会发现 description 是个属性啊! 这是因为 Gradle 默认会为一些属性添加一个同名的方法. 所以我们可以用 group = '...' 也可以用 group '...'.

符号重载

Groovy 是支持符号重载的:

Operator Method
a + b a.plus(b)
a - b a.minus(b)
a * b a.multiply(b)
a ** b a.power(b)
a / b a.div(b)
a % b a.mod(b)
a | b a.or(b)
a & b a.and(b)
a ^ b a.xor(b)
a++ or ++a a.next()
a-- or --a a.previous()
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)
a << b a.leftShift(b)
a >> b a.rightShift(b)
a >>> b a.rightShiftUnsigned(b)
switch(a) { case(b) : } b.isCase(a)
if(a) a.asBoolean()
~a a.bitwiseNegate()
-a a.negative()
+a a.positive()
a as b a.asType(b)
a == b a.equals(b)
a != b ! a.equals(b)
a <=> b a.compareTo(b)
a > b a.compareTo(b) > 0
a >= b a.compareTo(b) >= 0
a < b a.compareTo(b) < 0
a <= b a.compareTo(b) <= 0

所以最终我们最开始的 Gradle 脚本就变成了这样:

apply([plugin: 'java']);

task('myTask', {
    setGroup('myTasks');
    description('Prints $description and $group');
}).leftShif({
    println("$description");
    println("$group");
})

这样是不是熟悉多了?

Compilation customizers

通常我们定义一个 task 会这样写 task myTask {}, myTask 外面是没有引号的. 这样的话按照我们上面的说法这就会等价于 task(myTask) {}, 可是 Gradle 并没有为我们定义 myTask 哇.

其实这是 Gradle 利用了 Groovy 的 Compilation customizers. 显然, Gradle 要执行我们写的 build.gradle, 必然会在代码中调用 Groovy 提供的接口来编译运行脚本, Groovy 比较灵活的地方在于, 我们可以配置 Groovy 的编译器:

import org.codehaus.groovy.control.CompilerConfiguration

// create a configuration
def config = new CompilerConfiguration()
// tweak the configuration
config.addCompilationCustomizers(...)
// run your script
def shell = new GroovyShell(config)
shell.evaluate(script)

我们上面的 myTask 就是 Gradle 利用 CompilationCustomizer 修改了 AST (Abstract Syntax Tree), 类似 Java 的字节码修改. 这个比较进阶, 可以参考下面给出的链接进一步了解.

总结

知道了这些 Groovy 的特性, 应该就能看懂 Gradle 的脚本了. 我们也可以发现 Groovy 是一门非常灵活的语言, 所以这也使得 Groovy 非常适合作为 DSL (Domain-Specific Languages) 的开发.

Gradle tip #2: understanding syntax

看懂Gradle脚本(1)- Groovy语言的Map语法糖

看懂Gradle脚本(3)- Groovy AST转换

Groovy Style Guide

Groovy Domain-Specific Languages

Groovy Closure

Groovy Syntax

Groovy Operators