xiaobaoqiu Blog

Think More, Code Less

Build Openjdk

前几天看了微博上build openJDK 的一篇文章, 决定也尝试一下自己 build 出来一个 JDK.下面是 build 的过程以及遇到的一些问题及解决办法. 本文基于 Ubutu 14.04 LTS 版本.

build 过程涉及 openJDK 部分源码的修改, 修改完的代码我上传到github了, 地址: https://github.com/xiaobaoqiu/ubuntu_build_openjdk7.git

1.准备工作

OpenJDK 官网: http://openjdk.java.net/

注意OpenJDK的版本控制使用 Mercurial 做的.

下面是 build 之前的一些准备工作, 主要包含几个:

1.安装 Mercurial

Mercurial 官网: https://www.mercurial-scm.org/

1
sudo apt-get install mercurial

2.下载 openJDK 源码

1
hg clone http://hg.openjdk.java.net/jdk7/jdk7

之后, 进入源码目录, 有个 get_source.sh 的脚本, 执行就可以了.

3.安装 build 需要的工具

就是 g++ 之类的 make 需要的工具.

1
sudo apt-get install gawk g++ libcups2-dev libasound2-dev libfreetype6-dev libx11-dev libxt-dev libxext-dev libxrender-dev libxtst-dev libfontconfig1-dev

4.准备一个BOOT JDK

需要注意, 如果 build openJDK7, 需要准备 JDK6(如果用JDK7, 会碰到一个错误, 后面会讲到).

2.Build

写了简单的小shell, 配置一些环境变量(如 BOOT JDK 位置).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cd ./jdk7

unset JAVA_HOME

export LANG=C
export ALT_BOOTDIR="/home/q/java/jdk1.6.0_45"

# 并行编译线程数,与CPU核数一致
export HOTSPOT_JOBS=8
export ALT_PARALLEL_COMPILE_JOBS=8

export ALLOW_DOWNLOADS=true

export SKIP_COMPARE_IMGAGES=true
export USE_PRECOMPILED_HEADER=true

# Compiling contents
export BUILD_LANGTOOLS=true
export BUILD_HOTSOPT=true
export BUILD_JDK=true

# Avoid javaws & applet build
BUILD_DEPLOY=false
# Avoid installation build
BUILD_INSTALL=false
export EXTRA_LIBS=/usr/lib/x86_64-linux-gnu/libasound.so
make sanity && time make

和源码在同级目录(自己调整):

1
2
3
4
xiaobaoqiu@xiaobaoqiu:~/Work/SourceCode/jdk/openjdk7$ ll
-rwxrwxr-x  1 xiaobaoqiu xiaobaoqiu  735  2月  4 11:48 build7.sh*
drwxrwxr-x  4 xiaobaoqiu xiaobaoqiu 4096  2月  3 17:30 hgforest-crew/
drwxrwxr-x 14 xiaobaoqiu xiaobaoqiu 4096  2月  4 14:01 jdk7/

之后, 执行 build7.sh 脚本就行了(需要chmod +x build7.sh). 幸运的话就没问题了(基本不可能没问题), 下面是我碰到的一些问题和解决办法.

3.问题

下面是 build 过程的一些问题总结.

1.This OS is not supported

错误信息大致如下:

1
"*** This OS is not supported:" `uname -a`; exit 1;

这是由于内核版本太高了, 需要简单修改 Makefile, 首先看一下当前的 Linux 系统版本:

1
2
xiaobaoqiu@xiaobaoqiu:~/Work/SourceCode/jdk/openjdk7$ uname -r
3.13.0-63-generic

在 jdk7/hotspot/make/linux/Makefile 文件中修改 SUPPORTED_OS_VERSION, 增加自己的内核版本支持

1
SUPPORTED_OS_VERSION = 2.4% 2.5% 2.6% 2.7% 3.13%

2.error: “__LEAF” redefined

