最近处理的两个坑:Spring 启动问题与 log4j 配置

Spring 依赖注入问题

近半年,在生产环境老是出现应用在 JBoss 里启动不来,在下面的方法输出日志、进入循环后就走不出那个循环了:

Spring 3.0.5: org.springframework.beans.factory.support.DefaultListableBeanFactory

public void preInstantiateSingletons() throws BeansException {
     if (this.logger.isInfoEnabled()) {
          this.logger.info("Pre-instantiating singletons in " + this);
     }

     synchronized (this.beanDefinitionMap) {
          for (String beanName : this.beanDefinitionNames) {
               RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
               if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
                    if (isFactoryBean(beanName)) {
                         final FactoryBean factory = (FactoryBean) getBean(FACTORY_BEAN_PREFIX + beanName);
                         boolean isEagerInit;
                         if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
                              isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
                                   public Boolean run() {
                                        return ((SmartFactoryBean) factory).isEagerInit();
                                   }
                              }, getAccessControlContext());
                         }
                         else {
                              isEagerInit = factory instanceof SmartFactoryBean && ((SmartFactoryBean) factory).isEagerInit(); 
                         }
                         if (isEagerInit) {
                              getBean(beanName);
                         }
                    }
                    else {
                         getBean(beanName);
                    }
               }
          }
     }
}

因为是生产环境,没法直接远程 debug,只能 dump 出栈和堆来分析,从栈来看,Spring 一直在做 bean 实例化;从堆来看,是这样的:

Top elements include:

•13,701 × Error creating bean with name ‘sqlSessionFactory’ … > (496 bytes)
•13,732 × Could not autowire method: public final void org.m… (336 bytes)
…….//还有很多其他的类创建失败

这个问题有一定的随机性,因为不是总是起不来,重启多次之后就可能顺利启动了,搞得每次发版本都胆战心惊。

继续阅读

Spring Hessian 集成

一、介绍

Hessian 是工作在 HTTP 协议上的远程调用框架:请求信息被序列化为二进制数据通过 HTTP 请求传输到服务器端,服务端查找目标方法,用请求参数进行调用,然后把响应结果序列化为二进制数据,作为 HTTP 响应返回给客户端,客户端再解析数据组成应用所使用的 Java 对象。

二、服务接口

首先要定义一个 Java 接口来表示远程服务:

public interface IHessianService {
     User getUser();

     User createUser(String name, int age, char sex);

     Map<String, Object> getMap();

     String getString(String value);

     int getInt(int i);
}

定义多个方法是为了测试下不同的数据类型的支持度。User 是个自定义的类型,必须实现 java.io.Serializable

服务端暴露这个接口来表示可以被调用的服务,客户端使用这个接口来表示可以调用的远程服务。

继续阅读

Java 任务处理

最近梳理其他同事以前写的 job 后有点想法,记录下。

一、业务场景

在大多数的系统都有类似这样的逻辑,比如下单了给用户赠送积分,用户在论坛上发表了帖子,给用户增加积分等等。

下单赠送积分,那么一个订单肯定不能重复赠送积分,所以需要一些状态来比较来哪些是已赠送的,哪些是没有赠送的。或许可以在订单表里加个字段来标记是否赠送了积分。

有时候,业务人员出于营销的需要,可能要搞个某某时间段内下单返券的活动。难道又在订单表里加个字段?肯定不能,谁知道还要搞多少活动呢。

二、实现

为了使核心的业务流程尽可能简单高效,赠送积分、返券(后面简称为task)之类的逻辑应该通过异步的job来处理。

因为 task 的处理状态不能放在核心的业务表里,所以,可以另外建一个表示异步任务的 async_task 表,结构如下:

-- 业务job处理 任务
create table async_task (
  id number(11) primary key,
  key_work  varchar2(32),  --  不同业务逻辑的task用不同的keyword
  biz_id char(32),         --  业务数据 ID,比如订单号
  biz_data varchar2(256),  --  核心的业务数据,用于避免关联业务表;具体结构取决于keyword
  status number,           --  任务的处理状态; -2:未处理, -1:处理中, 0:已处理, 大于 0 的数字表示失败次数
  create_tm date,          --  任务的创建时间
  modify_tm date           --  任务的修改时间
);

处于性能考虑,可以在 key_work 字段上建立分区,在 biz_id 上建立索引。

当业务表有需要处理的数据时,就往 async_task 插入一条相应的记录(可以异步插入),异步 job 再从 async_task 表里取数据来处理。

注意:处理 task 时,要保证数据的一致性。所在的项目组曾出现过,下单返券的活动里,送券与更新状态的操作没有放在同一个事务里,出现券送了,状态没更新,重复送券的问题。一定要注意事务的正确处理。

继续阅读

