Maven 根据不同的 profile 对不同的构建环境进行配置

我在使用 maven 管理项目的时候, 非常头疼的问题之一就是可能存在不同的构建环境, 你不得不去为这些环境去做一些定制化的配置. 比如开发环境, QA环境, 生产环境中 log4j 生成的日志 level 不同, 数据库使用url, 用户名, 密码不同, 甚至可能在开发和生产环境中使用的是 MySql, 而单元测试和CI环境使用却是基于内存的 HSqlDB. 一直在考虑如果能通过添加一个配置参数来切换不同的构建方案就好了.

查看了一些资料, 看到 maven 可以通过在 pom.xml 中添加 profile 来切换配置. 下面来掩饰一下整个过程.

在 pom.xml 中添加如下代码:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  ...

  <!-- 不同 profile 对应的构建配置 -->
  <profiles>
    <profile>
      <id>dev</id>
      <properties>
        <env>dev</env>
      </properties>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
    </profile>
    <profile>
      <id>product</id>
      <properties>
        <env>product</env>
      </properties>
    </profile>
    <profile>
      <id>qa</id>
      <properties>
        <env>qa</env>
      </properties>
    </profile>
    <profile>
      <id>test</id>
      <properties>
        <env>test</env>
      </properties>
    </profile>
  </profiles>

  ...

</project> 

可以看到, 我们定义了4个 profile: dev, product, qa, test. 分别对应到四个不同的 env 值, 其中通过

<activation>
  <activeByDefault>true</activeByDefault>
</activation>

来定义了 dev 为默认的 profile

我们只要在执行 maven 命令时, 通过 -P 来指定不同的 profile id 即可, 如:

$ mvn install -Pproduct

定义了 env, 下面通过 filter 来和 properties 文件模板来生成对应 env 的 properties 文件

我们举个例子, 在 src/main/resources/ 中添加 log4j.properties:

db.default.driver=${db.default.driver}

db.master.url=${db.master.url}?useUnicode=true&characterEncoding=utf8
db.master.user=${db.master.user}
db.master.password=${db.master.password}

这里定义和数据库相关模板配置, 接下来, 我们来写 filter 来把生成和 env 相关的配置文件.

我们在 src/main/resources/filters/ 中添加四个文件: filter-dev.properties, filter-product.properties, filter-qa.properties, filter-test.propertes, 文件的内容针对上面的模板进行配置:

db.default.driver=com.mysql.jdbc.Driver
db.master.url=jdbc:mysql://vipshop.db.master:3306/vipshop_passport
db.master.user=root
db.master.password=root

接下来, 我们继续编辑 pom.xml, 来给 maven 指定模板 resources 和 filter文件:

<project>

  ...

  <build>    

    ...

    <filters>
      <filter>src/main/resources/filters/filter-${env}.properties</filter>
    </filters>
    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
      </resource>
    </resources>

    ...
    
  </build>

  ...

</project>

ok, 基本的配置完成了. 我们测试一下:

$ mvn process-sources -Pproduct

看看在 /target/classes/ 中有没有生成 db.properties, 并且已经根据指定的 env 配置好了参数?

Hibernate 配置写入数据库时自动设置系统时间

如果类的一个特定属性有着数据库生成的值, 通常在第一次插入实体行的时候. 典型的数据库生成的值是创建的时间戳, 还有其它默认值等.

使用 property 映射中的 generated 开关启用这个自动刷新:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<!-- Generated Jan 5, 2013 2:10:24 PM by Hibernate Tools 3.4.0.CR1 -->
<hibernate-mapping>
    <class name="com.vipshop.cmprice.vo.ItemInfo" table="item_info">
        <id name="id" type="int">
            <column name="id" />
            <generator class="increment" />
        </id>
        <property name="sku" type="java.lang.String">
            <column name="sku" />
        </property>
        <property name="platform" type="java.lang.String">
            <column name="platform" />
        </property>
        <property name="itemTitle" type="java.lang.String">
            <column name="item_title" />
        </property>
        <property name="itemPrice" type="java.lang.String">
            <column name="item_price" />
        </property>
        <property name="itemLink" type="java.lang.String">
            <column name="item_link" />
        </property>
        <property name="storeLink" type="java.lang.String">
            <column name="store_link" />
        </property>
        <property name="storeTitle" type="java.lang.String">
            <column name="store_title" />
        </property>
        <property name="itemImage" type="java.lang.String">
            <column name="item_image" />
        </property>
        <property name="createTime" type="java.util.Date" generated="insert" not-null="true">
            <column name="create_time" sql-type="timestamp" default="CURRENT_TIMESTAMP" />
        </property>
    </class>
</hibernate-mapping>

其中, 最下面createTime的配置:

<property name="createTime" type="java.util.Date" generated="insert" not-null="true">
    <column name="create_time" sql-type="timestamp" default="CURRENT_TIMESTAMP" />
