让mysql支持emoji表情符号存储

mysql的文本(varchar, text),对emoji表情符号不是很好的支持,在5.5之前的版本,varchar和text都是不支持存储emoji表情符号的(即使是utf8)的编码模式。原因在于mysql的utf8是规定了每一个utf8字符按照3个字节来存储,而一个emoji(最初来自苹果系统,现在流行于各种移动操作系统)却需要4个字节来存储。这就导致了如果强制将emoji存储到varchar,text等字段上的时候,mysql会抛出异常,认为emoji是个“不正确”的文本。

  • ERROR 1366 (HY000): Incorrect string value: ‘\xF0\x9F\x91\xBD\xF0\x9F…’ for column ‘name’ at row 31

所幸,mysql在5.5之后的版本,针对四个字节的utf8字符推出了一种新的兼容的编码,叫 utf8mb4。utf8mb4比utf8支持的字符集更广,可以支持utf8以及四个字节的字符集,关于utf8mb4和utf8的区别可以这篇官方文档1

简而言之就是:“utf8mb4 is a superset of utf8” ,utf8mb4是utf8的超集,utf8是utf8mb4的子集。utf8mb4理论上是兼容utf8. 所以如果你的项目需要支持存储emoji表情,同时mysql的版本是5.5以上的版本,那么就可以把字段的charset改为 utf8mb4就可以完美支持emoji了。

alter table category modify name text charset utf8mb4;

那如果当前mysql版本不支持utf8mb4编码怎么办?

解决方法:

1. 升级mysql版本到5.5.3以上的 :)

2. 把需要支持emoji表情存储的字段改成 blob的。(这是针对mysql升级有限制的情况)

blob类型一般是用来存储二进制文件的,当时用来存储文本其实也是可以的,只不过存进去之前,把文本变成byte数据就可以了。已java为例,使用String.getBytes(charset)方法,可以把字符串转化成二进制,然后存储到数据库中。如果你有很多字段都要这么搞的话,估计都得疯了。怎么办?用orm框架~

已ibatis为例,如果你的对象字段是String文本,存储的字段确实blob,其实是没有关系的,不需要写特殊的代码,直接支持写入。但是读出来的时候就需要做转换,否则出来的是乱码。所以这里需要借助ibatis的typehandler和resultMap来解决这个问题。ORM框架的好处就是你不用一直重复劳动,可以在各种地方留着钩子(hook),随时让你在需要的时候可以插点东西到关键的地方上去。好了,废话不多说,看看这个typeHandler怎么实现:(这里是ibatis2.3.*的版本,如果是myBatis,可能报名和接口参数不太一样,但实现方式是一样的)

public class BlobStringTypeHandler extends BaseTypeHandler {

    //charset
    private static final String DEFAULT_CHARSET = "utf-8";

    @Override
    public void setParameter(PreparedStatement ps, int i, Object parameter, String jdbcType) throws SQLException {
        ByteArrayInputStream bis;
        String param = (String) parameter;
        try {
            //###把String转化成byte流
            bis = new ByteArrayInputStream(param.getBytes(DEFAULT_CHARSET));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Blob Encoding Error!");
        }
        ps.setBinaryStream(i, bis, param.length());
    }

    @Override
    public Object getResult(ResultSet rs, String columnName) throws SQLException {
        Blob blob = rs.getBlob(columnName);
        byte[] returnValue = null;
        if (null != blob) {
            returnValue = blob.getBytes(1, (int) blob.length());
        }
        try {
            //###把byte转化成string
            return new String(returnValue, DEFAULT_CHARSET);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Blob Encoding Error!");
        }
    }

    @Override
    public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
        Blob blob = rs.getBlob(columnIndex);
        byte[] returnValue = null;
        if (null != blob) {
            returnValue = blob.getBytes(1, (int) blob.length());
        }
        try {
            //###把byte转化成string
            return new String(returnValue, DEFAULT_CHARSET);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Blob Encoding Error!");
        }
    }

    @Override
    public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
        Blob blob = cs.getBlob(columnIndex);
        byte[] returnValue = null;
        if (null != blob) {
            returnValue = blob.getBytes(1, (int) blob.length());
        }
        try {
            return new String(returnValue, DEFAULT_CHARSET);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Blob Encoding Error!");
        }
    }

    @Override
    public Object valueOf(String s) {
        try{
            return s.getBytes(DEFAULT_CHARSET);
        }catch (Exception e){
            return null;
        }
    }
}

重点看几个getResult方法,就是从ResultSet中拿到blob字段数据(byte[]),然后把byte数组转化成string就OK了。
怎么使用?
在sqlMap文件定义resultmap, 对需要转换的字段指定这个typeHandler就可以了:

<resultMap id=“EntityMap" class=“your.pack.Entity">
     <result property="name" column="name" jdbcType="BLOB" javaType="java.lang.String" typeHandler=“your.BlobStringTypeHandler"></result>
</resultMap>

注意select的statement语句返回使用resultMap指定这个 resultMap

<select id=“EntityDAO.getByXXX" parameterClass="java.util.Map" resultMap="EntityMap">
          select
            name
          from
          your_table
          where
          ...
        limit 1
 </select>

OK, mysql支持emoji了:)


  1. http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html 

Mac使用jdk7启动Intellij Idea

Mac使用jdk7启动Intellij Idea

我们当前研发的产品,不论是线上还是开发环境,基本上都已经用上了jdk1.7,而在macos 13.* 版本中,如果本地环境已经安装了1.7的jdk,但是没有1.6的jdk,idea启动的时候会报错,说需要旧的java se6才能支持。如题图所示。