踩坑之 Java 可变长参数列表

Java 可变长参数列表

这是 Java 5 引入的一个特性,如果一个方法要接收的参数数量是不确定的,那么这个特性就可以派上用场了。

比如,在涉及IO操作的地方,基本上至少需要关闭两个流:输入、输出,我喜欢把流关闭的操作封装成下面的方法,这样只需一次调用就可以关闭多个流。

public static void closeSilent(Closeable... closeables) {
     for (Closeable closeable : closeables) {
          if (closeable != null) {
               try {
                    closeable.close();
               } catch (IOException ignored) {
               }
          }
     }
}

这是我觉得这个特性唯一适合使用的地方,具备下面的特点:

  • 这些参数具有相同的类型;
  • 参数数量不确定,每一个都是可选的;
  • 这些参数的用途都是一样的,比如上面都是执行关闭。

Java 可变长参数列表只能放在方法参数列表的最后。

继续阅读

Java 反射 抽取类的方法信息

目前参与的项目是用 Spring MVC + MyBatis 实现的,项目部署就是一个war包。公司从外面请了个顾问,建议将公司网络分为A、B两个区,B区的安全级别高些,可以访问数据库,A区的安全级别低些,不能访问数据库,直接面向互联网,应用需要访问外部互联网服务时 或 外部用户请求应用时都必须在 A 区完成,A区通过定制的网关访问 B 区的应用。这个建议是强制执行,所以就需要拆分项目了。

考虑到开发的方便性,A区与B区之间就必须工作在类似Hessian之类的远程调用上,而不能直接在http层上,要不然装包拆包都累死人了。

项目目前的代码层次是 Rest 风格的 Controller + Service + MyBatis 的 Mapper。Controller 里大量使用servlet的API,所以不能把controller层抽取出来作为远程调用的接口。Mapper本身只是一个接口,service层与mapper层之间没法再拆,只能在controller与service之间拆。项目里没有专门为每个 service 组件定义一个相应的接口,需要根据已有的service组件抽取出对应的接口。

由于组件太多,只能写工具类抽取。

工具类的目标:

  1. 抽取所有组件的公开方法作为接口的方法,保留方法定义的类型信息和参数名等信息。
  2. 生成接口所依赖的导入并拷贝所有依赖的导入类。
  3. 生成对应的Hessian配置。

要保留方法的参数名信息需要 Java 8 的特性。Java 8 的 javac 增加了一个选项 -parameters,表示在生成的字节码文件里保留方法的参数名。

继续阅读

Session Fix 与 Jboss 4.2.2

Session Fix 会话固定

Session Fix,会话固定,是一个安全漏洞,以 servlet 容器为例。

  • 无状态的HTTP与会话:由于 HTTP 是无状态的服务,容器为了在同一个用户的不同请求之间保持状态,为每个状态维持一个会话,Servlet 容器里一般就是 HttpSession 对象。HTTP 客户端每次请求时,都需要把表示这个 HttpSession 对象的 ID (一般命名为 JSESSIONID)传递过来,JSESSIONID 的传递机制常用的是作为URL 的一部分或放在 Cookie,由于每个 HTTP 请求都会传输 Cookie,所以这是最常见的机制。在第一次请求时,由于没有 JSESSIONID,容器会创建一个 HttpSession 对象,把它的的ID 作为 JSESSIONID 的值设置到 Cookie,HTTP 客户端第二次请求时,把这个 JSESSIONID 的值也传递给了容器,容器就可以找到对应的 HttpSession 对象。

  • 会话固定
    登陆与注销前后,如果这个 JSESSIONID 不会改变,则表示存在会话固定的漏洞。
    登陆前,用户第一次请求容器,容器生成一个 HttpSession 对象,设置 JSESSIONID,但这个JSESSIONID 是未验证的,如果这个 JSESSIONID 被恶意用户获取到,那么用户登录后,这个 JSESSIONID 变为有效的后,恶意用户就可以把用 JSESSIONID 来伪冒合法用户。
    注销后,也需要把合法的 JSESSIONID 作废,以免被恶意用户获取后假冒。

  • 修复方法
    登陆成功后作废登陆前的会话;注销成功后作为之前的合法会话。
    注销后需要调用 HttpSession.invalidate() 方法来作废合法的 HttpSession 对象,要不然还会存在内存泄漏,因为未注销的 HttpSession 对象只有在超时后才会被回收。

Jboss 4.2.2 默认配置导致会话固定

Jboss 4.2.2 的配置 .../deploy/jboss-web.deployer/server.xml 里的默认配置 emptySessionPath="true",这会导致 HttpSession.invalidate() 方法无效,需要修改为 false


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

Java 对象内存布局

本文来自:http://www.ibm.com/developerworks/cn/java/j-codetoheap/