</property>

关于 generated 的适用值说明:

  • never(默认): 标明此属性值不是从数据库中生成, 也就是根本不用刷新实体类了.
  • insert: 标明此属性值在insert的时候生成, 但是不会在随后的update时重新生成. 也就是只在insert情况下才会刷新实体类.
  • always: 标明此属性值在insert和update时都会被生成, 也就是在insert, update情况下都会刷新实体类.

sql-type 指生成的时间的类型

default Hibernate本身提供 current_date, current_timestampcurrent_time 三种函数.

解决使用BlackberryDesktopManager同步Outlook时崩溃问题

通过Blackberry Desktop Manager将blackberry和Outlook同步的时候, 100%会在同步时直接崩溃. 经过反复测试和网上资料的搜集, fix了这个问题.

下面有几种解决方案, 如果一个不管用, 请分别尝试.

1. 扫描Outlook的PST文件尝试解决问题: 关闭Outlook后运行outlook的pst扫描程序C:\Program Files\Microsoft Office\Office12\SCANPST.exe, 它将尝试去修复一些由于pst文件异常造成的错误. 一些情况下可以解决你的Blackberry Desktop Manager崩溃问题.

2. 卸载Blackberry Desktop Manager, 并以管理员方式安装: 先完全卸载Blackberry Desktop Manager, 然后对安装文件鼠标右击选”Run as administrator”. 重新安装以后, 问题解决.

3. 如果以上两种解决方案都失效的情况下, 可以尝试使用这种方案, 删除以前所有的同步信息, 以全新的方式重新同步: 关闭Blackberry Desktop Manager删除这个文件夹C:\Documents and Settings\user_name\Application Data\Research In Motion\BlackBerry, 然后打开Blackberry Desktop Manager并重新配置同步方案, 尝试同步. 我几次出现问题都是通过这种方案得到解决的.

proxool 0.8.3使用java搭建MySql连接池的配置和测试

在企业开发的时候, 很多情况都会使用数据库连接, 下文将介绍proxool连接池的使用和配置以及测试方案.

下载 proxool 0.8.3

创建一个xml配置文件db.xml, 放在CLASSPATH路径:

<xml version="1.0" encoding="UTF-8">
<something-else-entirely>
  <proxool>
    <!-- 数据源的别名 -->
    <alias>search</alias>
    <driver-url>jdbc:mysql://localhost:3306/search</driver-url>
    <driver-class>com.mysql.jdbc.Driver</driver-class>

    <!-- 连接池使用状况统计 -->
    <statistics>1m,15m,1d</statistics>
    <driver-properties>
      <property name="user" value="root" />
      <property name="password" value="root"/>
      <property name="characterEncoding" value="UTF-8"/>
      <property name="useUnicode" value="true"/>
    </driver-properties>

    <!--
    proxool自动侦察各个连接状态的时间间隔(毫秒)
    侦察到空闲的连接就马上回收,超时的销毁 默认30秒
    -->
    <house-keeping-sleep-time>90000</house-keeping-sleep-time>
    
    <!--
    连接池中可用的连接数量.
    如果当前的连接池中的连接少于这个数值. 新的连接将被建立(假设没有超过最大可用数).
    例如.我们有3个活动连接2个可用连接, 而我们的prototype-count是4, 那么数据库连接池将
    试图建立另外2个连接. 这和 minimum-connection-count 不同. minimum-connection-count
    把活动的连接也计算在内.
    prototype-count 是spare connections 的数量.
    -->
    <prototype-count>5</prototype-count>

    <!--
    最大连接数(默认5个),超过了这个连接数,再有请求时,就排在队列中等候, 最大的等待
    请求数由maximum-new-connections决定
    -->
    <maximum-connection-count>100</maximum-connection-count>
    <simultaneous-build-throttle>100</simultaneous-build-throttle>
    <!--最小连接数(默认2个) -->
    <minimum-connection-count>2</minimum-connection-count>
    
    <!--
    如果housekeeper 检测到某个线程的活动时间大于这个数值.它将会杀掉这线程
    所以确认一下你的服务器的带宽.然后定一个合适的值.默认是5分钟
    -->
    <maximum-active-time>300000</maximum-active-time>
    <house-keeping-test-sql>select CURRENT_DATE</house-keeping-test-sql>
  </proxool>
</something-else-entirely>

如果有多个数据库, 则要使用多个proxool标签, 配置不同的alias.

然后配置web.xml, 里见加入:

<servlet>
  <description>proxool配置servlet</description>
  <servlet-name>ServletConfigurator</servlet-name>
  <servlet-class>org.logicalcobwebs.proxool.configuration.ServletConfigurator</servlet-class>
  <init-param>
    <param-name>xmlFile</param-name>
    <!-- 这里是上面那个配置文件的路径 -->
    <param-value>WEB-INF/classes/db.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
