分类目录归档:编程语言

记一次使用高版本jdk接口被坑的经历

最近在一个项目中,使用了一个jdk1.8才有支持的方法:List.sort,开发的时候没有发现注释那写着“since 1.8”,然后打包,发布了。而线上的环境用的是1.7!!!所以在线上访问含有用到这个高版本接口的逻辑的api时,就报500错误了。
我的开发工具是eclipse,本机的jdk版本是1.8,项目是用maven打包的,并且打包时jdk版本设置为1.6。
后来经过研究,发现以下3种情况:

  • 在preference->java->install jres中,将Java se 7选中的话,代码直接报错了。应该是直接改变了整个工作空间的运行环境版本,所以有正确的检验。
    WechatIMG27
  • 如果不改installed jres,而是将server中的运行环境,则会在运行时报error(是error,不是异常)。 通过这样,就可以在本地测试时发现问题了。
    WechatIMG47
    WechatIMG48

  • 改项目的编译版本不起作用,还不知道为什么。

所以后面做项目,看来不仅要将编译等级改成和线上的一致,同时还要将installed jres中改成相同的版本或在server使用的运行环境中设置

Jedis不相信眼泪

最近愉快地上线了一个新的调度系统,主节点和从节点使用redis队列进行通信,然后奇怪的事情发生了,有一个海外的节点,和redis之间的网络一直比较差,经常出现异常(类似下面):

redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
    at redis.clients.jedis.Protocol.process(Protocol.java:79)
    at redis.clients.jedis.Protocol.read(Protocol.java:131)
    at redis.clients.jedis.Connection.getAll(Connection.java:225)
    at redis.clients.jedis.Connection.getAll(Connection.java:217)
    at redis.clients.jedis.Pipeline.sync(Pipeline.java:68)
    at com.game.data.stat.share.redis.JedisUtil.batchPop(JedisUtil.java:151)
    at com.game.data.stat.share.db.JedisUtilTest.testBatchPop(JedisUtilTest.java:185)
    at com.game.data.stat.share.db.JedisUtilTest.main(JedisUtilTest.java:488)
Caused by: java.net.SocketTimeoutException: Read timed out
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.read(Unknown Source)
    at java.net.SocketInputStream.read(Unknown Source)
    at java.net.SocketInputStream.read(Unknown Source)
    at redis.clients.util.RedisInputStream.fill(RedisInputStream.java:109)
    at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:45)
    at redis.clients.jedis.Protocol.process(Protocol.java:64)
    ... 7 more

一开始以为是纯粹的网络延时太大,于是加大了的socket的超时时间,可是这个异常还是如大姨妈一般时常来访,而在重启服务之后,异常便会消失。
于是在本地做了下测试,发现当断开redis连接之后重连,便会不断出现如上异常。
可是奇怪的是异常只出现在使用redis管道的地方,其他redis操作一切正常,如下:

Pipeline pipe = jedis.pipelined();
List<Response<String>> responseList = new ArrayList<Response<String>>();
for (int i = 0; i < size; i++) {
     responseList.add(pipe.lpop(key));
}
pipe.sync();

在调用管道的sync方法时,抛出了异常,本着先简单快速解决问题的思路,试着把jedis客户端从2.1升级到2.7,再测试,异常消失了,于是更怀疑这是jedis客户端的bug。
异常是没出现了,可是原因依旧未知,于是继续测试,先贴一下部分测试代码:

 public static void testBatchPop() {

        String key = "test_key";
        // for (int i = 0; i < 10; i++) {
        // JedisUtil.push(key, i + "");
        // }
        List<String> list = null;
        String value;
        while (true) {
            try {
                value = System.currentTimeMillis() + "";
                System.out.println("push:" + value + ", return:"
                        + JedisUtil.push(key, value));
                System.out.println("push:" + value + ", return:"
                        + JedisUtil.push(key, value));

                // batch pop
                try {
                    // String str = JedisUtil.pop(key);
                    // System.out.println("pop:" + str);
                    // 使用redis管道批量lpop
                    list = JedisUtil.batchPop(key, 3);
                    if (list == null || list.isEmpty()) {
                        System.out.println("empty list");
                    } else {
                        for (String str : list) {
                            System.out.println("pop:" + str);
                        }
                    }
                } catch (Exception e) {
                    System.out.println(ExceptionUtil.parseString(e));
                }

                System.out.println("length of " + key + ":"
                        + JedisUtil.llen(key));
            } catch (Exception e) {
                System.out.println(ExceptionUtil.parseString(e));
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println(ExceptionUtil.parseString(e));
            }
        }
    }

代码很好理解,不断往redis队列里面push消息,再批量pop出来消费,断开重连redis之后,batchPop方法抛异常,push方法正常。再贴一下其中使用到redis管道的方法,目光如炬的小伙伴可能已经发现问题所在了:

public static List<String> batchPop(String key, int size) {
        Jedis jedis = pool.getResource();
        try {
            Pipeline pipe = jedis.pipelined();
            List<Response<String>> responseList = new ArrayList<Response<String>>();
            for (int i = 0; i < size; i++) {
                responseList.add(pipe.lpop(key));
            }
            pipe.sync();
            List<String> list = new ArrayList<String>();
            for (Response<String> response : responseList) {
                if (response != null && StringUtils.isNotBlank(response.get())
                        && !NULL_RES.equals(response.get())) {
                    list.add(response.get());
                }
            }
            return list;
        } catch (JedisConnectionException ex) {
            log.error("Jedis Connection Exception");
            returnBrokenResource(pool, jedis);
            throw ex;
        } finally {
                returnResource(pool, jedis);
        }
    }

该方法使用jedis连接池获取连接,每次批量从redis队列中pop消息。看到这里的代码,立刻把Jedis连接池pool列入可疑名单中,猜测会不会是returnBrokenResource的时候,没有把断开的连接给销毁掉。
为了排除是否连接池也有问题,所以将以上代码改为每次操作时,先new一个新的Jedis,而不是使用连接池,结果异常消失了。
然后自己又根据jedis的源代码,使用Apache的Commons Pool 1.5.5写了个简单的连接池(jedis 2.1也是使用了这个版本的Commons Pool),将创建对象,销毁对象每一步的日志都打印出来,并且对jedis进行一层封装,赋予一个ID,这样易于观察每个jedis链接的生命周期,部分代码如下:

    private static class JedisTest extends Jedis {
        private int jedisID;
        public int getJedisID() {
            return jedisID;
        }
        public void setJedisID(int jedisID) {
            this.jedisID = jedisID;
        }
        public JedisTest(String host, int port, int connectionTimeout,
                int soTimeout, int jedisID) {
            super(host, port, connectionTimeout, soTimeout);
            this.jedisID = jedisID;
        }
    }

使用Commons Pool时,需要创建一个工厂类,将jedis连接池源代码中的工厂类拷贝出来使用即可:

    private static int NUM;
    // jedis 2.1
    private static class JedisFactory extends BasePoolableObjectFactory {
        private final String host;
        private final int port;
        private final int timeout;
        private final String password;
        private final int database;

        public JedisFactory(String host, int port, int timeout,
                String password, int database) {
            this.host = host;
            this.port = port;
            this.timeout = timeout;
            this.password = password;
            this.database = database;
        }

        public Object makeObject() throws Exception {
            NUM++;
            JedisTest jedis = new JedisTest(this.host, this.port, this.timeout,NUM);
            System.out.println("makeObject jedisID:" + jedis.getJedisID());
            jedis.connect();
            if (null != this.password) {
                jedis.auth(this.password);
            }
            if (this.database != 0) {
                jedis.select(this.database);
            }

            return jedis;
        }

        public void destroyObject(Object obj) throws Exception {
            if (obj instanceof Jedis) {
                JedisTest jedis = (JedisTest) obj;
                System.out.println("destroyObject jedisID:"
                        + jedis.getJedisID());
                if (!(jedis.isConnected()))
                    return;
                try {
                    try {
                        jedis.quit();
                    } catch (Exception e) {
                    }
                    jedis.disconnect();
                } catch (Exception e) {
                }
            }
        }

        public boolean validateObject(Object obj) {
            boolean res = false;
            if (obj instanceof Jedis) {
                JedisTest jedis = (JedisTest) obj;
                System.out.println("validateObject jedisID:"
                        + jedis.getJedisID());
                try {
                    res = ((jedis.isConnected()) && (jedis.ping()
                            .equals("PONG")));
                } catch (Exception e) {
                    res = false;
                }
            }
            System.out.println("validateObject return:" + res);
            return res;
        }
    }

经测试,确实调用了destroyObject方法,但是每次从连接池中拿到的都是上次销毁的jedis连接。
回头看一下上面的batchPop方法,发现问题所在了,当抛出JedisConnectionException异常时,调用了returnBrokenResource,将连接放回池中销毁,但是最后的finally代码块里又调用了一次returnResource。没错,这里两次将连接放回了连接池里,所以这个broken的jedis根本没有被销毁掉,又被重复拿出来使用了。
但是,真相到这里还远远没有明了,为啥使用管道lpop的时候异常,使用非管道push的时候却是正常的?
深入看一下jedis管道lpop的源代码,如下:

public Response<String> lpop(String key) {
        this.client.lpop(key);
        return getResponse(BuilderFactory.STRING);
    }

对比下非管道时的lpop方法:

public String lpop(String key) {
        checkIsInMulti();
        this.client.lpop(key);
        return this.client.getBulkReply();
    }