首先怀疑是不是环境变量没设置好:于是增加了JAVA_HOME环境变量之后,问题依旧:
java home
jdk明明是向前兼容的,1.7下跑1.6能支持的软件一般(1)情况下是没问题的,为什么会出现这样的提示呢?原因肯定是在于idea哪里配置指定了只能使用1.6版本的jre才能运行。所以解决方法应该是从idea的配置文件入手。

进入idea的安装目录,有个Contents目录,里面有个Info.plist配置文件。
Info.plist
打开Info.plist,检索“1.6”字符串,果不其然,还真找到了:
modify
配置项是JVMVersion,配置的值是1.6, 把值改成1.7之后启动idea:
idea home

perfect,完美运行。看下这个Info.plist,其实还有其他的配置项在这里可以修改,在JVMOptions下面,除了有JVMVersion之外,还有VMOptions, ClassPath等配置项,可以修改。当然一般情况下,这些选项是够用的,不需要手工去修改他。

扩展阅读:
1: http://stackoverflow.com/questions/13575224/comparison-method-violates-its-general-contract-timsort-and-gridlayout
2: http://stackoverflow.com/questions/6626437/why-does-my-compare-method-throw-exception-comparison-method-violates-its-gen


  1. 为什么说1.6支持的软件“一般”能跑在1.7下面而不是“一定”呢?因为jdk7把一些底层的接口默认实现改了,但是会带来问题。比如Arrays.sort()方法,在1.6之前,这个方法模式使用的排序方式是mergeSort,而到了1.7,默认的实现变成了TimSort。在1.6下的正常排序到1.7可能会出现不正常或者说排序结果跟想象中的不一样。TimSort对每次比较的结果要求跟严格,在1.6下可以正常排序的结果,1.7可能会抛异常。 

靠谱的分布式锁实现

几年前介绍过一种基于zk的分布式锁1的实现,那种是没有经过实践证明的,听过一场分享,然后觉得原来分布式锁可以这么搞,然后在实验环境写了一些代码,简单验证一下,就认为成了。其实那里面有几个比较严重的问题,第一个是锁操作如果在并发的情况下不是block的,而是通过循环+sleep的方式来反复判断,性能上是比较差的,不够实时。第二个是设计太复杂,其实可以不需要ip地址的介入的。

基于zookeeper,其实可以设计出优雅的分布式锁。这个锁可以有这几个API:lock(), unlock(), isLock(),lock用于加锁,unlock用于解锁,isLock用于判断是否已经锁住了。zookeeper提供了这么一套机制,你可以监控watch节点的变化(内容更新,子节点添加,删除),然后节点变化的时候通过回调我们的监控器(watcher)来通知我们节点的实时变化。在这种机制下,我们可以很简单的做一个锁。

在单机模式,没有引入zookeeper时,我们可以通过创建一个临时文件来加锁,然后在事务处理完毕的时候,把这个临时文件删除就能代表解锁了。这种简单的加锁和解锁模式可以移植到zookeeper上,通过创建一个路径,来证明该锁已经存在,然后删除路径来释放该锁。而同时zookeeper又能支持对节点的监控,这样一来,我们在多机的情况下就能同时且实时知道锁是存在还是已经解锁了。

firstimage

如图所示,我们在/lock下创建了/app1  /app2 … /appN n个子目录,用于适用不同的应用, 每个/app* 下面都可以根据业务需求创建锁 /lock. , 而每个机器在获取锁的时候,会在/lock. 下面创建 _0000000* 的自增长临时节点,这个节点上的数字用于表示获取锁的先后顺序。前面说的还是有点抽象,下面举个例子:

一个后台应用(app为back)总共有3台机器在处理事务,其中有一个业务(lock为 biz)同一时间只能有一台机器能处理,其他如果也同时收到处理消息的时候,需要对这个事务加个锁,以保证全局的事务性。三台机器分别表示为 server1, server2和 server3。

secondimage

对应的,锁的路径就是 /lock/back/biz 。 首先 server1先收到消息,要处理事务biz,那么获取锁,并在/lock/back/biz下创建一个临时节点/lock/back/biz/_00000001 ,这时候判断/lock/back/biz 下的子节点,看最小的那个是不是和自己创建的相等,如果是,说明自己已经获取了锁,可以继续下一步的操作,直到事务完成。

thirdimage

与此同时,server2和server3也收到的消息(稍微慢于server1收到),这时候server2和server3也分别会在/lock/back/biz下面创建临时节点/lock/back/biz/_00000002 和 /lock/back/biz/_00000003,他们这时候也会判断/lock/back/biz下的子节点,发现最小的节点是_00000001,不是自己创建的那个,于是乎就会进入一个wait的操作(通过mutex很容易实现)。

fouthimage

之后,等server1的事务处理完毕,释放server1获取的锁,也就是把 /_00000001删掉。由于zookeeper可以监控节点的变化,删掉/_00000001的时候,zookeeper可以通过节点删除的事件,通知到server1 server2 server3,这时候server2和server3对上面通过mutex block住的区块发送信号量(notify),server2和server3继续进入判断/lock/back/biz下面的子节点以及最小的节点和自己做对比,这时候server2由于创建了节点_00000002,所以轮到他来获取锁:

fifthimage

之后server2开始进入事务处理,然后释放锁,server3开始得到锁,处理事务,释放锁。以此类推。

这样一来,整个事务(biz)的处理就能保证同时只有一台机能处理到了。

整体伪代码如下:

public class LockImpl{
   //获取zk实例,最好是单例的或者是共享连接池,不然并发高的时候,容易挂
   private Zookeeper zk = getZookeeper();
   //用于本地做wait和notify
   private byte[] mutex = new byte[0]; 
   //节点监控器
   private Watcher watcher;
   //这个锁生成的序列号
   private String serial;
   public LockImpl(){
       watcher = new DefaultWatcher(mutex);
       createIfNotExist();
   }

