xiaobaoqiu Blog

Think More, Code Less

Enum反序列化问题

1.Enum原理

定义一个Enum,通过编译之后的字节码,我们可以发现其实现原理:

1
2
3
public enum FruitEnum {  
    APPLE, ORAGE  
}

编译器是在为我们创建一个类,这个类继承自 java.lang.Enum,有两个公共的、静态的、被声明成final的属性,它们的类型就是我们定义的FruitEnum。

编译器还生成了一个静态初始话器,就是字节码中static{};这一行下面的代码,其中的字节码创建了两个FruitEnum对象,同时分别赋值给APPLE和ORANGE这两个属性,调用的构造函数是定义在java.lang.Enum中的protected Enum(String name, int ordinal)方法。

在创建完成两个FruitEnum对象并且分别赋值给APPLE和ORIGIN之后,还创建了一个名叫ENUM$VALUES的数组,然后把APPLE和ORIGIN按照定义的顺序放如这个数组中。

除了这个静态初始化器之外,编译器还为我们生成了两个静态方法,values()和 valueOf(java.lang.String)方法。其中values()方法将ENUM$VALUES数组拷贝一份然后返回,而valueOf(java.lang.String)方法则会调用java.lang.Enum类中的valueOf方法,其作用是根据参数名找到对应的具体的枚举对象,如果找不到的话会抛出一个IllegalArgumentException异常。

2.Enum序列化反序列化原理及问题

2.1原理

序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。

同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

2.2问题

在系统或者类库升级时,对其中定义的枚举类型多加注意,为了保持代码上的兼容性,如果我们定义的枚举类型有可能会被序列化保存(放到文件中、保存到数据库中,进入分布式内存缓存中),那么我们是不能够删除原来枚举类型中定义的任何枚举对象的,否则程序在运行过程中,JVM就会抱怨找不到与某个名字对应的枚举对象了。

另外,在远程方法调用过程中,如果我们发布的客户端接口返回值中使用了枚举类型,那么服务端在升级过程中就需要特别注意。如果在接口的返回结果的枚举类型中添加了新的枚举值,那就会导致仍然在使用老的客户端的那些应用出现调用失败的情况。

3.Enum序列化反序列化问题解决

使用class代替Enum,原来的枚举使用static对象替换,valueOf()方法使用一个Map实现,示例代码如下:

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
public final class FruitEnum implements Serializable {

    private static final long serialVersionUID = -7230925342774763449L;

    private static final Map<Integer, FruitEnum> MAP = new HashMap<Integer, FruitEnum>();

    public static final FruitEnum APPLE = new FruitEnum("Apple", 0);
    public static final FruitEnum ORAGE = new FruitEnum("Orige", 1);

    private String text;
    private int code;