这是 openJDK 源码和 gcc 源码冲突, 一个bug, 参见: http://bugs.java.com/view_bug.do?bug_id=7103224

需要按照 http://hg.openjdk.java.net/lambda/lambda/hotspot/rev/a6eef545f1a2 修改代码, 涉及的代码包括以下 6 个文件, 具体的完整代码可以从文章开头提供的 github 地址下载.

1
2
3
4
5
6
jdk7/hotspot/src/share/vm/opto/addnode.cpp
jdk7/hotspot/src/share/vm/prims/jniCheck.cpp
jdk7/hotspot/src/share/vm/prims/jvmtiEnter.xsl
jdk7/hotspot/src/share/vm/prims/jvmtiEnv.cpp
jdk7/hotspot/src/share/vm/prims/jvmtiExport.cpp
jdk7/hotspot/src/share/vm/runtime/interfaceSupport.hpp

3.error: converting ‘false’ to pointer type ‘methodOop’

错误信息:

1
2
3
4
/home/xiaobaoqiu/Work/SourceCode/jdk/openjdk7/jdk7/hotspot/src/share/vm/oops/constantPoolOop.cpp:272:39: error: converting 'false' to pointer type 'methodOop' [-Werror=conversion-null]
   if (cpool->cache() == NULL)  return false;  // nothing to load yet
                                       ^
cc1plus: all warnings being treated as errors

修改(具体的完整代码可以从文章开头提供的 github 地址下载.): /openjdk/hotspot/src/share/vm/oops/constantPoolOop.cpp 第272行 return false改为return (methodOop)false;

4.error: converting ‘false’ to pointer type ‘Node’*

错误信息

1
2
3
4
/home/xiaobaoqiu/Work/SourceCode/jdk/openjdk7/jdk7/hotspot/src/share/vm/opto/loopnode.cpp:896:49: error: converting 'false' to pointer type 'Node*' [-Werror=conversion-null]
   if (expr == NULL || expr->req() != 3)  return false;
                                                 ^
cc1plus: all warnings being treated as errors

修改(具体的完整代码可以从文章开头提供的 github 地址下载.): openjdk/hotspot/src/share/vm/opto/loopnode.cpp: 第896行 return false改为return (Node*)false;

5.Unable to load native library: /home/q/java/jdk1.7.0_80/jre/lib/amd64/libjava.so

错误信息:

1
Unable to load native library: /home/q/java/jdk1.7.0_80/jre/lib/amd64/libjava.so: symbol JVM_SetNativeThreadName, version SUNWprivate_1.1 not defined in file libjvm.so with link time reference

解决: 使用 JDK6 作为BOOT JDK

6.gcc: error: unrecognized command line option ‘-mimpure-text’

这个-mimpure-text是gcc给Solaris的编译选项,所以注释掉即可

修改/jdk/make/common/shared/Compiler-gcc.gmk, 去掉 -mimpure-text (具体的完整代码可以从文章开头提供的 github 地址下载.)

7. Error: time is more than 10 years from present: 1136059200000

错误信息

1
2
3
4
5
6
Error: time is more than 10 years from present: 1136059200000
java.lang.RuntimeException: time is more than 10 years from present: 1136059200000
  at build.tools.generatecurrencydata.GenerateCurrencyData.makeSpecialCaseEntry(GenerateCurrencyData.java:285)
  at build.tools.generatecurrencydata.GenerateCurrencyData.buildMainAndSpecialCaseTables(GenerateCurrencyData.java:225)
  at build.tools.generatecurrencydata.GenerateCurrencyData.main(GenerateCurrencyData.java:154)
make[4]: *** [/home/xiaobaoqiu/Work/SourceCode/jdk/openjdk7/jdk7/build/linux-amd64/lib/currency.data] Error 1

修正 jdk7/jdk/src/share/classes/java/util/CurrencyData.properties 文件, 保证时间在 10 年之内.(具体的完整代码可以从文章开头提供的 github 地址下载.):