   private createIfNotExist(){
      String path = buildPath();
      //创建路径,如果不存在的话
      createIfNotExsitPath(path);
      //注册监控器,才能监控到。
      registWatcher(watcher);
   }

   public  void lock(){
       //获取序列号,其实就是创建一个当前path下的临时节点,zk会返回该节点的值
       String serial = genSerial();
       while(true){
          //获取子节点
          List<string> children = getChildren();
         //按从小到大排序
          sort(children);
          //如果当前节点是第一个,说明被当前进程的线程获取。
          if(children.index(serial) == 0){
               //you get the lock
               break;
          }else{
               //否则等待别人删除节点后通知,然后进入下一次循环。
               synchronized(mutex){
                   mutex.wait();
               }
          }
       }
      return;
   }

   public void unlock(){
          //删除子节点就能解锁
          deletePath(serial);
   }

   public void isLock(){
        //判断路径下面是否有子节点即可
        return ifHasChildren();
   }

}

//监控器类
public class DefaultWatcher implements Watcher{
      private byte[] mutex;
      public     DefaultWatcher(byte[] mutex){
          this.mutex = mutex;
      }

     public void process(WatchedEvent event){
          synchronized (mutex) {
               mutex.notifyAll();
          }
     }
}

至此,一个看起来优雅一点的分布式锁就实现了。这是一个理想状态下的实现,咋看起来没问题,其实里面隐藏了比较严重的问题。就是这里把zookeeper理想化了,认为他是完美的,不会出现问题。这里说的问题倒不是一定是zk的bug,比如网络问题,在分布式系统中,网络问题是一个很常见的问题,很容易就会有异常的情况。如果出现网络问题,会出现什么情况呢?答案是“死锁”

下面按多种情况来分析这个问题:

当我们向zk发起请求,要求创建一个临时节点的时候,发生了异常(包括网络异常,zk的session失效等)

  1. 假设zk的服务端还没收到请求,这时候很简单,我们客户端做一下重连和重新创建就可以了,问题不大。
    sixth
  2. 假设zk服务端收到请求了,但服务端发生异常,没有创建成功,那么我们客户端再次重试一下就可以了。
    seventh

  3. 假设zk服务端收到了请求,子节点创建成功了,但是返回给客户端的时候网络发生了异常。这时候我们要是再做重试,创建的话,就会进入“死锁”。这里的“死锁”跟我们平时理解的死锁不是一个概念,这里的意思不是回路循环等待,而是相当于这个锁就是死掉了,因为前面的异常请求中其实创建了一个节点,然后这个节点没有客户端与他关联,我们可以称为幽灵节点。这个幽灵节点由于先后顺序,他是优先级最高的,然而没有客户端跟他关联,也就是没有客户端可以释放这个幽灵节点,那么所有的客户端都会进入无限的等待,造成了“死锁”的现象。
    eighth
    ninth

  4. 假设zk服务端收到请求了,子节点创建成功了,返回给客户端成功了,但是客户端在处理的时发生了异常(这种一般是zk的bug才会出现),这时候我们再做一次重试,也会进入上面的“死锁”现象。
    tenth
    为什么会出现3,4的现象呢?因为我们只是做了简单的重试,并没有对服务端和客户端做验证。就是客户端创建了一个幽灵节点,但是创建者本身甚至都不知道是自己创建的幽灵节点,还是别人创建的。要如何规避这个问题呢?废话,引入验证的流程。就是验证+无限重试。怎么做验证,不要把验证想的太复杂,验证就是你创建的节点里面有你创建的私有的信息,你客户端本身也拥有这个信息,然后两者一对比,就能知道哪个节点是哪个客户端创建的。当然,这个信息必须保证我有你无的,也就是唯一性。简单了,引入UUID就可以搞定这个问题。

1. 创建临时节点之前,客户端先生成一个UUID,(直接用JDK的UUID.randomUUID().toString()就可以了)。

2. 创建临时节点时,把这个uuid作为节点的一部分,创建出一个临时节点。

3. 重试创建的流程中加入对已存在的UUID的判断,对于是当前进程创建的子节点不再重复创建。

4. 对children排序的时候,把uuid去除后再排序,就能达到先进先出的效果。

比如客户端1,生成了UUID: fd456c28-cc85-4e2f-8d52-bcf7538a2acf, 然后创建了一个临时节点: /lock/back/biz/_fd456c28-cc85-4e2f-8d52-bcf7538a2acf_00000001
这时候服务端返回异常,拿客户端第一件事是先把children捞出来,然后判断这些children里面有没有自己创建的 uuid,如果有的话,说明自己其实是创建成功了,然后就看是不是轮到自己了,解决3的问题。如果返回正常,但是客户端有bug抛异常了,这时客户端仍要进行重试,重试之前也会走前面的流程,可以解决4的问题。

对children排序不能简单的把node的路径进行排序,因为randomUUID是完全随机的,按这个排序可能会导致某些锁请求一直没有被响应,也会有问题。这里因为UUID的长度是固定的,而且也有规律可循,所以很容易从node中分解出 uuid和序列号,然后对序列号进行排序,找出最小的值,再赋予锁,就可以了。

分布式系统中,异常出现很正常,如果你的业务需要觅等操作(N^m = N)的话,就需要引入验证和重试的机制。分布式锁就是需要一个觅等的操作,所以一个靠谱的分布式锁的实现,验证和重试的机制是少不了的,这就是我想说的。具体要参考实现的话,可以看netflix开源的zk客户端框架curator2, 比直接用zk简单得多也健壮得多。框架里面也实现了一套分布式锁(还有其他各种有用的东西),生产线其实可以直接拿来使用的,非常方便。


  1. https://www.jtalk.top/blog/122 

如何读懂小票据热敏打印机编程指南

背景