    private FruitEnum(String text, int code) {
        this.text = text;
        this.code = code;

        MAP.put(code, this);
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    @Override
    public int getCode() {
        return code;
    }

    /**
     * 根据code获取FruitEnum
     *
     * @param code
     * @return
     */
    public static FruitEnum valueOf(int code) {
        return MAP.get(code);
    }
}

参考:http://mysun.iteye.com/blog/1581119

Tomcat架构

主要整理Tomcat的整体架构.分析各个主要模块的功能.

1.整体结构

Tomcat基本结构图如下所示:

简单介绍:

1.一个serve包括多个service;
2.一个service包含两个核心部件:一个容器(Container)和多个连接器(Connector),Connector主要负责对外交流,Container主要处理Connector接受的请求,主要是处理内部事务;
3.Tomcat基于JMX(Java Management Extensions,即Java管理扩展,是一个为应用程序、设备、系统等植入管理功能的框架)管理这些组件,另外实现以上接口的组件也实现了代表生存期的接口Lifecycle,使其组件履行固定的生存期,在其整个生存期的过程中通过事件侦听LifecycleEvent实现扩展;

2.核心部件

Tomcat的核心类图如下所示:

其中Container是一个接口,定义一些容器的行为, 比如addChild(),getPipeline()等.每个Container都有一个Pipeline,其中包含一系列的表示请求处理器的Valve,注意Pipeline不是通过server.xml配置产生的,是必须有的,在pipeline上配置不同的valve,当需要调用此容器实现逻辑时,就会按照顺序将此pipeline上的所有valve调用一遍,类似于责任链模式.Engine,Host,Context和Wrapper都是实现了Container这个接口.Container还包含用来处理安全里授权与认证Realm等组件.

Tomcat(Container)的容器分成4个等级,如下:

假设访问的URL是 http://www.mydomain.com/app/index.html 各个容器处理的详细情况如图所示:

Wrapper封装了具体的访问资源,例如 index.html Context 封装了各个wrapper资源的集合,例如 app Host 封装了各个context资源的集合,例如 www.mydomain.com

下面简单介绍一下各个类:

2.1 Bootstrap

Bootstrap简单说就是Tomcat的启动类,它只负责创建Catalina实例并将输入参数传递给Catalina实例;

2.2 Catalina

Catalina是Tomcat实际的入口,执行Tomcat的初始化,启动,停止等关键逻辑; (1).初始化的逻辑中初始化一个解析配置文件的Digester,这个Digester使用SAX方式解析xml文件,它获取解析conf目录下的server.xml这个配置文件并会生成Server等实例;并执行Server的初始化;Server会顺序往后执行Service,Connector,Containner等的初始化. (2).启动逻辑就是调用Server的启动; (3).停止逻辑就是调用Server的停止;

2.3 Server

Server(参考StandardServer类)是整个Tomcat组件的容器,包含一个或多个Service.

1
public void addService(Service service);

2.4 Service

Service(参考StandardService类)是包含Connector和Container的集合,Service用适当的Connector接收用户的请求,再发给相应的Container来处理.

2.5 Connector

Connector是网络socket相关接口模块,默认实现HTTP、AJP等协议的.connector主要作用包括:

(1).接收socket
(2).从socket获取数据包,并解析成HttpServletRequest对象
(3).从engine容器开始走调用流程,经过各层valve,最后调用servlet完成业务逻辑
(4).返回response,关闭socket

connector配置举例为:

1
2
3
<Connector port="80" URIEncoding="UTF-8" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="7443" />

其中protocol的选择包括 (1).BIO实现: org.apache.coyote.http11.Http11Protocol (2).NIO实现 org.apache.coyote.http11.Http11NioProtocol

以Http11Protocol举例解析connector结构:

Http11ConnectionHandler对象维护了一个Http11Processor对象池, 完成http request的解析和分派.

JIoEndpoint维护了两个线程池,Acceptor及Worker Acceptor是接收socket,然后从Worker线程池中找出空闲的线程处理socket,如果worker线程池没有空闲线程,则Acceptor将阻塞;Worker是典型的线程池实现.Worker线程拿到socket后,就从Http11Processor对象池中获取Http11Processor对象;

Mapper维护了一个从Host到Wrapper的各级容器的快照.它主要是为了,当http request被解析后,能够将http request绑定到相应的servlet进行业务处理.

CoyoteAdapter将http request解析成HttpServletRequest对象,之后绑定相应的容器,然后从engine开始逐层调用.

2.6 Engine

Engine(参考StandardEngine类)

2.7 Host

Host(参考StandardHost类)就是我们所理解的虚拟主机.

2.8 Context

Context(参考StandardContext类)就是我们所部属的具体Web应用的上下文,每个请求都在是相应的上下文里处理的.

2.9 Wrapper

Wrapper(参考StandardWrapper类)Wrapper是针对每个Servlet的Container,每个Servlet都有相应的Wrapper来管理.

它封装的处理资源的每个具体的servlet,除了上面提到的Pipeline和Valve之外,还包含我们熟悉的servlet对象.

wrapper主要包括三大类

(1).处理静态资源的一个wrapper:例如html, jpg.对应wrapper为org.apache.catalina.servlets.DefaultServlet
(2).处理jsp文件,对应wrapper为org.apache.jasper.servlet.JspServlet
(3).自定义的servlet对象. 在web.xml中定义的serlvet

2.10 Pipeline和Valve

Pipeline(参考StandardPipeline类),Pipeline就是一串Valve.请求到达当前容器,则从前往后执行每个Valve的逻辑.

Valve实现具体业务逻辑单元.可以定制化valve(实现特定接口),然后配置在server.xml里.对其包含的所有host里的应用有效.定制化的valve是可选的,但是每个容器有一个缺省的valve,例如engine的StandardEngineValve,是在StandardEngine里自带的,它主要实现了对其子host对象的StandardHostValve的调用,以此类推.

缺省的valve的Valve实在Pipeline的最后面.

Mysql优化

最近项目组遇到的慢查询比较多,大家的慢查询意识逐渐有了,同事做的工作中使用到的优化慢查询的方法分享,这里简单总结一些,便于以后使用。

通常这些优化方式是组合使用的。

1.合理使用索引

1.1 一个表只会使用一个索引

假设customer表有两个索引:

1
2
  KEY `idx_create_time` (`create_time`) USING BTREE,
  KEY `idx_city` (`city`)

sql语句的where条件包含create_time和city

1
explain select * from customer where city = 'beijing' and create_time > '2015-01-01';

这个sql语句只会走一个索引。

1.2 索引区分度越大越好

索引的区分读越大越好,可以使用show index from table看表的索引情况。

1.3 行数越少越好

执行计划中sql搜索的行数(rows)越小越好。

1.4 查询条件字段不要套函数,无法使用索引

比如下面这个sql是不走索引的

1
explain select * from customer where left(city, 2) = 'be';

2.不要复合条件查询

将复合条件根据规则转换为单个条件,避免多个字段去or。

比如我们在city上面建立了索引,但是我们没有在name上建立索引,下面这个sql会走全表而不走索引

1
explain select * from customer where city = 'beijing' or name = 'beijing';

3.减少join

利用Mybatis的动态SQL,保证同一时刻JOIN的表最少。即满足某一个条件的时候才join。

1
2
3
4
select * from customer
<if test="conditions.name != null and conditions.name != ''">
    join user on user.id = customer.user_id
</if>

4.分步查询

比如一个sql需要大量的join,并且每个join表的数据量都很大,这时候,尝试将单独的sql拆分成多个sql。

我们可以从搜索条件入手,不包含搜索条件的表一定可以拆分成单独的sql。

1
2
3
4
5
6
7
8
9
select *
from A
join B on ...
join C on ...
join D on ...
join E on ...
where
A.name = ...
and B.city = ...

这是可以考虑对C,D,E表的查询单独拆分出来。

5.合理使用Cache

通常根据条件Count比较耗时,可以将Count数Cache,比如list接口的count通常成千上万,count数只是用来展示页数,这时候可以考虑缓存下来,但是需要考虑好缓存策略。

6.业务逻辑上优化

当我们使用上述步骤无法优化时,可以考虑从逻辑上优化。

比如join很多个表查询的时候,我们需要确认应用方所有的字段都需要,通常减少字段能让我们少join表。

Tomcat简介

最近会重新看一下Tomcat的源代码,整理一些东西,包括但不限于:

1.Tomcat架构
2.Tomcat启动过程
3.请求在Tomcat中的流转
4.Tomcat目录结构与配置

这里会首先简单介绍一些Tomcat的知识。后面会逐渐详细介绍。

Tomcat是由Apache软件基金会下属的Jakarta项目开发的一个Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持,并提供了作为Web服务器的一些特有功能,如Tomcat管理和控制平台、安全域管理和Tomcat阀等。由于Tomcat本身也内含了一个HTTP服务器,它也可以被视作一个单独的Web服务器。

由于有了Sun的参与和支持,最新的Servlet和Jsp规范总能在Tomcat中得到体现。而进来因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。

官方网站:http://tomcat.apache.org/

Tomcat wiki:http://wiki.apache.org/tomcat/FrontPage

1.JSP和Servlet

Jsp(JavaServer Page)是在普通Html中嵌入了Java代码的一个脚本,在这一点上,它与其它的脚本语言(如Php)一样,但它与其它语言不同的是:其它脚本语言由服务器直接解释这个脚本,而Jsp则由Jsp容器(如Tomcat)首先将其转化为Servlet,然后再调用Javac将Servlet编译为Class文件。

Servlet其实是一个特殊的Java类,Servlet类一般从HttpServlet类继承而来,在这个类中至少要实现doGet或者doPost函数,在这两个函数中处理来自客户的请求,然后将结果返回。

2.Servlet容器

负责处理客户请求、把请求传送给Servlet并把结果返回给客户。不同程序的容器实际实现可能有所变化,但容器与Servlet之间的接口是由Servlet API定义好的,这个接口定义了Servlet容器在Servlet上要调用的方法及传递给Servlet的对象类。

3.Servlet生命周期

1.Servlet容器创建Servlet的一个实例
2.容器调用该实例的init()方法
3.如果容器对该Servlet有请求,则调用此实例的service()方法
4.容器在销毁本实例前调用它的destroy()方法
5.销毁并标记该实例以供作为垃圾收集

4.Tomcat简介

Tomcat中的Servlet容器称之为Catalina。

Tomcat中的应用程序是一个WAR(Web Archive)文件。WAR是Sun提出的一种Web应用程序格式,与JAR类似,也是许多文件的一个压缩包。这个包中的文件按一定目录结构来组织:通常其根目录下包含有Html和Jsp文件或者包含这两种文件的目录,另外还会有一个WEB-INF目录,这个目录很重要。通常在WEB-INF目录下有一个web.xml文件和一个classes目录,web.xml是这个应用的配置文件,而classes目录下则包含编译好的Servlet类和Jsp或Servlet所依赖的其它类(如JavaBean)。

在Tomcat中,应用程序的部署很简单,你只需将你的WAR放到Tomcat的webapp目录下,Tomcat会自动检测到这个文件,并将其解压。你在浏览器中访问这个应用的Jsp时,通常第一次会很慢,因为Tomcat要将Jsp转化为Servlet文件,然后编译。编译以后,访问将会很快。另外Tomcat也提供了一个应用:manager,访问这个应用需要用户名和密码,用户名和密码存储在一个xml文件中。通过这个应用,你可以在远程通过Web部署和撤销应用。

5.Tomcat各个版本

http://wiki.apache.org/tomcat/TomcatVersions

6.参考:

http://zh.wikipedia.org/wiki/Apache_Tomcat

http://www.ibm.com/developerworks/cn/java/j-lo-tomcat1/

http://gearever.iteye.com/category/223001

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