1
2
3
4
5
AZ=AZM;2005-12-31-20-00-00;AZN
MZ=MZM;2006-06-30-22-00-00;MZN
RO=ROL;2005-06-30-21-00-00;RON
TR=TRL;2004-12-31-22-00-00;TRY
VE=VEB;2008-01-01-04-00-00;VEF

4.后续

经过一番折腾之后, build 成功:

1
2
3
4
5
6
7
8
9
10
11
12
-- Build times ----------
Target all_product_build
Start 2017-02-04 12:02:33
End   2017-02-04 12:14:00
00:00:07 corba
00:00:05 hotspot
00:00:07 jaxp
00:00:05 jaxws
00:10:59 jdk
00:00:04 langtools
00:11:27 TOTAL
-------------------------

默认 build 的结果在 jdk7/build/linux-amd64 下:

1
2
3
4
xiaobaoqiu@xiaobaoqiu:~/Work/SourceCode/jdk/openjdk7/jdk7/build/linux-amd64/bin$ ./java -version
openjdk version "1.7.0-internal"
OpenJDK Runtime Environment (build 1.7.0-internal-root_2017_02_04_11_48-b00)
OpenJDK 64-Bit Server VM (build 21.0-b17, mixed mode)

需要 build openJDK8 也基本一样, 下载源码地址变一下, openJDK8 源码没有需要该的, 一路通畅:

脚本稍微变更(注意需要 configure , 参考其自带的 README-builds.html):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cd ./jdk8

unset JAVA_HOME

export LANG=C
export ALT_BOOTDIR="/home/q/java/default"

# 并行编译线程数,与CPU核数一致
export HOTSPOT_JOBS=8
export ALT_PARALLEL_COMPILE_JOBS=8

export ALLOW_DOWNLOADS=true

export SKIP_COMPARE_IMGAGES=true
export USE_PRECOMPILED_HEADER=true

# Compiling contents
export BUILD_LANGTOOLS=true
export BUILD_HOTSOPT=true
export BUILD_JDK=true

# Avoid javaws & applet build
BUILD_DEPLOY=false
# Avoid installation build
BUILD_INSTALL=false
export EXTRA_LIBS=/usr/lib/x86_64-linux-gnu/libasound.so
#export EXTRA_LIBS=/usr/lib/x86_64-linux-gnu/libasound.so.2
bash ./configure
time make all

build 成功:

1
2
3
4
xiaobaoqiu@xiaobaoqiu:~/Work/SourceCode/jdk/openjdk8/jdk8/build/linux-x86_64-normal-server-release/jdk/bin$ ./java -version
openjdk version "1.8.0-internal"
OpenJDK Runtime Environment (build 1.8.0-internal-root_2017_02_06_11_46-b00)
OpenJDK 64-Bit Server VM (build 25.0-b70, mixed mode)

Api Blocking

1.背景

接口限流是保证系统稳定性的三大法宝之一(缓存, 限流, 降级).

本文使用三种方式实现Api限流, 并提供了一个用Spring实现的Api限流的简单Demo, Demo的git地址: https://github.com/xiaobaoqiu/api-blocking

其中接口限流配置在文件 blocking-config.properties 中, 内容实例如下:

1
2
3
4
5
6
7
8
9
10
11
# 每一块审核一个限流配置,一块内的起始数字相同,数字依次往下递增
# 每一块由一下四个信息组成:
#   name - 请求url
#   redirectUrl - 请求被阻塞的时候跳转的 url
#   duration,limit - 在 duration 秒的时间内最多访问 limit 次

# 表示接口 /business/detail.json 10 秒只能被访问 2 次, 超过的请求讲被跳转到 /noAuth 上
0.url=/business/detail.json
0.redirectUrl=/noAuth
0.duration=10
0.limit=2

里面包含了三种方式来实现限流, 下面将主要审核分别详细介绍三种方式:

1.Redis
2.滑动窗口
3.Guava的RateLimiter