买了个网络打印机, 随机光盘附带了一本 《XX系列中文编程手册.pdf》,打开一看, 艾玛, 这都是神马啊!
sample
艾玛,这都是什么命令啊?windows,明显不是,难不成他打印机里面装了个系统?哦对了,怎么他妈才能给这个打印机发送命令啊??


数据传输方式

有上面的疑问很正常,人家是堂堂网络打印机,当然是通过网络来发送命令啦,那怎么连呢?恩,他没告诉你,但这个应该是通用的标准,果不其然,该打印机(容大RP58-L)出厂设置了该ip地址是192.168.1.87,打印机端口是9100。既然IP地址和端口号都齐全了, 明显就是一个套接字(Socket)嘛,所以,通过socket来发送命令没跑了.


命令解读

command
这些命令怎么回事? 怎么奇葩的用 ESC HT GS什么的开头的?其实没那么复杂, 这些ESC, HT, GS, FS什么的不是一个字符串, 他们都是ASCII码表里面排名靠前的控制字符. 通过套接字你传送的数据是流, 而流的本质上是一个一个的字节, 所以这里的命令中, 一个符号代表一个字节, 而字节的值在每条命令的详解里面有参考值.

命令解读例子

command sample
比如这条命令, 他的用途是用来 “选择打印模式”
其中, 看[格式] 这一栏, 他的 ESC! 都是ASCII码对应的值, 下面列出了16进制码和10进制码提供对照.
然后主要变化的值是n, n是一个数字, 0~255的区间内变化,下面的表格列举的时候n的值是什么的时候可以做什么. 他这里有8个位(刚好一个字节)可以设置, 每个位的1/0代表开关的开启和关闭, 比如要设置字体加粗, 那么n的第3位应该是1, 也就是 2^3 = 8, 十六进制是08, 所以整个命令下来就是(用16进制表示)
0x1B,0x21,0x08


如何执行命令

还是上面的例子, 我们要给打印机设置接下来打印字体加粗, 那么要把 0x1B,0x21,0x08这三个字节发送给打印机, 从第二步我们已经知道, 打印机通过socket来提供服务, 我们可以直接用socket来发送指令:(java代码, 其他语言类似, 不同网络打印机其提供打印服务的套接字可能不同)

Socket client = new Socket("192.168.1.87", 9100);
OutputStream output = client.getOutputStream();
byte[] command = new byte[]{0x1B, 0x21, 0x08};
output.write(command);
output.flush();
client.close();

如何打印文字

通过上面的例子可以知道, 我们给打印机发送命令只有通过socket的流来发送指令, 所以只能把字符串转换为字节流, 再传送给打印机:

Socket client = new Socket("192.168.1.87", 9100);
OutputStream output = client.getOutputStream();
byte[] chars = "strings".getBytes();
output.write(chars);
output.flush();
client.close();

当然, 这里不同的打印机可能会有不同的字符, 比如中文, 你可能在转换为字节流的时候需要设置不同的编码.


最后

上面这些命令的格式在小票据热敏打印机中一般是通用的, 不同的是提供打印服务的方式, 有的通过网络来提供套接字服务, 有些通过COM串口来提供服务, 对于不同的服务最都可以最终转化为流来处理, 所以本质上都是差不多的

Android – AsyncTask与Handler 的关系

前面的文章已经分析了HandlerAsyncTask的原理,现在说说他们的异同点:

1. 相同点:AsyncTask就是封装了Thread+Handler,来简单实现做异步任务同时又能更新UI

2. 不同点: 在android 3.0之后的AsyncTask中的任务默认是串行执行的,如果你有多个异步任务要并发执行,应该使用Thread(Pool)来替代。 当然AsyncTask也是考虑到了这一点,所以提供了一个executeOnExcutor方法,可以传入我们自定义的executor来进行并发执行。AsyncTask内部实现的Executor是SerialExecutor, 是串行执行的,来看代码他是如何串行化的:1

调用execute的时候,会先把异步任务封装为一个runnable放到一个队列里面,然后再判断要不要执行该任务。
判断的依据很简单,如果当前已经有任务在跑了,那么就不跑了。那么,这个任务会被丢弃吗?不会!当前任务执行后的finally块里面会执行下一个任务。这里的mActive变量设计的很巧妙,当mTasks.poll()得到的下一个任务为空的时候, 就不会再往下执行了, 所以可以保证所有任务都能被执行到,而且任务都是串行执行的。这里execute和scheduleNext两个方法都加上了synchronized关键字,所以也不会有线程安全的问题。
看下AsyncTask的execute方法:
2
这里用的是默认的Executor, 而这个默认的Executor就是SerialExecutor, 而且是单例的
3
看到sDefaultExecutor前面有个volatile关键字就说明了这个Executor是可以被更改的,果不其然, AsyncTask提供了这么一个方法:
4
这样一来,我们通过更改默认的Executor就能达到AsyncTask里面的任务并发执行的目的。
另外,回到AsyncTask的execute方法,是通过executeOnExecutor来提交任务的,而恰巧,executeOnExecutor这个方法是public的,说明我们也可以通过executeOnExecutor这个方法来指定我们自定义的Executor来执行任务,从而达到并发执行的目的。
5
同时,AsyncTask也定义了一个Executor常量供我们使用(实际上他的SerialExecutor也在用),就是
6
所以我们调用AsyncTask.executeOnExecutor的时候,可以把AsyncTask.THREAD_POOL_EXECUTOR作为第一个参数传过去即可,也省去了我们自己定义(如果实在需要还是真的要自己定义)的代码了。
如果用Thread+Handler来实现类似AsyncTask类似的功能,可以用Handler的post方法,在Thread中处理任务的过程中,如果想要更新UI线程,有几种方法,一种是post,一种是sendMessage。如果从用法上来看,post应该说更实用一点,sendMessage的话你还要去封装一个消息,然后接受到消息的时候还要再把数据拿出来,进行UI的更新,例如:
7
3. 最后画一幅图来总结一下AsyncTask和Handler之间的关系:
9
绿色部分是Android框架定义的方法,我们无需去重载,而粉色部分的方法我们可以去重载来实现自己的业务逻辑。
注意:在doInBackground被调用之前会有一个线程池的调度过程,以及会先执行onPreExecute这个方法,这里省略了。