<servlet>
  <description>proxool管理servlet</description>
  <servlet-name>proxool</servlet-name>
  <servlet-class>org.logicalcobwebs.proxool.admin.servlet.AdminServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>proxool</servlet-name>
  <url-pattern>/admin</url-pattern>
</servlet-mapping>

创建一个DBMgr.java, 作为连接数据库的模板类:

package com.pptv.config;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import org.apache.log4j.Logger;

/**
 * 数据库的基本连接类, 通过proxool连接池连接Mysql
 *
 * @author Dan Shan
 *
 */
public final class DBMgr {

    private static final Logger Logger = Logger.getLogger(DBMgr.class);
    
    static {
        try {
            Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
        } catch (ClassNotFoundException e) {
            logger.error("Load proxool failed", e);
        }
    }

    /**
     * 所有方法都为static, 不允许实例化.
     */
    private DBMgr() {
    }

    public static Connection getDBconn(final String dbName) throws SQLException {
        Connection conn = null;
        try {
            conn = DriverManager.getConnection("proxool." + dbName);
        } catch (Exception e) {
            logger.error("Read the database configuration files error", e);
        }
        return conn;
    }
}

至于怎么用, 都应该知道了, 调用getDBconn时, 传入最上面db.xml中设定的alias值, 即可连接不同的数据库.

再来说测试, 使用单元测试的时候, 并不希望每次都启动网站来测试, 希望直接就能调用数据库的配置. 其实也很简单:

package com.pptv.config;

import java.sql.Connection;
import java.sql.SQLException;
import junit.framework.TestCase;
import org.junit.Test;
import org.logicalcobwebs.proxool.ProxoolException;
import org.logicalcobwebs.proxool.ProxoolFacade;
import org.logicalcobwebs.proxool.configuration.JAXPConfigurator;

public class DBMgrTest extends TestCase {
    private Connection conn;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        String dbConfigFile = DBMgrTest.class.getResource("/db.xml").getPath();
        try {
            JAXPConfigurator.configure(dbConfigFile, false);
        } catch (ProxoolException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        if (conn != null) {
            conn.close();
        }
        ProxoolFacade.shutdown(0);
    }
    
    @Test
    public void testGetDBconn() throws SQLException {
        conn = DBMgr.getDBconn("search");
        assertNotNull(conn);
    }
}

只要手动加载配置文件, 见setUp()方法. 这里注意一下, 单元测试需要手动的关闭连接池, 否则测试程序停止后会抛异常, 见tearDown()方法的最后一行:

ProxoolFacade.shutdown(0);

连接池个别情况不能替代传统的jdbc连接, 比如需要建立长连接时, 这个时候, 可能就需要创建一个保持连接的Connection, 而不能使用proxool了, 因为会被自动Kill.

jsp解决session过期时写入数据库操作

背景如下:

我在做一个系统的时候希望实现当用户点击jsp页面上的注销按钮时实现在数据库中保存用户注销的时间. 另外如果用户没有正常退出, 则在session超时时自动记录超时时候的时间.

仿照找到的关于利用HttpSessionListener实现在线人数统计的方法来处理:

对每一个正在访问的用户, J2EE应用服务器会为其建立一个对应的HttpSession对象. 当一个浏览器第一次访问网站的时候, J2EE应用服务器会新建一个HttpSession对象, 并触发HttpSession创建事件, 如果注册了HttpSessionListener事件监听器, 则会调用HttpSessionListener事件监听器的sessionCreated方法.

相反,当这个浏览器访问结束超时的时候, J2EE应用服务器会销 毁相应的HttpSession对象, 触发HttpSession销毁事件, 同时调用所注册HttpSessionListener事件监听器的sessionDestroyed方法.

可见, 对应于一个用户访问的开始和结束, 相应的有sessionCreated方法和sessionDestroyed方法执行. 因此, 我们只需在HttpSessionListener实现类的sessionDestroyed方法中让其执行数据库的更新操作就可以了. 下面是示例代码:

package com.shanhh.session;

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import libms.service.UserServiceImpl;

public class UserOnlineListener implements HttpSessionListener {
    
    private static long userId = 0;
    
    public static void setUserId(long id) {
        userId = id;
    }

    public void sessionCreated(HttpSessionEvent event) {
    }

    public void sessionDestroyed(HttpSessionEvent event) {
        if (userId > 0) {
            // TODO 这里写更新数据库的操作
        }
    }
}

在web.xml文件中注册一个监听器:

<listener>
    <listener-class>com.online.OnlineCountListener</listener-class>
</listener>

在用户登录的时候, 把用户的id使用UserOnlineListener.setUserId(id)的方法保存下来. 当用户点击注销按钮的时候, 调用session.invalidate()的方法清空session, 就会触发监听器sessionDestroyed(HttpSessionEvent event)方法了, 同样, 如果用户非正常退出, 则在session超时的时候, 也会出发该方法.