2.Redis实现限流

Redis的官网的命令手册的例子就是如何使用 incr 指令实现接口限流.参见官网: https://redis.io/commands/incr/

简单说就是每个请求生成一个key(可以根据IP + 接口url生成, 也可以直接根据接口url生成), value为计数值. 设置过期时间.

需要注意 Redis 的过期策略是混合的:

1.被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key;
2.主动删除:Redis会定期(默认好像是100ms)主动淘汰一批已过期的key;当已用内存超过限定时, 也会触发主动清理策略;

3.滑动窗口实现限流

大家都知道TCP中的滑动窗口有调节发送速率的作用.这里是一个类似的想法.

按照我们的配置, 我们期望 duration 时间内最多 limit 个请求, 我们可以想象有一个事件窗口, 其宽度就是 duration, 因为每个请求都有一个时间戳(可以用Long表示), 每次请求过来的时候, 我们只需要校验当前请求为尾端的时间窗口内的请求数目是否满足 limit 需求就行了.

实现很简单, 使用一个环形队列就行.具体参考 demo 代码.

4.RateLimiter实现限流

直接使用Guava提供的RateLimiter实现.

RateLimiter的原理参考: http://xiaobaoqiu.github.io/blog/2015/07/02/ratelimiter/

ShallowEtagHeaderFilter

1.背景

之前组内遇到的一个 ShallowEtagHeaderFilter 导致流式导出失效的问题, 具体的问题分析不是我做的, 这里记录下问题及原理.

现象就是一个数据导出接口, 用户的频繁调用导致线上内存吃满, 进而频繁 GC 导致 CPU 过高最终影响用户请求.

2.Mysql流式读取

先交代一下导出接口的实现方式, 考虑到导出数据可能比较大导致撑爆内存, 数据导出是流式导出的.

默认情况下, 我们的select从服务器一次取出所有数据放在客户端内存中, 当一条SQL返回数据量较大时可能会出现OOM.

这里首先涉及到Mysql如何配置流式读取数据, 涉及到两个配置(第一个配置很重要很容易被忘记)

1.Mysql连接串中配置上参数useCursorFetch=true;
2.Sql中配置上FetchSize等参数

下面以MyBatis为例子, 通过设置Mapper文件中select的属性来启用这一特性

1
2
3
<select id="xxx" fetchSize="100" resultSetType="FORWARD_ONLY">
  ...
</select>

然后, 配合 ResultHandler 就可以实现逐行流式处理数据库中的记录.

我们就是采用这种方式, 流式获取数据写到 Response 的输出流中.

3.ShallowEtagHeaderFilter

3.1 Content-Length 和 Transfer-Encoding

在描述问题之前, 我们先普及一下 Http 请求中 Content-Length 和 Transfer-Encoding 的知识.

Content-Length 用于描述HTTP消息实体的传输长度(the transfer-length of the message-body), 需要注意消息实体长度和消息实体传输长度是有区别, 比如说gzip压缩下, 消息实体长度是压缩前的长度, 消息实体的传输长度是gzip压缩后的长度.

通过读取 Content-Length 这个信息, 服务器/客户端可以预先知道数据量的大小, 以便预先分配空间.

Transfer-Encoding 所描述的是消息请求(request)和响应(response)所附带的实体对象(entity)的传输形式. 可选值有: chunked 和 identity, 其中chunked指把要发送传输的数据切割成一系列的块数据传输, identity指传输时不做任何处理, 自身的本质数据形式传输. 举个例子, 如果我们要传输一本小说到服务器, chunked方式就会先把这本小说分成一章一章的, 然后逐个章节上传, 而identity方式则是从小说的第一个字按顺序传输到最后一个字结束.