Android – AsyncTask 源码分析

AsyncTask,看名字直接翻译就是异步任务的意思,顾名思义,是用来处理异步的任务的,那么什么任务需要异步处理呢,那些需要比较耗时的计算和资源获取都需要异步处理。如果不异步处理的话,处理这个任务的UI线程就会出现卡顿的情况。

1. AsyncTask是什么?
先看源码中的javadoc:
1
简单翻译一下: AsyncTask能够适当和简单地使用UI线程。可以处理后台操作以及发送操作结果到UI线程,而不需要操作Thread和Handler。AsyncTask就是设计用来简化Thread和Handler的使用的工具类,在做一些短操作的时候(最多几秒)应该观念性的想到AsyncTask。如果你需要保持线程在后台跑一段时间,那么强烈建议你使用JUC里面系统的一些并发相关的API,比如Executor,ThreadPoolExecutor和FutureTask等类。一个异步任务是由一个计算逻辑来定义的,跑在后台,在跑完之后将结果反馈给UI线程进行处理。定义一个异步任务需要三个泛型类型(Params,Progress和Result)以及4个步骤(onPreExecute,doInBackground,onProgressUpdate和onPostExecute)。
翻译太差,看得云里雾里有木有,其实简单的说就是异步任务(AsyncTask)是为了简化Thread和Handler的配合使用而定义的一种工具类,实现他,只要制定好几个参数的泛型类型以及覆盖几个步骤的方法(事件)就可以了。其底层还是通过线程(池)和Handler来实现的,后面会提到。

2.定义一个AsyncTask
可以通过匿名内部类的方式,也可以通过类继承的方式,定义一个AsyncTask,但是都必须实现doInBackground这个方法。(吐槽:这个方法的参数类型就是泛型定义里面的Params,但是为什么是不定数组,这个比较奇怪,不知道这个接口的设计者怎么想的。)后面再将实现机制的时候在说说这个方法要怎么实现。

3.实例化AsyncTask
new一个AsyncTask的时候,发生了什么?直接看他的构造方法就知道了:
2
这里mWorker就是一个Callable对象,执行的时候会调用到他的call方法,这里call方法会调用doInBackground方法,所以这个mWorker实际上就是把doInBackground做了封装,保证在执行的时候会调用到这个后台方法。而mFutrueTask就是吧mWorker再封装成FutureTask对象,在任务完成的时候执行postResult方法,把数据发给内部的Handler进行处理。这里内部的handler是在类加载的时候定义好的:
3
看看他的实现:
4
很简单, 根据发过来的消息类型(what),去执行对应的那个方法。注意这里有finish和onProgressUpdate两个路径,先看看onProgressUpdate是用来干嘛的:
我们在定义AsyncTask的时候,可以覆盖其onProgressUpdate方法,这个方法可以更新UI,而且不是等AsyncTask执行结束的时候, 那么是在什么时候触发呢,是在doInBackground中调用了publishProgress这个方法的时候,就会触发这个事件。这个可以干嘛用呢?最直接的一个例子就是进度条(下载,听歌播放等)。进度条一般是有一个进度的过程,不仅仅是开始和结束两个状态,所以我们处理了部分数据之后,为了更及时地反馈给用户,需要更新进度条的进度,所以需要在doInBackground里面调用 publishProgress方法。这就是为什么定义一个AsyncTask需要第三个泛型参数,这个参数就是为了进度中的数据传过来的。他的实现也很简单:
5
把当前的AsyncTask包装成AsyncTaskResult,加上当前进度的参数,发送到内部的handler去处理。
再回头来看handler中,对于消息的类型(what)是MESSAGE_POST_PROGRESS的消息会触发onProgressUpdate事件,从而实现线程的更新。
那么什么时候触发finish事件呢?很显然上面提到的mFutureTask的结束方法里面会postResult,而postResult就是给handler发送一个what等于MESSAGE_POST_RESULT的消息,这时就会触发finish事件(其实不是finish事件,后面会分析到)了。
所以这里几个参数是对应的
execute(Params…)
doInbackground(Parmas…)
publishProgress(Progress …)
onProgressUpdate(Progress…)
finish(Result)
画个图来简单理解一下吧:
6
其中, 绿色的部分是我们不需要实现的, 粉色的部分是我们可以实现的一些事件。这里已经把整个AsyncTask的生命周期画出来了。

4.启动AsyncTask
调用AsyncTask的execute(Params…)方法就可以了, 这个方法是直接调用了内部的另一个方法,executeOnExecutor(Executor, Params…),这里传入的executor参数是AsyncTask内部自己实现的SerialExecutor,用法就是一个线程池。当然我们可以在外部直接调用这个executeOnExecutor方法,然后指定我们自己实现(or定义)的Executor就可以了。不过既然人家提供了,不用白不用你说是不?
7
看吧,这里会先触发onPreExecute事件,然后把参数交给mWorker定义的mParams,然后才丢到池里面进行处理。这个SerialExecutor实现也不复杂:
8
先把runnable对象放到队列里面(mTasks.off),然后再出列交给ThreadPoolExecutor处理,这的实现就是实现了异步任务的串行化处理,先来先执行。