发现它们其实都使用了this.client.lpop(key),继续进入这个方法,一直点啊点,直到这里:

protected Connection sendCommand(Protocol.Command cmd, byte[][] args) {
        connect();
        Protocol.sendCommand(this.outputStream, cmd, args);
        this.pipelinedCommands += 1;
        return this;
}

一个重要的方法出现了:connect(),点进去:

    public void connect() {
        if (isConnected())
            return;
        try {
            this.socket = new Socket();

            this.socket.setReuseAddress(true);
            this.socket.setKeepAlive(true);
            this.socket.setTcpNoDelay(true);
            this.socket.setSoLinger(true, 0);

            this.socket.connect(new InetSocketAddress(this.host, this.port),
                    this.timeout);
            this.socket.setSoTimeout(this.timeout);
            this.outputStream = new RedisOutputStream(
                    this.socket.getOutputStream());
            this.inputStream = new RedisInputStream(
                    this.socket.getInputStream());
        } catch (IOException ex) {
            throw new JedisConnectionException(ex);
        }
    }

没错,所有的redis操作都会调用到sendCommand方法,而该方法会先判断jedis客户端是否连接,如果不是,则重新new一个socket,这就解释了,为什么断开redis之后重连,可以继续往redis队列中push消息,但是,更疑惑的是,既然重新new了socket,为何管道操作还是会报异常。
继续测试,发现虽然 JedisUtil.batchPop抛出了异常,但是redis队列的长度并没有在增加,所以队列里面的消息应该已经被成功pop出来了,而且,当batchPop的数量为1时,程序竟然偶尔会恢复正常。
继续研究pipline的sync代码,发下如下:

    public List<Object> getAll(int except) {
        List all = new ArrayList();
        flush();
        while (this.pipelinedCommands > except) {
            try {
                all.add(Protocol.read(this.inputStream));
            } catch (JedisDataException e) {
                all.add(e);
            }
            this.pipelinedCommands -= 1;
        }
        return all;
    }

其中pipelinedCommands记录了往redis发送的命令的条数(见上面的sendCommand方法),所以调用sync取返回结果时,要循环pipelinedCommands次,从输入流中读取返回结果,程序也是在这里抛出了SocketTimeoutException。对比这里的sync方法与其他非管道操作获取结果的方法,区别便在于其他操作只调用了一次Protocol.read(this.inputStream),而管道要调用pipelinedCommands次。
注意到这点,真相已经隐隐约约出现了,做个假设,如果pipelinedCommands的数量大于所要读取的返回结果,那么这里是否就读不到输入流的结果,然后就一直阻塞直到socket timeout?
重新回过头看一下前面的代码,梳理整个流程,测试过程中,最耗时的其实是在调用sync读取返回结果,所以连接中断最有可能在这个时候发生,抛出异常,然后这个已经broken的jedis又被放回到了连接池中,下一次又被重新从连接池中拿出来,此时调用pipline.sync之前,虽然socket又重新new了一遍,但是pipelinedCommands的值并没有清0啊,因此调用batchPop的时候,pipelinedCommands的数量要大于所要返回的结果数量,因此读取输入流时阻塞,直到抛出socket timeout异常。
测试中,如果批量获取1条记录,就减少了pipelinedCommands大于返回结果数量的概率,所以偶尔可以测试正常!
另外,看一下jedis的配置文件:

redis.pool.testOnBorrow=true
redis.pool.testOnReturn=true
redis.pool.testWhileIdle=true

发现从连接池获取jedis之前都会检查是否已连接,如果获取到的是broken的jedis,应该会重新创建一个才对。仔细打印出redis.pool.testOnBorrow的值,发现被坑了,这里后面多了个空格!空格!空格……WTF!
再接着看一下jedis 2.7,发现其用了commons pool2-2.3,对比Commons Pool 1.5.5的源代码(好累,不贴代码了),在新的版本中,如果一个对象如果已经被销毁,则不能重新放回对象池中,因此每次从连接池中拿到的是新的jedis,所以没有出现上面的异常。
到这里,总结一下,这个问题的发生其实是正好三方的代码都有问题:

  • 业务代码中,重复将已经broken的jedis放回了连接池中;
  • Commons Pool 1.5.5又没有对重复returnObject做限制;
  • jedis重新new socket的时候,也没有清空pipelinedCommands。