HTTP/1.1 规定, 所有服务器必须支持 Chunked 类型的 Transfer-Encoding, 通过这个头部, 服务器不必在返回回应报文的时候预先知道返回 Body 的大小, 而是将数据分割到一个一个的 Chunk 中(每个 Chunk 通过特定的头部和尾部区分, 最后一个 Chunk 大小为 0, 以表示报文结束), 然后持续不断地写入 TCP 流, 通过这个机制, 可以实现服务器数据到客户端的流式输出.

注意关于 Content-Length 和 Transfer-Encoding 一个准则: 有了Transfer-Encoding, 则不能有Content-Length

参考: https://tools.ietf.org/html/rfc2616#section-4.4. http://www.cnblogs.com/jcli/archive/2012/10/19/2730440.html http://blog.csdn.net/pud_zha/article/details/8809878

3.2 问题分析

按照我们上面的分析, 我们的 Response 的 Transfer-Encoding 肯定是 chunked, 并且没有设置 Content-Length.但是我们发现请求的 Response 中存在 Content-Length 字段.

我们将请求的 Response 分为三个阶段: (1).从数据库读取数据; (2).服务器端整理数据发往浏览器端; (3).浏览器端接受数据;

现在已知从 Mysql 中读取数据是流式的, 而浏览器接收到的数据不是流式的, 因此问题审核出在服务器端.

最终定位到的罪魁祸首是一个 ShallowEtagHeaderFilter 的 Filter.

3.3 ShallowEtagHeaderFilter原理

ShallowEtagHeaderFilter 继承了 OncePerRequestFilter, 我们可以分析一下 ShallowEtagHeaderFilter 的主要代码逻辑:

1
2
3
4
5
6
7
8
9
10
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {

  response = new ShallowEtagResponseWrapper(response);    //1.将所有的请求包装成 ShallowEtagResponseWrapper

  filterChain.doFilter(request, response);        //2. 其他 filter

  updateResponse(request, response);      //3.返回给客户端之前处理一次 response
}

看一下包装 ShallowEtagResponseWrapper 做了什么: (其实看注释就已经知道, 将所有写到 output stream 和 writer 的内容缓冲起来放到, 并提供 oByteArray() 方法用于读取缓冲的数据):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
 * {@link HttpServletRequest} wrapper that buffers all content written to the
 * {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer},
 * and allows this content to be retrieved via a {@link #toByteArray() byte array}.
 */
private static class ShallowEtagResponseWrapper extends HttpServletResponseWrapper {

  private final ByteArrayOutputStream content = new ByteArrayOutputStream();

  private final ServletOutputStream outputStream = new ResponseServletOutputStream();

  private PrintWriter writer;
  ...
  public PrintWriter getWriter() throws IOException { //获取 writer, 采用自定义的 ResponsePrintWriter, 后续的写会将内容写到 content 中
      if (this.writer == null) {
          String characterEncoding = getCharacterEncoding();
          this.writer = (characterEncoding != null ? new ResponsePrintWriter(characterEncoding) :
                  new ResponsePrintWriter(WebUtils.DEFAULT_CHARACTER_ENCODING));
      }
      return this.writer;
  }
  private byte[] toByteArray() {  // 读取 content 内容
      return this.content.toByteArray();
  }

  // 自定义 ServletOutputStream, write操作将内容写入到 content 中
  private class ResponseServletOutputStream extends ServletOutputStream {
      @Override
      public void write(int b) throws IOException {
          content.write(b);   //写到 content 中
      }
      ...
  }

  // 自定义 PrintWriter, 写入到 content 中
  private class ResponsePrintWriter extends PrintWriter {
      private ResponsePrintWriter(String characterEncoding) throws UnsupportedEncodingException {
          super(new OutputStreamWriter(content, characterEncoding));
      }
      ...
  }
}

最后我们看看 updateResponse 做了什么事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void updateResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
  ...
  byte[] body = responseWrapper.toByteArray();        //缓冲起来的数据
  int statusCode = responseWrapper.getStatusCode();

  if (isEligibleForEtag(request, responseWrapper, statusCode, body)) {
      String responseETag = generateETagHeaderValue(body);    //基于内容生成 ETag
      response.setHeader(HEADER_ETAG, responseETag);      //设置 ETag Header

      String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);   //If-None-Match 
      if (responseETag.equals(requestETag)) {                 //请求的If-None-Match和ETag相同, 则304
          response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);    //304
      } else {
          copyBodyToResponse(body, response);     //将缓冲的内容写入到 response 中
      }
  } else {
      ...
      copyBodyToResponse(body, response);
  }
}