5. 更新UI
在执行过程或者结果的时候需要更新UI,那么在哪里更新呢?不可能直接在doInBackground方法里面更新,因为这时候还不是在UI线程里面,只有在几个on*的事件方法内可以执行UI的更新,因为这几个事件确实是在UI线程中执行的。其中onPreExecute是在启动时还没有进入线程池之前的更新,其他几个都是通过handler来实现的。那么,我们在doInBackground中要如何更新UI呢?很简单,调用publishProgress方法就可以了,看他javadoc里面提供的代码:
9
调用publishProgress的时候把当前处理的进度(Progress的含义)传过去,然后在onProgressUpdate事件里面就可以使用这个进度的结果,然后更新UI.

6. 任务结束
任务结束,也会触发事件, 一种是正常结束, 会调用onPostExecute;另一种是用户自己取消(调用AsyncTask的cancel方法), 则会调用onCancelled事件.在结束这里我们也可以执行UI操作,比如结束进度条神马的, 你看这个AsyncTask简直就是为了类似进度条的UI的完美设计啊.
10

7.分析总结
最后来分析一下AsyncTask是如何搭建起Thread和Handler之间的桥梁的。首先,先从AsyncTask的入口方法,execute入手,他最终是提交任务给了本地的线程池去处理,这就打通了Thread的这一层。然后他提供了publishProgress这个API来发送消息到Handler里面,就打通了Handler的这一层,如此而为之的目的是为了具备线程的异步执行的特性同时又有更新UI的能力。如下图
11

Android – Handler 源码分析

android开发我也是在学习阶段,由于喜欢看源码,所以顺便把经常用到的一些类都看看顺便分析一下他们的工作机制。

Handler在android开发中应该比较常用,主要用于接收异步处理结果的消息并更新UI,比如做了一个天气应用,用户点击刷新的按钮的时候,为了不让界面卡住,这时候应该起一个异步任务去获取数据,然后等数据获取完成之后才更新UI的结果。不同于js更新web上的元素,android的UI更新操作都不是线程安全的,所以需要在同一个线程下才能更新UI,而js是没有多线程的概念的,所有操作都在同一个线程中,所以不会有线程安全的问题。来看看Handler的官方注释:handler.comment

简单翻译一下:一个Handler可以让你用来发送和处理消息(Message),以及消息上附带的Runnable对象,整个是跟消息队列(MessageQueue)一起使用的。每一个Handler实例会关联到一个唯一的线程和该线程的MessageQueue。如果你创建了一个Handler,他将会跟创建这个Handler的线程和该线程的消息队列绑定在一起。也就是说通过把消息发往这个队列和在出列的时候处理他们。Handler一般有两种应用产景,(1)就是调度消息和runnable对象在未来的某个时间点执行(归纳起来就是消息的发送);(2)可以把消息发送到其他线程里面。再简单概括一下就是Handler是跟创建他的线程绑定在一起的,然后通过消息队列方式,实现线程安全的操作。

1. Handler的创建

当你在一个Activity(或者其他)里面new一个Handler的时候,他最终会执行下面这个构造函数:handler.create

第一段if可以无视,是判断Handler是不是静态的,如果不是给你一个警告说可能会内存泄露。Looper.myLooper()其实就是在当前线程的ThreadLocal中获得Looper对象,同时从looper中获取MessageQueue。这里的callback其跟覆盖handleMessage的机制差不多,估计是为了某些地方使用了这种方式,然后一直兼容到现在。

2. 消息的发送机制

当我们调用handler的send()/post()系列的方法的时候(这些方法的调用一般发生在异步处理的任务完成之后,像触发线程的更新操作),handler做了什么操作?他们实际上殊途同归,最终都调用了这个方法:handler.sendMessage

再看看enqueueMessage方法:handler.enqueue

第一句把msg.target设置为当前的handler本身,这一步很重要,因为在这之后,就跟handler没关系了,后面会分析;接着就调用消息队列的入列方法把消息体丢到队列里面排队等待执行。

可以看到,不管是什么方式,最终都是把消息(runnable最后也是包装成消息)丢到消息队列里面。

3. Looper的工作机制

Looper的工作机制就跟他的名字一样,就是一个循环器,作为MessageQueue的消费者,进行消息的出列和处理操作。其核心代码就在loop()方法中,来看看looper

看到for(;;)我们就能猜到这是一个典型的生产者消费者模型了,在for循环里面,looper不断的订阅消息队列的下一个元素(next()方法),然后调用Handler的dispatchMessage方式分发消息给handler进行处理。上面说到设置msg.target那一步很重要就重要在这里。消息处理完之后还可以回收再利用,这点不细说了(如果你的Message是new出来的,回收了也没用,科学的使用方式是使用Message.obtain() 系列的方法来创建消息。)这里消息一个一个处理完之后才会处理下一个,是单线程串行执行的,而且跟创建handler的线程是同一个线程,所以完美的避免的线程安全的问题。那么你的疑问会是,究竟是谁来调用这个loop方法的,不是会卡住吗?是的,这就是精髓所在,这里的looper是UI线程在初始化完所有的UI操作之后调用的,这样一来,就不会有卡住的问题了。

4. 消息分发机制

上面Looper拿到消息之后,会调用Handler的dispatchMessage进行消息的分发操作,这里分发不是简单的调用Handler中的handleMessage方法,而是有其他逻辑在里面的,来看handler.dispatchMessage

惊呆了有木有,handleMessage方法是优先级最低的!为什么呢,因为有Handler有个post方法呀。post方法的参数是一个Runnable对象,然后通过创建一个Message,再把message的callback设置为这个runnable,然后再发送到消息队列里面。post方法的场景是你更新UI的时候需要知道获取到了什么新的数据,然后直接更新。而handleMessage方法可以不需要知道更新了哪些数据,就仅仅更新UI就可以了(当然,神奇的Message里面还配置了bandle,可以传数据的,所以其实都差不多,哈哈)。至于Handler的mCallback成员则是回调函数的通用写法,跟handleMassage的方式差别也不大。到这里就能明白为什么handleMessage在定义的时候不是abstract protected void handleMessage()了吧,因为他确实不是必须的。。。