在新版本的jedis客户端中,其实已经简化了编程,如下使用即可:

    public static long push(String queueName, String value) {
        Jedis jedis = null;
        try {
            jedis = pool.getResource();
            // 以队列右边为尾,将数据从右边插入
            return jedis.rpush(queueName, value);
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }

看一下jedis.close的源代码,如下:

    public void close() {
        if (this.dataSource != null) {
            if (this.client.isBroken())
                this.dataSource.returnBrokenResource(this);
            else
                this.dataSource.returnResource(this);
        } else
            this.client.close();
    }

这里已经根据客户端是否为broken,决定如何返还连接。当发生连接异常时,client.borken将被标志。

回头想一想,在查找这个问题时,也犯了一些错误,如果一开始就可以仔细梳理代码流程,不要盲目测试,也不用花这么多时间。
与君共勉吧!


简单,率性,不虚妄

Java并发编程的特性

1. Java内存模型:

java内存模型

       java的对象都是在主内存(物理内存)中创建的;而各个线程在执行的时候,为了隔离相互之间的影响,以及降低CPU和内存IO之间速率相差太大的影响,会将主内存中的变量读取到工作内存中(高速缓存,寄存器),然后在工作内存中操作,完成之后再写回到主内存中。
       主内存和工作内存中的操作由以下几部分操作组成,每部分均是原子性的:
内存操作

       首先通过read、load将变量的值放入工作内存中;当执行引擎需要使用变量时,使用use操作,读取工作内存的变量,操作完之后使用assign,将新的变量值写回工作内存;当需要将变量值同步回主内存时,通过store、write操作将工作内存的值刷新到主内存中。
       java虚拟机规定read和load,store和write必须成对出现。
       每次lock一个变量,将会清空该变量在工作内存中的值。

2. 并发编程的3个特性

2.1 原子性:

  • 基本变量的读取和赋值是原子性的,结合上面的操作顺序,即可得知,因为每次read和write均是原子性的。但是java对于64位的变量,允许拆分成两部分的32位分别操作,因此对于double和long的读写,可能没有原子性。但实际上大多数jvm在具体实现时,也都将这两种类型的读写实现为原子性了。
  • synchronized。必须先lock才能进入同步代码块,因此一个线程在unlock之前,其他线程无法进入同步代码块,保证了同步代码块的原子性。

2.2 可见性

  • volatile。被volatile修饰的变量,在每次use之前,都需要先read和load,因此保证了每次读取的都是主内存的最新值;而在每次assign的时候,都需要store和write回主内存,因此保证了最新值可以刷新到主内存。
  • synchronized。每次进入同步代码块之前,都会先lock住变量,然后清空工作内存中共享变量的值;每次退出同步代码块之前,都会先将工作内存中的共享变量刷新到主内存中,然后unlock(关于synchronized的可见性,可以见:synchronized可见性)。

2.3 有序性

  • volatile。JVM规定,在volatile之前的操作,不能重排序到volatile之后。
  • synchronized。同步代码块对于不同线程来说串行进入的。

3. 一段同步代码的分析

import java.util.Date;
import java.util.concurrent.TimeUnit;

public class ThreadTest extends Thread {
 private static boolean stop;
 // private static volatile boolean stop;

 synchronized void f() {}

 @Override
 public void run() {
  System.out.println("start");
  while (!stop) {
   // f();
   // System.out.println("loop");
  }
  System.out.println("end");
 }

 public static void main(String[] args) {
  try {
   ThreadTest t = new ThreadTest();
   t.start();
   Thread.sleep(2000);
   t.stop = true;
   System.out.println("stop");
   Thread.sleep(2000);
  } catch (InterruptedException e) {
   System.out.println(e.getMessage());
  }
 }
}

       由于stop变量的不可见性的,所以线程并不会在stop设为true之后停止,原因是stop不具有可见性,但是如果用volatile修饰stop,或者在循环中调用synchronized方法,则可以使线程停下来;另外循环中调用控制台输出,也可以停下线程,猜测是输出到控制台的时候调用了synchronized方法。
       这段类似的代码在《Effective Java》中提到,编译器在优化的时候也可能优化为:

while(!stop){
    while(true){
        // do something
    }
}

4. 双重锁单例模式的一个细节分析

private static volatile TestSingleton instance = null;  

    public static TestSingleton getInstance() {  
           if (instance == null) {    
             synchronized (TestSingleton.class) {    
                if (singleton == null) {    
                   singleton = new TestSingleton();   
                }    
             }    
           }   
           return instance;  
    }  

       上面是一个典型的单例实现方式,关于volatile的作用,大部分地方会解释为可见性,但是此处使用synchronized已经保证了singleton的可见性,为何还要使用volatile。其实这里更多的是考虑有序性,防止代码重排序,如果在创建TestSingleton的时候,先将TestSingleton对象的内存地址返回给singleton了,而TestSingleton还没有完成初始化,此时,其他线程可能拿到一个不完整的singleton。具体可见:单例模式的实现,在这篇博文的评论中有说到。

参考资料:

  • 《深入理解Java虚拟机》
  • 《Effective Java》
  • http://blog.csdn.net/jason0539/article/details/23297037
  • http://www.imooc.com/video/6780

C++字符串转码处理

公司项目要做一个桌面应用,需要使用nodejs V8编写C++ addon(扩展)。
在C++的方法中获取入参中含有中文时,使用V8 String类下的Utf8Value类,可以构造出UTF-8编码的字符串,代码如下:

String::Utf8Value param1(args[0]->ToString());

String::Utf8Value重载了operator,返回char 类型,所以可以用

std::string str = string(*param1)

这样的方法来创建一个c++的string对象。
这样对于英文来说,是没有问题,但是对于中文,会出现乱码的情况。
一开始搜索资料时,只是将关键字设定为“char*转string 中文乱码”,能找到一些方法,多数如
http://www.blogbus.com/hx1987-logs/211009970.html
这个blog里面说的,但是都不凑效。(不过也不是没有收获,至少还了解到MultiByteToWideChar这个方法和wchar_t这个类型,C++字符转码都是需要MultiByteToWideChar来转换的)
其实这样搜出来的结果,基本上都有一个共同的问题,就是没有说明这个方法是由什么编码转成什么编码。
因为我入参是UTF8编码,而C++的中文编码,是unicode或GBK,所以查找的方向应该是如果将UTF8编码转换成GBK或unicode编码。
分享两篇博文:
关于C++中文字符的处理
VC URLEncode & UrlDecode (这篇的的话是从另外一个方向找到的:我的一个同事说,应为是用node调用,可以在js中先用encodeURI编码后再调用addon,所以我就往这个方向的搜索了一下,但是最后发现,其实在js中用decodeURI时,也是用UTF8来URIencode,所以在C++中使用URIdecode后,还是UTF8编码,不过里面的Utf8ToStringT方法最终帮我解决了问题^-^)

CString Utf8ToStringT(LPSTR str)
{
    _ASSERT(str);
    USES_CONVERSION;
    WCHAR *buf;
    int length = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
    buf = new WCHAR[length+1];
    ZeroMemory(buf, (length+1) * sizeof(WCHAR));
    MultiByteToWideChar(CP_UTF8, 0, str, -1, buf, length);

    return (CString(W2T(buf)));
}

能得到正确编码的CString,然后就好办了:
CString,string,char*之间的转换(转)

最后在C++中处理完后,还需要返回中文提示信息,所以需要将GBK转成UTF8~~~~下面的一篇问题贴帮到了我:
http://bbs.csdn.net/topics/320078111
GBK转UTF8

void ConvertGBKToUtf8( CString& strGBK )
{
    int len=MultiByteToWideChar(CP_ACP, 0, (LPCTSTR)strGBK, -1, NULL,0);
    WCHAR * wszUtf8 = new WCHAR[len+1];
    memset(wszUtf8, 0, len * 2 + 2);
    MultiByteToWideChar(CP_ACP, 0, (LPCTSTR)strGBK, -1, wszUtf8, len);

    len = WideCharToMultiByte(CP_UTF8, 0, wszUtf8, -1, NULL, 0, NULL, NULL);
    char *szUtf8=new char[len + 1];
    memset(szUtf8, 0, len + 1);
    WideCharToMultiByte (CP_UTF8, 0, wszUtf8, -1, szUtf8, len, NULL,NULL);

    strGBK = szUtf8;
    delete[] szUtf8;
    delete[] wszUtf8;
}

使用jQuery的clone+模板的形式动态生成页面

公司的官网改造,其中有些地方要获取服务器返回的json数据,并动态生成列表。

原来的代码是用字符串的形式拼接html标签内容来动态生成的。这样虽然是没什么问题,但是我觉得代码看起来不够整齐,并且后面为何还得在一串String中找到要修改的地方,实在够难受。所以想着能不能在html先写好一个模板,然后在js中复制这个模板,然后修改需要变动的地方后,再嵌入到页面中呢?

有了这个想法,就到网上各种搜,后来发现使用jQuery选择出来的元素对象,有个clone方法,刚好适用。有了可行性后就着手开始尝试了。

1、添加模板

<div id=”news_list”></div>
<div id=”news_templete” style=”display:none;”>
<div class=”divider”></div>
<div class=”media”>
<div class=”blink-dot-top”></div>
<div class=”blink-dot-bottom”></div>
<div class=”blink-dot-right”></div>
<div class=”blink-dot-left”></div>
<div class=”media-left”>
<!– <a href=”#”> –>
<img class=”news_thumd_img media-object” src=”” alt=”…”>
<!– </a> –>
</div>
<div class=”media-body”>
<h4 class=”media-heading”><a class=”news_title_link” href=”#” data-toggle=”modal” data-target=”.bs-example-modal-lg”></a></h4>
<h4 class=”news_time media-heading”>
<small>2015/05/15</small>
</h4>
<div class=”news_body”>
</div>

</div>
</div>
</div>

例如这里的一个id=”news_templete”的div包裹的元素,就可以作为一个模板,用于在页面展示一条新闻。

2、使用js动态复制模板

for(var i = 0 ; i < newsJson.length; i++){

var newTitle = newsJson[i].title;
var newContent = newsJson[i].content;
var newPicUrl=newsJson[i].picUrl;
if(null==newPicUrl||””==newPicUrl){
newPicUrl=”images/123.JPG”;
}
newsListContent.push(newContent);//储存起来,点展开时显示内容
var newContentFormatted = formatContent(newContent);
if(newContent.length>150){
newContentFormatted+=’…’;
}
var newTime=””;
if(newsJson[i].time != null){
newTime = dateTotime(newsJson[i].time);
}else{
newTime = “”;
}
var new_temp_div=$(“#news_templete”);
var newDiv = new_temp_div.clone(true);
newDiv.removeAttr(“id”);
newDiv.find(“.news_thumd_img”).attr(“src”,newPicUrl);
newDiv.find(“.news_title_link”).attr(“onclick”,”javascript:showNewsDetail(“+i+”);”);
newDiv.find(“.news_title_link”).html(newTitle);
newDiv.find(“.news_time”).html(“<small>”+newTime+”<small>”);
newDiv.find(“.news_body”).html(newContentFormatted);
newDiv.show();//取消从模板中带过来的“隐藏”设置
$(“#news_list”).append(newDiv);
}

根据服务器返回的json对象列表,循环生成需要展示新闻列表。

蓝色字体部分,就是使用clone方法复制news_templete模板后,将json的数据填充到这个新div中。这个newDiv就是一条新闻,最后使用append的方式添加到页面中($(“#news_list”).append(newDiv);,其中$(“#news_list”)即为你想要这些内容出现的地方,如第一点中的蓝字html标签)。

这样代码看起来就整齐多了,后面修改也比较容易。如果只是需要修改页面布局,则只需要修改页面中的模板即可。

后来发现公司有用一个react.js这套界面构造框架,用来动态构建界面,感觉还是比较方便。嗯,它是Facebook推出的。

reactjs官网

 

Spring 使用@AspectJ实现aop

《Spring参考手册》中定义了以下几个AOP的重要概念:

  • 切面(Aspect) :官方的抽象定义为“一个关注点的模块化,这个关注点可能会横切多个对象”,在本例中,“切面”就是类TestAspect所关注的具体行为,例如,AServiceImpl.barA()的调用就是切面TestAspect所关注的行为之一。“切面”在ApplicationContext中<aop:aspect>来配置。
  • 连接点(Joinpoint) :程序执行过程中的某一行为,例如,AServiceImpl.barA()的调用或者BServiceImpl.barB(String _msg, int _type)抛出异常等行为。
  • 通知(Advice) :“切面”对于某个“连接点”所产生的动作,例如,TestAspect中对com.spring.service包下所有类的方法进行日志记录的动作就是一个Advice。其中,一个“切面”可以包含多个“Advice”,例如TestAspect
  • 切入点(Pointcut) :匹配连接点的断言,在AOP中通知和一个切入点表达式关联。例如,TestAspect中的所有通知所关注的连接点,都由切入点表达式execution(* com.spring.service.*.*(..))来决定
  • 目标对象(Target Object) :被一个或者多个切面所通知的对象。例如,AServcieImpl和BServiceImpl,当然在实际运行时,Spring AOP采用代理实现,实际AOP操作的是TargetObject的代理对象。
  • AOP代理(AOP Proxy) 在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。默认情况下,TargetObject实现了接口时,则采用JDK动态代理,例如,AServiceImpl;反之,采用CGLIB代理,例如,BServiceImpl。强制使用CGLIB代理需要将 <aop:config>proxy-target-class 属性设为true

通知(Advice)类型

  • 前置通知(Before advice) :在某连接点(JoinPoint)之前执行的通知,但这个通知不能阻止连接点前的执行。ApplicationContext中在<aop:aspect>里面使用<aop:before>元素进行声明。例如,TestAspect中的doBefore方法
  • 后通知(After advice) :当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。ApplicationContext中在<aop:aspect>里面使用<aop:after>元素进行声明。例如,TestAspect中的doAfter方法,所以AOPTest中调用BServiceImpl.barB抛出异常时,doAfter方法仍然执行
  • 返回后通知(After return advice) :在某连接点正常完成后执行的通知,不包括抛出异常的情况。ApplicationContext中在<aop:aspect>里面使用<after-returning>元素进行声明。
  • 环绕通知(Around advice) :包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。ApplicationContext中在<aop:aspect>里面使用<aop:around>元素进行声明。例如,TestAspect中的doAround方法。
  • 抛出异常后通知(After throwing advice) : 在方法抛出异常退出时执行的通知。 ApplicationContext中在<aop:aspect>里面使用<aop:after-throwing>元素进行声明。例如,TestAspect中的doThrowing方法。

好,进入主题。原先在项目中,在controller返回的状态码,是写在返回的json体中的,故无论业务逻辑是否成功执行,在返回到客户端时,Response的status都是正常(即http返回码是200)。现要改成根据controller返回的状态码,设置Response的status。由于已经存在很多controller方法,就不想一个个地找到每个方法的返回的地方来增加处理逻辑,所以决定用AOP来实现。

而我使用的@AspectJ的方式来实现。

首先要引入cglib-nodep.jar这个包,因为要使controller也能凑效,需要使用CGLIB代理。我使用的是3.1版本。

1、先定义切点

import java.lang.annotation.*;

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseStatusPC {

}

2、定义切面,由于这里是controller返回后处理的,所以实现“返回后通知(After return advice)”。

import javax.servlet.http.HttpServletResponse;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.kainaodong.xsst.common.api.constant.CommConstant;
import com.kainaodong.xsst.common.api.domain.ServiceRes;

@Aspect
@Component
public class HttpResponseStatusAspect {
final static Logger log = LoggerFactory.getLogger(HttpResponseStatusAspect.class);
//Controller层切点
@Pointcut(“@annotation(com.kainaodong.xsst.common.api.util.aop.ResponseStatusPC)”)
public void controllerAspect() {
//System.out.println(“controllerAspect”);
}

//Service层切点
@Pointcut(“@annotation(com.kainaodong.xsst.common.api.util.aop.ResponseStatusPC)”)
public void serviceAspect() {
}

@AfterReturning(pointcut=”controllerAspect()”,returning=”res”)
public void doAfter(JoinPoint jp,ServiceRes res){
int reCode=res.getReCode();
if(reCode!=CommConstant.RECODE_SUCCESS){
log.info(“根据controller的返回值设置HttpServletResponse的status:”+reCode);
setResponseStatus(jp.getArgs(), reCode);

}

}

public void setResponseStatus(Object[] args,int status){
for(Object arg:args){
if(arg instanceof HttpServletResponse){
//System.out.println(“HttpServletResponse”);
HttpServletResponse response=(HttpServletResponse) arg;
response.setStatus(status);
break;
}
}
}
}

ServiceRes是封装controller返回值的类。

3、将切点添加到目标对象中:

@ResponseStatusPC
public ServiceRes getUserInfor(HttpServletRequest request,
HttpServletResponse response,Integer userId,Integer loginUserId)

在controller中需要织入通知的方法上,添加@ResponseStatusPC切点的注解即可。

4、修改配置(一般是在applicationContext.xml中配置):

  • 增加切面的bean定义:<bean class=”com.aop.HttpResponseStatusAspect”></bean>  (我开发时,一开始忘了添加,写好切面,添加了切点,却怎样都没有代理成功)
  • 增加自动代理:<aop:aspectj-autoproxy proxy-target-class=”true”/>,如果是代理service,proxy-target-class=”true”可加可不加,而代理controller,则必须要加。

启动项目,即可发现添加了切点的method每次执行完返回后,都会调用HttpResponseStatusAspect.doAfter()。

get方式乱码解决

一开始并没有发现是get方式引起的问题,当中的幸酸不讲了,直接说结果:

  1. post方式将参数放在数据包的消息体中,get方式的参数放在URL中,而request.setCharacterEncoding(“UTF-8”)只对消息体起作用。

  2. 在struts2中,即时设置了<constant name=”struts.i18n.encoding” value=”UTF-8″></constant>,对于get方式,依旧不起作用。

  3. 使用过滤器,通过重写HttpServletRequest的getParameter方法,将返回的字符串重新编码,可以处理部分get方式乱码。

  4. 而struts2并不直接通过getParameter或getAttribute获取参数,因此,上面的方式不管用,所以只能考虑,将保存在HttpServletRequest中的参数值直接改为UTF-8编码,因此过滤器如下:

request.setCharacterEncoding(“UTF-8”);
Iterator iter = request.getParameterMap().values().iterator();
while (iter.hasNext()) {
String[] parames = (String[]) iter.next();
for (int i = 0; i < parames.length; i++) {
parames[i] = new String(parames[i].getBytes(“iso8859-1”), “utf-8”);
}
}

  1. 当然,也可以考虑使用拦截器来解决,对于struts2,如下:

@Override
public String intercept(ActionInvocation arg0) throws Exception {
// TODO Auto-generated method stub
ActionContext actionContext = arg0.getInvocationContext();
HttpServletRequest request= (HttpServletRequest)                  actionContext.get(StrutsStatics.HTTP_REQUEST);
// 以下代码同上面的过滤器
return arg0.invoke();
}

关于mybatis中mapper引用另外一个mapper的resultMap或select的问题

先来看一下两个mapper:

UserMapper.xml——主要完成对用户表(user)的表操作

<mapper namespace=”com.A.xsstuser.impl.dao.UserDao”>
<resultMap id=”BaseResultMap” type=”com.kainaodong.xsstuser.impl.model.User”>
<id column=”user_id” jdbcType=”INTEGER” property=”userId” />
<result column=”nickname” jdbcType=”VARCHAR” property=”nickname” />
<result column=”school_id” jdbcType=”INTEGER” property=”schoolId” />
<result column=”password” jdbcType=”VARCHAR” property=”password” />
……
</resultMap>
……
</mapper>

 UserDetailMapper.xml——主要完成对用户详情表(UserDetail)的表操作

<mapper namespace=”com.A.xsstuser.impl.dao.UserDetailDao”>
<resultMap id=”UserDetailMap” type=”com.kainaodong.xsstuser.impl.model.UserDetail” >
<id column=”user_id” property=”userId” jdbcType=”INTEGER” />
<result column=”realname” property=”realname” jdbcType=”VARCHAR” />
<result column=”gender” property=”gender” jdbcType=”TINYINT” />
<result column=”birthday” property=”birthday” jdbcType=”DATE” />
……
</resultMap>
<sql id=”UserDetail_Base_Column_List” >
user_id, realname, gender, birthday
</sql>
<select id=”selectUserDetailById” resultMap=”UserDetailMap” parameterType=”java.lang.Integer” >
select
<include refid=”UserDetail_Base_Column_List” />
from user_detail
where user_id = #{userId,jdbcType=INTEGER}
</select>
</mapper>

现在有一个需求是需要将User表和UserDetail进行关联查询。我的设计是将这个操作放在UserMapper.xml里面。复杂的关联查询在网上都有很多说明了,这里就不复述了。

既然UserDetailMapper.xml中已经有了映射UserDetail表的列,因此按照编程习惯,肯定要复用一下的,所以根据网上的资料,一开始我在UserMapper.xml里面添加了如下resultMap:

<resultMap id=”UserJoinUserDetail” type=”User”>
<id column=”user_id” jdbcType=”INTEGER” property=”userId” />
<result column=”nickname” jdbcType=”VARCHAR” property=”nickname” />
<result column=”school_id” jdbcType=”INTEGER” property=”schoolId” />
<result column=”password” jdbcType=”VARCHAR” property=”password” />
……
<!– 关联userdetail表 –>
<association property=”userDetail” column=”user_detail” resultMap=”UserDetailMap” />
</resultMap>

保存,然后测试,嗯~妥妥的报错了。看了一下提示,大概就是UserDetailMap不存在的意思。那就奇怪了,明明在UserDetailMapper.xml中定义了的,为什么呢?然后就查看两个mapper,看到<mapper namespace ……>,难道由于namespace不同原因,所以果断将UserDetailMapper.xml的namespace换成和UserMapper的相同的。再测试,这次成功了,真的是namespace的原因。

那这样说,是不是应该同一个模块的mapper的namespace都设成一样呢?我测试了一下,如果改成其他名称如:User,启动的时候会报错(由于mapper是用mybatis的相关功能自动生成的,所以当时只引入了xml,而没将mapper 接口类引入,不然改了UserDetailMapper的namespace时,应该启动就报错)。原因是这个namespace有一个意义是指定相关POJO接口的路径,详情可查看以下链接。

Mybatis的namespace问题说明

那该怎么办,上网找,好像都没有很明确关于如何引用其他mapper文件中的resultMap的文章。那只能再回去看错误日志:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:

Error querying database. Cause: java.lang.IllegalArgumentException: Result Maps collection does not contain value for com.A.kainaodong.xsstuser.impl.dao.UserDao.UserDetailMap

The error may exist in com/A/xsstuser/impl/dao/UserMapper.xml

细心看,其实可以发现,错误中com.A.kainaodong.xsstuser.impl.dao.UserDao.UserDetailMap的UserDetailMap前面还有一串东西,刚好也就是UserMapper的namespace,在UserMapper下当然是找不到UserDetailMap的了,那如果在UserDetailMap前加上UserDetailMapper的namespace的话可能可以,即:

<association property=”userDetail” column=”user_detail” resultMap=”com.A.xsstuser.impl.dao.UserDetailDao.UserDetailMap” />

然后保存,测试,通过。确实可以这样写。

如果不引用resultmap的话,可以应用其他文件的select,如下

<association property=”userDetail” column=”user_id” select=”com.kainaodong.xsstuser.impl.dao.UserDetailDao.selectUserDetailById” /> // 当然也要加上namespace咯。

出现这种问题,也可能由于我对mybatis的运行机制还不太了解。所以写一下这篇文章,整理一下思路。