private void copyBodyToResponse(byte[] body, HttpServletResponse response) throws IOException {
  if (body.length > 0) {
      response.setContentLength(body.length);         //设置 Content-Length
      FileCopyUtils.copy(body, response.getOutputStream());       //ShallowEtagResponseWrapper数据拷贝到response中
  }
}

总结 ShallowEtagHeaderFilter 作用: 1.ShallowEtagHeaderFilter 缓冲了写到 response 的所有数据; 2.如果请求的 If-None-Match 头部信息和当前根据内容生成的 ETag 相同,则请求为 304; 3.最后 ShallowEtagHeaderFilter 将缓冲的所有内容一次性写到 response 中;

需要注意的是, 使用这个 ShallowEtagHeaderFilter, 服务端的性能并没有得到提高, 因为请求逻辑还是执行了, 只是没往客户端发送, 因此节省了一定的网络带宽.

另外, 这个 ShallowEtagHeaderFilter 会让 Transfer-Encoding=Chunked 失效(我们这里就是这种情况).

3.4 解决方案

根据上面的分析, 我们这里的问题就很好解释了, ShallowEtagHeaderFilter 将我们的流式写缓冲起来, 最后一次性写给客户端并且设置上了 Content-Length 字段.

关于这个问题, 其实很多人都遇到坑了, 有人给 Spring 提 Bug 了, 但是官网认为只是误用(我赞同这种观点). 参考: https://jira.spring.io/browse/SPR-10855

解决很简单, 去掉 ShallowEtagHeaderFilter 这个过滤器, 或者给 ShallowEtagHeaderFilter 配置 url pattern 的时候排除掉流式的接口.

Activiti自动流转任务

1.自动流转任务

用户任务是审核流程中最重要的节点, 在流程设计中通常会遇到需要自动流转的需求, 比如请假半天的自动通过.

2.实现

下面是我能想到的几种实现自动流转任务方式.

2.1 ServiceTask

Activiti的设计者已经考虑到我们这个需求,因此在引入了 ServiceTask 这中类型的任务.参考 Activiti 的文档: https://www.activiti.org/userguide/index.html#bpmnJavaServiceTask ServiceTask的目的是调用我们自定义的一个Java类:

1
A Java service task is used to invoke an external Java class.

ServiceTask 的使用方式也有几种:

1. 自定义类实现 JavaDelegate 或者 ActivityBehavior 接口;
2. 设计流程的时候, 显示的告诉 Activiti 引擎需要调用的 Bean;
3. 调用一个方法表达式;
4. 调用一个求值表达式;

下面审核一个示例的自定义ServiceTask调研逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Service;

/**
 * 自动完成的任务(ServiceTask)
 *
 * @author xiaobaoqiu  Date: 16-12-23 Time: 下午3:46
 */
@Service
public class AutoCompleteTaskService implements JavaDelegate {

    @Override
    public void execute(DelegateExecution execution) throws Exception {

    }
}

回到我们的需求上, 要实现我们需要的自动流转任务, 我们可以使用 ServiceTask, 以上面定义的 AutoCompleteTaskService 为例子:

1
2
3
4
5
6
7
8
# 方式1
<serviceTask id="javaService"
             name="My Java Service Task"
             activiti:class="org.xxx.AutoCompleteTaskService" />

# 方式2
<bean id="delegateExpressionBean" class="org.xxx.AutoCompleteTaskService" />
<serviceTask id="serviceTask" activiti:delegateExpression="${delegateExpressionBean}" />