分析到这里,可以看出,handler之所以可以更新UI,不是系统做了什么神奇般的兼容,而是因为他跟UI线程使用的本来就是同一个线程,UI线程通过Looper.loop来等待消息的分发,handler发送消息后把消息放到消息队列里面,而Looper负责从消息队列里面拿数据,又交给handler进行处理,最终实现了UI的异步更新操作。这是个生产者消费者模型典型应用,其中消息队列的功劳巨大,我们来看看他有哪些功能。如果我们开发中也需要实现这种类似的生产者消费者模型,可以使用这一套机制。需要注意的是,MessageQueue我们不能单独定义来使用,因为其核心API的可访问修饰符都是包级别的,我们不能把代码定义到android.os这个包下面,所以他要通过配合Looper来使用。Looper的核心API都是开放的。

5. 消息队列

enqueueMessage入列,可以把消息放到队列里面,这里队列的底层是android的本地代码实现的,其实可以参照juc里面的DelayQueue的实现,机制差不多,都支持延时出列的,这不过实现方式不同罢了。

next()出列,消息出列,队列的出列优先级是入列的时候定义的时间来决定的,时间值越小(长整形)优先级越高。

RESTful与伪RESTful

REST

关于RESTful,前几年就开始火了,其崛起是跟各大互联网开放平台的OPENAPI息息相关的,twitter的API就是声称完全RESTful的,不过其实呢,还是有些是伪RESTful的。那么,什么是RESTful呢?

REST很早(2000年)就有个博士( Roy Fielding,HTTP规范的主要编写者之一)提出来了,英文全称是REpresentational State Transfer,直译过来就是表述性状态转移,看起来更像物理学概念有木有?其核心理念就是把提供服务的所有事物定义一个ID,而这个ID在HTTP服务中通常是一个URI,然后使用标准的HTTP方法(HTTP Method,比如GET, POST, PUT, DELETE, HEAD,TRACE等)来提供同一个资源的不同服务(通常就是CRUD)。至于无状态这一点,HTTP都是无状态了,实际上也可以不用去深究。

那么,RESTful究竟是怎样一种风格呢?看几个例子:

http://www.zhihu.com/people/jiacheo

http://weibo.com/1790181393/AxqXitFF6

http://weibo.com/1790181393

看出来了没有,这里的URI都是 资源URI+资源ID组成的,也就是说,我们要访问一个资源的某种服务的时候,可以直接附上ID,就能获取了,如果要修改资源呢?HTTP方法中有POST, PUT, DELETE等可以对应用来新增/修改/删除资源的操作,但是现实中,我们大多数人用的浏览器都仅支持GET和POST两个方法,所以这个时候RESTful的理念在WEB站点中就无法完美实现了。上面说到,RESTful火起来那段时间刚好是各大开放平台开始崛起的时间,因为他们的OPENAPI大多数是RESTful风格的,由于提供API比展示页面要简单很多,仅提供CRUD操作就可以实现了,这么一来,HTTP METHOD就足够用了,而且各种编程语言提供的httpclient基本也都支持这几个方法,所以RESTful能在OPENAPI中大行其道跟API的简洁性是分不开的。

不能简单的认为没有后缀名的URI就是RESTful的,也不能认为有后缀名的URI就不是RESTful的,RESTful风格不在于你有没有后缀名(RESTful的API接口中,后缀名通常是为了实现区分返回的数据格式,如xml/json等),而是在于你的URI定义是不是对应到一个资源(这里通常是个名词),而不是一个操作(通常是动词)。由于浏览器不支持更多的HTTP方法,所以伪RESTful诞生了。

怎样的URI是伪RESTful呢?比如说这个:

http://www.zhihu.com/people/edit

这个是以动词结尾的URI,定位到的不是一个资源,而是一个动作(编辑),这个页面主要是用于编辑个人资料的时候使用的,如果要RESTful化,该怎么设计呢?

http://www.zhihu.com/people?to=edit

谁说RESTful不能有URL参数的?没有参数你如何实现复杂的查询(query)?

上面说到twitter的api中也有伪RESTful的,咱们来瞧瞧:

http://api.twitter.com/users/show.format

这里的URI是定位到show这个动作,然后传入的参数是用户的ID, 实际上, 如果遵循RESTful风格的话,可以这么搞

http://api.twitter.com/users/id.format 或者 http://api.twitter.com/users.format?id=

伪RESTful也是有存在的价值的,由于浏览器的原因,仅通过HTTP方法无法支持所有的资源的操作,那么何不把对资源的操作也定义一个URI呢?这样一来,伪RESTful其实对网站的开发者更友好,因为他一看这个URI就能知道对应后台的controller(MVC中的C)中的哪一个处理器,在写代码,调试的时候也有优势,不用去看POST的参数中对应的action(动作)究竟是谁,直接就知道了。

那么RESTful与伪RESTful孰优孰劣呢?其实没有什么定论,适合就好,管他是不是RESTful呢(你是处女座?当我没说)!但是当你在对外宣称你提供的API是标准的RESTful风格的时候,就要好好审视一下自己的API是不是标准的RESTful风格了。

国内的网站RESTful风格的也有,比如新浪微博,他的访问页面基本上都是标准的RESTful风格的(修改数据的URL我没研究,不能下定论)所以关于RESTful与伪RESTful,你是怎么看的呢?

apache & tomcat https配置

出于安全的考虑,一是确实能带来一点安全性提升,二是让用户看起来安全,一些网站开始要使用https协议来提供服务(web,api),刚好我们新产品是面向企业级的用户,需要提供https服务和接口,所以就遇到了配置问题。