寻址能力与用户空间

进程能够处理的位数取决于处理器能寻址的内存范围,处理器的寻址能力取决于处理器的位数,比如 32 位能寻址 2^32,也就是 4G。

处理器提供的部分可寻址范围由 OS 本身用,供操作系统内核以及 C 运行时。OS 和 C 运行时占用的内存数量取决于所用的 OS,比如 Windows 默认占用 2GB。剩余的可寻址空间是供运行的实际进程使用的内存(用户空间)。

对于 Java 应用程序,用户空间是 Java 进程占用的内存,实际上包含两个池:Java 堆和本机(非 Java)堆。Java 堆的大小由 JVM 的 Java 堆设置控制:-Xms 和 -Xmx 分别设置最小和最大 Java 堆。在按照最大的大小设置分配了 Java 堆之后,剩下的用户空间就是本机堆。
32bit JVM

可寻址范围总共有 4GB,OS 和 C 运行时大约占用了其中的 1GB,Java 堆占用了将近 2GB,本机堆占用了其他部分。请注意,JVM 本身也要占用内存,就像 OS 内核和 C 运行时一样,而 JVM 占用的内存是本机堆的子集。

继续阅读

Spring MVC 与 web开发

项目组用了 Spring MVC 进行开发,觉得对里面的使用方式不是很满意,就想,如果是我来搭建开发环境,我会怎么做?下面就是我的想法,只关注于 MVC 的 View 层。

一、统一的响应格式

现在基本上都是用 ajax 来调用后台接口,拿到 json格式的数据再展示,有的人直接返回数据,却没有考虑异常的情况,我觉得返回的报文里必须包含表示可能的异常信息的数据和业务响应数据。我定义了下面这个类来表示报文格式:

/**
 * 统一的 HTTP 响应格式。<br/>
 * code 为 "ok" 表示业务调用成功,否则是失败的错误码,如果有多个则以逗号分隔。<br/>
 * data 是业务数据,如果失败了则是 null。
 * 
 * @author http://coderbee.net
 *
 */
public class RespBody {
    public static final String OK_CODE = "ok";
    private final String code;
    private final Object data;

    private static final RespBody OK = new RespBody(OK_CODE, null);

    private RespBody(String code, Object data) {
        this.code = code;
        this.data = data;
    }

    public static RespBody ok() {
        return OK;
    }

    public static RespBody ok(Object data) {
        return new RespBody("ok", data);
    }

    public static RespBody error(String code) {
        return new RespBody(code, null);
    }

    public static RespBody error(String code, Object msg) {
        return new RespBody(code, msg);
    }

    public String getCode() {
        return code;
    }

    public Object getData() {
        return data;
    }
}

这个类提供了一些静态方法来快速构建响应报文,这也是很重要的一个设计:用静态工厂方法而不是构造函数。

这里的 code 不应该是直接的错误提示信息,应该只是简单的错误编码,这样不同的客户端都可以调用这个 API,然后再根据错误编码、客户端语言和自己的客户端特性选择合适的错误提示信息和提示方式。

继续阅读

Java 性能调优指南 – 高性能Java

本文主要基于 Java performance tuning tips or everything you want to know about Java performance in 15 minutes的翻译。

这篇指南主要调优 java 代码而不是 JVM 设置。

一、JDK 类

Java 1.7.0_06 String 内部表示的改变

  • 从 Java 1.7.0_06 开始,String.substring 总是为它创建的新字符串创建一个新的底层 char[] 值。这意味着这个方法现在有线性的复杂度,之前是常量的复杂度。这个改变的好处是字符串需要更少的内存 footprint(比以前少8字节),也是避免 String.substring 导致的内存泄露的一个保证。

Java 里二进制序列化的不同方法

  • 写单个字节到直接字节缓存是非常慢的。对写记录 — 大多数情况是单个字节的字段,你应该避免使用直接字节缓存。
  • 如果你有原始数组字段,总是使用bulk方法来处理它们(一次处理一批的)。ByteBuffer 的bulk 方法的性能接近于Unsafe的这些方法。如果你需要存储/加载任何其它原始数组 – 除字节型的,用 ByteBuffer.to[YouType]Buffer.put(array) 方法调用,字节缓存的位置会自动更新。不要在循环里调用 ByteBuffer.put[YouType] 方法。
  • 总是尝试用带本地字节顺序的直接缓存序列化原始数组。直接字节缓存的性能接近于Unsafe且是可移植的。

Java 集合概览