但是这种实现方式的一个致命的问题在于:流程设计的时候你需要知道你要调用的Java逻辑的类路径或者Bean的名称.

为什么说是致命的呢,因为这个的实现方式就没办法将流程设计交付给开发人员以外的其他人员了(他们不可能知道类路径或者bean名称这种信息).

下面我使用普通的 UserTask 来实现我们的需求.

2.2 主动触发UserTask,

我们将自动流转任务也设计成UserTask, 由业务自己主动触发.

比如请假半天的自动通过的例子, 业务可以在提交请假的时候判断, 让这个任务自动完成.

这种实现的问题在于: 业务逻辑实现起来太麻烦, 很容易出Bug(比如自动通过失败,可能会导致提交这个操作失败).

2.3 异步触发UserTask

在上一个方法的基础上, 我们可以将自动生效的逻辑放到异步任务里面,可以避免将原始业务逻辑复杂化.

但是我们需要筛选出哪些 UserTask 可以自动完成,因此我的方案就是给这些任务制定特定的任务类型.关于任务类型, 可以参考我前面的博文: http://xiaobaoqiu.github.io/blog/2016/12/29/activitiren-wu-zeng-jia-shu-xing/

有了任务类型之后, 我们就可以将哪些任务需要自动流转配置起来.

这个方案的问题在于(绝大部分情况下不是问题): 这里实现的自动生效的不是实时的(即使我们的异步任务足够频繁).

2.4 对比选择

下面是几种实现方式的对比:

成本 ServiceTask 主动触发UserTask 异步触发UserTask
设计成本 需要知道具体的执行类或者Bean
触发成本 需要业务主动触发, 逻辑复杂
配置成本 需要配置:任务类型 - 是否自动完成任务

倾向于使用 异步触发UserTask 这个方案, 成本和问题都比较小.

Spring上传组件冲突

最近会整理记录这一段时间组内同事以及个人遇到的一些问题。

1.背景

问题:Spring上传组件的冲突 背景:同事在原有的上传模块中增加了一个新的文件接口,导致就的上传接口不可用的故障。

新增的配置就是Spring自带的上传解析器:

1
2
3
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="utf-8"/>
</bean>

示例代码如下:

1
2
3
ApiResult upload(@RequestParam("uploadFile") CommonsMultipartFile uploadFile) {
    ...
}

原始的上传使用的是 commons-fileupload 组件,简单示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
upload(HttpServletRequest request) {
        //1.从请求中解析出文件
        ServletFileUpload servletFileUpload = createServletFileUpload();
        List<FileItem> itemList = servletFileUpload.parseRequest(request);
        Map<String, Serializable> fields = getFileFields(itemList);
        FileItem file = (FileItem) fields.get("Filedata");

        //2.内容写本地
        File f = new File(generateFilePath(...));
        FileOutputStream out = new FileOutputStream(f);
        out.write(file.get());
}

2.Spring上传机制分析

首先我们从Spring处理请求的源头开始分析,看看Spring是如何支持文件上传的。

1.DispatcherServlet中doDispatch方法中,如果请求是文件上传,会首先将 HttpServletRequest 请求做预处理,转换为 MultipartHttpServletRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
processedRequest = checkMultipart(request);

    protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
        if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {    //是文件上传请求
            if (request instanceof MultipartHttpServletRequest) {   //已经是 MultipartHttpServletRequest 了
            }
            else {
                return this.multipartResolver.resolveMultipart(request);    //转换为 MultipartHttpServletRequest
            }
        }
        // If not returned before: return original request.
        return request;
    }

这块两个问题:

1.如何判定一个请求是不是文件上传请求
2.转换为 MultipartHttpServletRequest 做了什么事

先看如何判定审核文件上传请求,就是 multipartResolver.isMultipart 的代码,跟代码会涉及到 CommonsMultipartResolver, ServletFileUpload, FileUploadBase,发现判定有两个条件