首先这篇文章是在你申请好了https(ssl)证书之后该干的事。本文介绍是linux系统下的apache2的https配置,windows下的基本相同,就是一些文件的路径写法不一样还有模块的安装不一样。

使用apache2的话,他自己内建了(build-in)mod_ssl 模块,不需要额外编译和安装,如果你用的apache1.*系列的,可以使用 sudo yum install mod_ssl 来给apache安装mod_ssl

如果是内建的mod_ssl, 不需要再httpd.conf里面配置LoadModule,默认是可以直接用的,如果是自己安装的,则需要LoadModule一下。下面讲具体配置

1. 首先你申请到https证书后,服务提供商会给你如下几个文件。

server.crt

server.key

ca.pem

sub.class1.server.ca.pem

说明一下, server.crt 是服务提供商颁发给你的证书,是一个纯文本文件,一般以

—–BEGIN CERTIFICATE—–

开头,以

—–END CERTIFICATE—–

结尾,前后都没有多余的空格。

server.key就是这个证书的私钥文件,用来解密用的,也是一个纯文本文件,以

—–BEGIN RSA PRIVATE KEY—–

开头,以

—–END RSA PRIVATE KEY—–

结尾,首位也都不带空格。

另外两个*.pem文件,是CA证书,其中ca.pem是root证书,sub.class1.server.ca.pem 则是颁发证书的网站给你的的这个https的证书的服务商证书,也就是说你要认证你的https(ssl)证书必须先通过这个sub.class1.server.ca.pem再通过ca.pem来完成整个链路的验证.

2. 接下来就是apache配置证书文件的路径了

先把上述文件都统一放到一个目录,要注意保障文件的安全,不要被盗用或者删除。这里我们放在 /var/www/ssl 目录下面

打开httpd-ssl.conf文件(一般在%apache安装目录%/conf/extra下面

首先配置证书文件:

SSLCertificateFile “/var/www/ssl/server.crt”
找到相应的项,有的话就覆盖,没有的话就新建一个,指定证书文件的位置
接下来是配置证书私钥文件:
SSLCertificateKeyFile “/var/www/ssl/server.key”
配置方法同上
最后配置证书链路文件路径
上面说到,我们证书要先通过sub….pem然后在通过root认证,所以需要把这两个文件合并。
cat  sub.class1.server.ca.pem  ca.pem  > ca.chain.pem.crt
然后定位到这个ca.chain.pem文件
SSLCertificateChainFile “/www/var/ssl/ca.chain.pem.crt”
至此,整个证书的配置就OK了,可以重启下apache,访问以下你的https://域名/ 就能看到已经生效。
3. 配置完证书后,配置一下apache请求转发给tomcat,有多种方式,可以用mod_jk,也可以用ProxyPass,我这里用ProxyPass。
还是编辑同一个文件httpd-ssl.conf
在<VirtualHost _default_:443>定义块的里面,添加ProxyPass

ProxyPass / ajp://localhost:7204/
ProxyPassReverse / ajp://localhost:7204/

我这里是把请求转发给ajp去处理,tomcat默认都会有ajp的端口,因为我应用比较多,为了区分开了,所以我自己使用了7204端口,如果默认的话,应该是8009端口,是在不清楚可以看下tomcat的配置(%tomcat安装目录%/conf/server.xml内的Connector protocol=”AJP/1.3″ 这个配置项)
如果我这里使用ProxyPass / http://localhost:8080/ 可不可以,答案是可以的,但是你的tomcat在处理request的时候,无法分辨用户访问的url是不是https的(当然apache可以配置保留用户的原始请求url给tomcat)
4. 大部分的https的ssl证书是颁布给某个子域名的,如果你的所有子域名都需要上https,建议使用购买一个可以多次使用的。对了,一开始可以申请startssl来用一下,可以免费使用30天,测试完了,可以再买高端的。
运行起来了,但遇到如下情况,该怎么办
nosecure
不要慌, 这时候你的https已经生效了,唯一的问题是你的页面请求的资源使用了其他非https的资源,比如cdn的图片,css等的引用,如果是js资源,建议都要使用https,不然浏览器会认为很不安全的哦。

通过apache proxy访问tomcat ERR_CONNECTION_RESET 问题排查

0. 这是个钓鱼贴你造么,看到后面你就造了。

1. 现象, 通过浏览器访问 www.qipeng.com 返回页面链接被重置

查看apache-access 日志, 是有发起请求的, 说明在proxy那里存在问题

2. ping www.qipeng.com 结果是DNS已经生效的
1
3. 改用mod_jk, 而不是proxy来处理请求转发, 发现服务器身上是可以curl通的
2
注意这台curl的机器不是企朋的服务器本身, 而是在服务器上随便找的
4. 然后用本地访问依旧是
3
5. 怀疑是DNS问题, 于是本地配置了hosts, 把www.qipeng.com 强制指向 223.4.49.236
结果还是一样, 本地配置别的域名 weike.taovip.com 指向 223.4.49.236, 访问这个域名, 结果是正确的, 我嚓, 这个正是我的企朋的页面!
4
6. 那么到底他妈的问题出在哪里呢,难道我被我自己墙了?找其他同事, 结果也是一样的- -||
找了半天, 突然想起老大好像昨天刚申请域名备案, 于是开始往这方面靠, 结果真正的原因是域名正在备案中, 期间访问不了, 呵呵! 天朝, 我又涨姿势了!
7. 解决方案
7.1 其实不需要解决, 产品也是刚做, 还没啥用户, 自己本地配置个其他域名先顶一阵等备案好了就OK了.
7.2 真的必须要访问的话可以找个国外的服务器, 先跑一跑, 前提是你的程序没有太多其他的依赖, 否则得不偿失.
7.3 知乎某知友的对策: http://www.zhihu.com/question/19794926