单线程 并发
Lists ArrayList : 一般基于数组
LinkedList :不要使用
Vector:废弃
CopyOnWriteArrayList:很少更新,经常遍历。
Queues/deques ArrayDeque:一般基于数组。
Stack:废弃。
PriorityQueue:有序的检索操作。
ArrayBlockQueue:有界阻塞queue。
ConcurrentLinkedDeque/ConcurrentLinkedQueue:无界链接queue(CAS)。
DelayQueue:在每个元素上带延迟的queue。
LinkedBlockingDeque/LinkedBlockingQueue:可选的有界链接queue(锁)。
LinkedTransferQueue:may transfer elements w/o storing。
PriorityBlockingQueue:并发的PriorityQueue。
SynchronousQueue:实现了Queue接口的Exchanger。
Maps HashMap:一般的map。
EnumMap:enum作为key的。Hashtable:废弃。
IdentityHashMap:用 == 比较键。
LinkedHashMap:保留了插入顺序。
TreeMap:有序的键。
WeakHashMap:可用于缓存。
ConcurrentHashMap:一般的并发map。
ConcurrentSkipListMap:有序的并发map。
Sets HashSet:一般的set。
EnumSet:enum的集合。
BitSet:比特位或稀疏整数的集合。
LinkedHashSet:保留了插入顺序。
TreeSet:有序集合。
ConcurrentSkipListSet:有序并发集合。
CopyOnWriteArraySet:很少更新,经常遍历。

继续阅读

sftp auth fail

今天要做一个功能:用sftp协议传文件到远程服务器,服务器是别人配置的,给出账号密码和端口2888。用的库是jsch,版本0.1.50。

根据jsch的示例,很快就改出一个实现:

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.UserInfo;

public class Sftp {
       private static Session getSession(String user, String passwd, String host,
                   int port) throws JSchException {
            UserInfo ui = new MyUserInfo(passwd);
            JSch jsch = new JSch();
            Session session = jsch.getSession(user, host, port);
            session.setUserInfo(ui);
            session.connect();

             return session;
      }

       private static ChannelSftp getFtpChannel(Session session)
                   throws JSchException {
            Channel channel = session.openChannel( "sftp" );
            channel.connect();
            ChannelSftp c = (ChannelSftp) channel;

             return c;
      }

       public static boolean putFile(String user, String passwd, String host,
                   int port, String localeFile, String remoteFile) {
            Session session = null ;
            ChannelSftp channel = null ;
             try {
                  session = getSession(user, passwd, host, port);
                  channel = getFtpChannel(session);
                  channel.put(localeFile, remoteFile, ChannelSftp. OVERWRITE);
                  channel.quit();
            } catch (JSchException e) {
                  e.printStackTrace();
                   return false ;
            } catch (SftpException e) {
                  e.printStackTrace();
                   return false ;
            } finally {
                   clear(session, channel);
            }
             return true ;
      }

       private static void clear(Session session, Channel channel) {
             if (channel != null) {
                  channel.disconnect();
            }
             if (session != null) {
                  session.disconnect();
            }
      }

       public static void main(String[] arg) {
             boolean b = Sftp.putFile( "user" , "passwd" , "host" , 2888, "localeFile" ,
                         "remoteFile" );
            System. out .println(b);
      }

       public static class MyUserInfo implements UserInfo {
             final String passwd ;

             public MyUserInfo(String passwd) {
                   this .passwd = passwd;
            }

             public String getPassword() {
                   return passwd ;
            }

             public boolean promptYesNo(String str) {
                   // step 1, confirm accept host
                   return true ; // 接受任意服务器的指纹
            }

             public String getPassphrase() {
                   return null ;
            }

             public boolean promptPassphrase(String message) {
                   return true ;
            }

             public boolean promptPassword(String message) {
                   // step 2
                   return true ;
            }

             public void showMessage(String message) {
            }
      }
}

拿自己的VPS linux测试下,上传是OK的。但是上传到测试服务器时却老是出错,在建立会话时总是抛出异常:com.jcraft.jsch.JSchException : Auth fail

一开始怀疑是账号有问题,但在Xftp下是可以登录的。在shell的sftp命令下显示指定oPortoPasswordAuthentication选项也可以登录,所以就不像是账号问题了。

无奈继续google,发现很多网上代码都指定StrictHostKeyChecking选项为no,就把代码改为下面这样:

     private static Session getSession(String user, String passwd, String host,
            int port) throws JSchException {
        JSch jsch = new JSch();
        Session session = jsch.getSession(user, host, port);
        session.setConfig( "StrictHostKeyChecking" , "no" ); // 不验证host-key,验证会失败。
        session.setPassword(passwd);
        session.connect();

        return session;
    }

还真行了。

这个问题其实是因为jsch进行严格的 SSH 公钥检查导致的,禁用 SSH 远程主机的公钥检查可以方便进行自动化任务执行。如果是在shell命令行下进行的自动化任务,建议采用客户端公钥认证,也就是ssh自动登录的方式。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。