(1). 请求是 Post 请求. 参见 ServletFileUpload.isMultipartContent()
(2).请求 contentType 必须以 multipart/ 开通. 参见 FileUploadBase.isMultipartContent()

下面看第二个问题,转换 MultipartHttpServletRequest 做的什么事,这个工作由 MultipartResolver.resolveMultipart 完成,我们可以看看注释:

    /**
     * Parse the given HTTP request into multipart files and parameters,
     * and wrap the request inside a
     * {@link org.springframework.web.multipart.MultipartHttpServletRequest} object
     * that provides access to file descriptors and makes contained
     * parameters accessible via the standard ServletRequest methods.
     */
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

其主要的代码实现在,主要的工作是, 首先将HttpServletRequest的inputStream最终塞入了FileItemStreamImpl的stream中,随后ServletFileUpload类逐个对FileItemStream进行处理(生成FileItem),通过Streams.copy方法对inputStream进行read操作,此时request中的inputStream被消耗(inputStream只能被读取一次), 最后将List返回,ServletFileUpload的解析方法执行完毕.

     public List<FileItem> parseRequest(RequestContext ctx) throws FileUploadException {
        List<FileItem> items = new ArrayList<FileItem>();
        FileItemIterator iter = getItemIterator(ctx);       //1. 构造FileItemStreamImpl
        ...
        while (iter.hasNext()) {                                            //2.逐个对FileItemStream进行处理
            final FileItemStream item = iter.next();
            ...
            FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName);       //2.1 构造一个 FileItem
            items.add(fileItem);
            ...
            Streams.copy(item.openStream(), fileItem.getOutputStream(), true);      //2.2 数据读取
        }
        return items;   // 3. 返回
    }

// 下面是更详细的代码
//1.将HttpServletRequest的inputStream最终塞入了FileItemStreamImpl的stream中
FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException {
    InputStream input = ctx.getInputStream();
    ...
    multi = new MultipartStream(input, boundary, notifier);
    ...
    findNextItem();
}

//2.2 数据读取,看看如何将 request 的 inputstream 数据拷贝到 FileItem 中
    /**
     * Copies the contents of the given {@link InputStream} to the given {@link OutputStream}.
     */
    public static long copy(InputStream inputStream, OutputStream outputStream, boolean closeOutputStream)
            throws IOException {
        return copy(inputStream, outputStream, closeOutputStream, new byte[DEFAULT_BUFFER_SIZE]);
    }

主要代码的位置:CommonsMultipartResolver.parseRequest, FileUploadBase.parseRequest, FileItemIteratorImpl

一个需要注意的点,在DispatcherServlet 中, MultipartResolver 的Bean的名称是写死的:

    /** Well-known name for the MultipartResolver object in the bean factory for this namespace. */
    public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";

    private void initMultipartResolver(ApplicationContext context) {
        try {
            this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);    //bean的name固定
            if (logger.isDebugEnabled()) {
                logger.debug("Using MultipartResolver [" + this.multipartResolver + "]");
            }
        }
    }

3.冲突分析

multipartResolver是一个全局的文件上传处理器,配置上 multipartResolver 这个Bean之后,全局的文件上传都会经过 multipartResolver 处理(读取并解析request的 inputstream ).而 inputstream 仅能处理一次,导致处理完的 HttpServletRequest 中的 inputStream 已经没有内容.

因此后面配置使用的 commons-fileupload 的 ServletFileUpload 无法从 request 中解析出文件上传内容.

4.问题解决

几个解决方案:

1.全局统一使用一个解析方式(统一使用 ServletFileUpload 或者 MultipartResolver 方式);
2.继承 CommonsMultipartResolver 实现自定义的 MultipartResolver, 覆写isMultipart方法, 仅部分 url 的上传请求走我们自定义的 MultipartResolver 处理器,保证新老逻辑兼容;