xiaobaoqiu Blog

Think More, Code Less

Guava Lazy Load引发的问题

最近碰到一个Guava Lazy Load导致的异常未被try-catch捕获,且未被spring全局的ExceptionHanler捕获的问题。这里简单总结一下。

1.代码

service的代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//service代码
public XXXVo searchList(...) {
    //从数据库查询
    List<XXXEntity> entityList = queryFromDb();

    //Guava Lists.transform转换
    return Lists.transform(entityList, new Function<XXXEntity, XXXVo>() {
                @Override
                public XXXVo apply(XXXEntity input) {
                    return convert(input);
                }
            });
        });
}

controller接口层是一个json接口,代码大致如下:

1
2
3
4
5
@RequestMapping("/search.json")
@ResponseBody
public ApiResult searchList_json(...) {
    return ApiResult.succ(xxxService.searchList(...));
}

代码存在的问题是没有try-catch一些业务异常。这里为了定位问题,try-catch了所有的异常:

1
2
3
4
5
6
7
8
9
10
@RequestMapping("/search.json")
@ResponseBody
public ApiResult searchList(...) {
    try{
        return ApiResult.succ(xxxService.searchList(...));   
    }catch(Exception e) {
        logger.error("...");
        throw new BusinessExceptidon(...);
    }   
}

2.问题

(1).access log显示search.json接口出现了500,但是try-catch没有抓到异常;
(2).全局配置的ExceptionHandler也没要抓到异常

3.Guava Lazy Load

这里使用的是Lists.transform()函数,这里涉及到一个Guava实现中常用的一个延迟加载的(Lazy Load)策略,包括在Splitter、Joinner等大量的类中广泛使用,其大致意思是,代码调用处不会真正执行实际的代码逻辑,在需要拿到处理后的数据的时候,才会去执行处理逻辑。

3.1 Guava Lazy Load示例

简单验证代码即其输出如下:

日志异常中展示出引发异常的是第82行的代码,即:

1
System.out.println(result.get(2));

3.2 Guava Lazy Load原理

这里简单分析一下Lists.transform()函数的Lazy Load的实现原理:

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
43
44
45
46
47
48
public static <F, T> List<T> transform(
      List<F> fromList, Function<? super F, ? extends T> function) {
    return (fromList instanceof RandomAccess)
        ? new TransformingRandomAccessList<F, T>(fromList, function)
        : new TransformingSequentialList<F, T>(fromList, function);
  }

private static class TransformingSequentialList<F, T>
      extends AbstractSequentialList<T> implements Serializable {
    final List<F> fromList;
    final Function<? super F, ? extends T> function;

    TransformingSequentialList(
        List<F> fromList, Function<? super F, ? extends T> function) {
      this.fromList = checkNotNull(fromList);
      this.function = checkNotNull(function);
    }
    /**
     * The default implementation inherited is based on iteration and removal of
     * each element which can be overkill. That's why we forward this call
     * directly to the backing list.
     */
    @Override public void clear() {
      fromList.clear();
    }
    @Override public int size() {
      return fromList.size();
    }
    @Override public ListIterator<T> listIterator(final int index) {
      return new TransformedListIterator<F, T>(fromList.listIterator(index)) {
        @Override
        T transform(F from) {
          return function.apply(from);
        }
      };
    }

    private static final long serialVersionUID = 0;
  }

//AbstractSequentialList.java
public E get(int index) {
        try {
            return listIterator(index).next();
        } catch (NoSuchElementException exc) {
            throw new IndexOutOfBoundsException("Index: "+index);
        }
    }

可以看出,Lists.transform()函数会生成内部类TransformingSequentialList的示例,TransformingSequentialList会保存原始的List引用和Function引用,只有在获取某一个元素的时候才会执行Function.apply()函数。

3.3 Lazy Load引发的问题

在我们的代码中,Service代码返回List数据的时候,其实是没有执行Function.apply()的逻辑,即XXXEntity到XXXVo的转换逻辑。

逻辑到达Controller的代码中,也没有机会执行Function.apply()的逻辑,直接用ApiResult.succ()包装并正常返回了。 这解释了为什么异常没有被try-catch捕获。

4.MappingJacksonHttpMessageConverter

下面解释一下json接口中return之后的逻辑。面是debug得到的主要两个步骤:

4.1 ServletInvocableHandlerMethod类对返回值进行处理

这里包含一系列的HandlerMethodReturnValueHandler,根据返回值的类型选择何时的返回值处理器,这里我们看到了一个RequestResponseBodyMethodProcessor,在这个处理器中就包含了我们熟悉的MappingJacksonHttpMessageConverter,用于将返回值序列化成json:

在MappingJacksonHttpMessageConverter中会调用writeInternal方法将对象序列化:

MappingJacksonHttpMessageConverter用于将对象转换为JSON(序列化, @ResponseBody注解)或者将JSON数据转换为对象(反序列化, @RequestBody注解),一般配置如下:

1
2
3
<bean id="jsonConverter" class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
   <property name="supportedMediaTypes" value="application/json" />
</bean>

MappingJacksonHttpMessageConverter序列化的时候执行其writeInternal方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
    protected void writeInternal(Object object, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {

        JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
        JsonGenerator jsonGenerator =
                this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding);

        // A workaround for JsonGenerators not applying serialization features
        // https://github.com/FasterXML/jackson-databind/issues/12
        if (this.objectMapper.getSerializationConfig().isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) {
            jsonGenerator.useDefaultPrettyPrinter();
        }

        try {
            if (this.prefixJson) {
                jsonGenerator.writeRaw("{} && ");
            }
            this.objectMapper.writeValue(jsonGenerator, object);    //序列化逻辑
        }
        catch (JsonProcessingException ex) {
            throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
        }
    }

更多的HttpMessageConverter见参考: http://www.ibm.com/developerworks/cn/web/wa-restful/

5.ExceptionHandler

5.1 DispatcherServlet异常处理

MappingJacksonHttpMessageConverter序列化出异常的时候,异常会进入著名的DispatcherServlet类的doDispatch()方法,最终进入processHandlerException()函数,这里会有一个List处理异常,处理逻辑如下:

1
2
3
4
5
6
7
ModelAndView exMv = null;
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
    exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
    if (exMv != null) {
    break;
    }
}

即某一个HandlerExceptionResolver成功处理了异常之后,后续的HandlerExceptionResolver就不会继续执行(所谓的责任链模式)。 这里我们发现我们自定义的CtExceptionHandler,它的顺序在最后:

我们自定义的HandlerExceptionResolver默认是DispatcherServlet中List的最后一个,前面包括三个默认的HandlerExceptionResolver:

1
2
3
ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver

5.2 HandlerExceptionResolver顺序

这些HandlerExceptionResolver的顺序是通过定义其顺序值(order)决定,值越小优先级越高(即在List中排名越靠前),默认的order是最低的优先级:

1
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

通过重写这个getOrder()函数,可以改变我们自定义的HandlerExceptionResolver的顺序:

1
2
3
4
5
6
7
public class MyExceptionResolver implements HandlerExceptionResolver,Ordered {
    ...
    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

5.3 ExceptionHandler引发的问题

序列化的异常,首先会执行前三个默认的HandlerExceptionResolver,自带的HandlerExceptionResolver处理了这种异常,我们自定义的ExceptionHandler根本没有起到作用。

6.解决方案

修改自定义ExceptionHandler的Order