分类目录归档:J2EE

通过Spring获取properties文件属性值

Spring提供了注解@Value,用于在程序中获取properties配置文件属性值。例如:

1. applicationContext.xml中指定配置文件。

<context:property-placeholder location="classpath:xxx.properties" 
    ignore-unresolvable="true" />

当有多个配置文件时,上述配置可以配置多条。

<context:property-placeholder location="classpath:xxx.properties" 
    ignore-unresolvable="true" />
<context:property-placeholder location="classpath:yyy.properties" 
    ignore-unresolvable="true" />

2. Spring bean中使用@Value注解获取指定参数。

// xxx.properties配置项:
// server.ip=192.168.1.1
// server.port=8080

@Value("${server.ip}")
private String ip;

@Value("${server.port}")
private int port;

使用@Value注解的前提是当前对象的生命周期由Spring管理,是Spring bean,无论通过XML配置文件还是@Component、@Service等注解声明。假如一个对象的生命周期是我们程序自己管理的,比如常规用法下的new Object(),特别是做一些框架开发,经常用到Class.forName().newInstance()来实例化对象,那么想要反射为新创建对象的成员变量赋值时,如何借助Spring来获取已经解析好的properties属性值是个值得一试的探索。上述场景可以简化为:

如何在一个拥有Spring上下文的平台上,对不受Spring管理的对象使用依赖注入,达到类似@Value注解实现的功能。

思路也很简单,既然Spring已经解析过properties文件,那么通过某种手段把这些值暴露出来就可以了,EmbeddedValueResolverAware接口很适合做这件事情。Aware接口是定义一些能在Spring bean中操作Spring上下文信息的一类接口,常见的有ApplicationContextAware,可以在Spring bean中拿到ApplicationContext;BeanFactoryAware,可以在Spring bean中拿到Spring BeanFactory。这里的EmbeddedValueResolverAware也是类似功能,它定义了一个void setEmbeddedValueResolver(StringValueResolver resolver)接口方法,在bean初始化后,Spring回调setEmbeddedValueResolver()方法,将StringValueResolver对象注入到bean中,从这个对象中就能获取properties文件中的属性名称和值。用法如下:

1. 声明一个实现EmbeddedValueResolverAware接口的实例,用@Component注解声明为Spring bean,重写setEmbeddedValueResolver()方法,将StringValueResolver实例的引用保存下来,并且对外提供getPropertiesValue()方法,用于获取properties值。

@Component
public class PropertiesUtils implements EmbeddedValueResolverAware {
    
    privat StringValueResolver stringValueResolver;

    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        stringValueResolver = resolver;
    }

    public String getPropertiesValue(String name){
        return stringValueResolver.resolveStringValue(name);
    }
}

2. 通过${key}作为name格式调用getPropertiesValue()方法,获取properties值。

String name = "${server.ip}";
String value = propertiesUtils.getPropertiesValue(name);

StringValueResolve解析出来的值都是String类型的,非String类型需要在拿到参数String值后自行转换。

本文只对特定场景下使用EmbeddedValueResolverAware接口借助Spring上下文从properties文件中获取参数值做了一个简单介绍,此方法有效但不唯一,供参考。

--EOF--

一次Java内存泄露问题排查

近期随着管理平台用户增加,后台多次出现OutOfMemoryError错误,尝试着把堆内存从2G加到4G(-Xms4096m -Xmx4096m)也不顶用,隔段时间就OOM,看来是出现内存泄露了。于是在Tomcat启动脚本中加入JVM参数-XX:+HeapDumpOnOutOfMemoryError,使其OOM时把堆内存dump成文件,便于分析。今天,程序又一次OOM,我拿到了这个4G大的java_pid32693.hprof文件。打开 Eclipse Memory Analyzer(MAT),导入dump文件,内存泄露问题显而易见:
 Eclipse Memory Analyzer

根据提示,有一个ConcurrentHashMap数据结构占用了所有4G内存中的99.11%,该Map名为asyncContentMap,后面是它的package路径。

回忆了一下,这个貌似是很早以前实现过的一个实例状态自动刷新功能,管理平台有一个实例列表页面,当实例状态变化时,无需用户刷新页面,状态栏自动变更。为了前端通用,放弃Websocket、socket.io,用了最简单的ajax实现,浏览器端和管理平台间用long polling实现状态推送。下图就是整个状态自动刷新功能的示意图,管理平台接到请求后,使用Servlet 3.0的异步Servlet挂住浏览器端连接,并将请求上下文(AsyncContext)放入队列。另一方面,通过worker线程池处理请求队列里的请求,worker线程做的事情就是依次轮询后端服务(Service1,Service2...Service N)接口,判断其实例状态有无变化,如果状态变化,则取出前端请求上下文,返回响应。用户就能从浏览器得知实例状态变更,之后,再发起一个HTTP long polling请求,重复上述流程。当然,long polling有个超时时间,由管理平台控制,当对后端服务连续polling了一定时间都没有状态变化时,也要将响应返回给浏览器,使之进入下一个long polling周期。
Long Polling

要实现上述描述的功能,管理平台这边需维护两个状态,分别对应两个线程安全的数据结构。一个是请求队列LinkedBlockingQueue asyncContextList,worker线程池会从请求队列中取出浏览器请求;另一个是映射表ConcurrentHashMap asyncContentMap,用于存放异步请求和改请求附带的一些元信息,比如上一次polling后端Service的时间戳、响应值等等。根据MAT的提示asyncContentMap出现内存泄露,那么可以肯定是有些异步请求已经完成响应,但是忘了从映射表里把记录清除。检查了一下程序,的确如此,加入remove操作后问题解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AsyncContext asyncContext = Container.asyncContextList.take();
AsyncContentDeliver asyncDeliver = Container.asyncContentMap.get(asyncContext);
 
//略去其余部分
 
if (respEntry.getResponseBody().equals(lastResponseBody)) {
  // 与上一次polling相比,实例状态未发生改变,异步请求重新入队。
  Container.asyncContextList.put(asyncContext);
  continue;
} else {// 实例状态发生了改变,进行响应。
  try {
    asyncContext.getResponse().setCharacterEncoding("UTF-8");
    asyncContext.getResponse().getWriter().print(respEntry.getResponseBody());
    asyncContext.complete();
    Container.asyncContentMap.remove(asyncContext); // BugFix!
  } catch (IOException e) {
    e.printStackTrace();
    Container.asyncContextList.put(asyncContext);
  }
}

总结这次Bug修复,深深感受到一点:编程语言始终解决不了业务逻辑层面上的内存泄露问题,资源的分配和释放程序员必须谨记在心。

--EOF--

FreeMarker Configuration的模板路径配置问题

FreeMarker的Configuration类可以指定模板文件的加载路径。比如最简单的:

1
2
Configuration cfg = new Configuration();
cfg.setDirectoryForTemplateLoading(new File(tmplDir));

tmplDir可以是通过Thread.currentThread().getContextClassLoader().getResource("")等方式拿到的目录,那么FreeMarker会从这个目录下寻找模板文件。大多情况下,这没什么问题。

但是,假如一个Web项目被打成了war包,或者模板文件在一个可执行jar包里,classpath不是以一个目录的形式存在,调用setDirectoryForTemplateLoading()方法时File对象new出来抛异常。这时候要用setClassForTemplateLoading()方法:

1
void setClassForTemplateLoading(Class cl, String prefix);

这个方法最底层其实就是封装了Class cl.getResource(prefix)方法,因此,prefix的格式就有些讲究,如果prefix以/开头,表示其为相对于classpath根目录为基准的绝对路径,这时候Class参数可以忽略了,任意填;如果不以/开头,表示所填路径是相对于Class文件的相对路径,需小心填写,一不小心就会得到一个FileNotFoundException:Template xxx not found.

跟了一下Configration.getTemplate()方法,它调用的是TemplateCache.getTemplate()方法:

1
2
3
4
5
6
7
8
public Template getTemplate(String name, Locale locale, String encoding,
        boolean parse) throws IOException {
    Template result = cache.getTemplate(name, locale, encoding, parse);
    if (result == null) {
        throw new FileNotFoundException("Template " + name + " not found.");
    }
    return result;
}

cache.getTemplate()中做的事情就是根据参数,判断缓存中是否已存在模板文件,如果已存在,则直接返回,否则调用findTemplateSource()方法查找模板文件:

1
newlyFoundSource = findTemplateSource(name, locale);

findTemplateSource()方法紧接着调用acquireTemplateSource()方法,再根据不同的TemplateLoader调用相应的findTemplateSource()方法,最终,ClassTemplateLoader的getURL()方法完成模板文件资源的定位:

1
2
3
4
protected URL getURL(String name)
{
    return loaderClass.getResource(path + name);
}

关于Class的getResource()方法doc上有如下说明:
The rules for searching resources associated with a given class are implemented by the defining class loader of the class. This method delegates to this object's class loader.

意思是它虽然是由Class实例发起的调用,但是可以看做是加载它的classloader的委托,classloader怎么搜寻类资源的,它就是怎么定位资源文件的。

关于FreeMarker定位模板文件的更多详情,参见官网说明:『Template loading』

--EOF--

如何用Spring实现集群环境下的定时任务

定时任务的实现方式有多种,例如JDK自带的Timer+TimerTask方式,Spring 3.0以后的调度任务(Scheduled Task),Quartz等。

Timer+TimerTask是最基本的解决方案,但是比较远古了,这里不再讨论。Spring自带的Scheduled Task是一个轻量级的定时任务调度器,支持固定时间(支持cron表达式)和固定时间间隔调度任务,支持线程池管理。以上两种方式有一个共同的缺点,那就是应用服务器集群下会出现任务多次被调度执行的情况,因为集群的节点之间是不会共享任务信息的,每个节点上的任务都会按时执行。Quartz是一个功能完善的任务调度框架,特别牛叉的是它支持集群环境下的任务调度,当然代价也很大,需要将任务调度状态序列化到数据库。Quartz框架需要10多张表协同,配置繁多,令人望而却步...

经过折中考虑,还是选择了Spring的Scheduled Task来实现定时任务。如下:

1. Spring配置文件application-context.xml中添加task命名空间和描述。

1
2
3
4
5
6
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/task
	http://www.springframework.org/schema/task/spring-task.xsd">

2. 添加调度器和线程池声明。

1
2
<task:executor id="taskExecutor" pool-size="10" />
<task:annotation-driven executor="taskExecutor" />

3. 实现调度方法。基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.netease.yx.service;
 
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
 
@Service
public class ScheduledService {
    @Scheduled(cron = "0 0 5 * * *")
    public void build() {
       System.out.println("Scheduled Task");
    }
}

@Scheduled注解支持秒级的cron表达式,上述声明表示每天5点执行build任务。

前文已经提过,这种方式在单台应用服务器上运行没有问题,但是在集群环境下,会造成build任务在5点的时候运行多次,遗憾的是,Scheduled Task在框架层面没有相应的解决方案,只能靠程序员在应用级别进行控制。

如何控制?

1. 无非是一个任务互斥访问的问题,声明一把全局的“锁”作为互斥量,哪个应用服务器拿到这把“锁”,就有执行任务的权利,未拿到“锁”的应用服务器不进行任何任务相关的操作。
2.这把“锁”最好还能在下次任务执行时间点前失效。

在项目中我将这个互斥量放在了redis缓存里,1小时过期,这个过期时间是由任务调度的间隔时间决定的,只要小于两次任务执行时间差,大于集群间应用服务器的时间差即可。

完整定时任务类如下:

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
package com.netease.yx.service;
 
import javax.annotation.Resource;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.netease.yx.service.ICacheService;
 
@Service
public class ScheduledService {
    @Resource
    private ICacheService cache = null;
 
    private static String CACHE_LOCK = "cache_lock";
 
    private static int EXPIRE_PERIOD = (int)DateUtils.MILLIS_PER_HOUR / 1000;
 
    @Scheduled(cron = "0 0 5 * * *")
    public void build() {
        if (cache.get(CACHE_LOCK) == null) {
            cache.set(CACHE_LOCK, true, EXPIRE_PERIOD);
            doJob();
        }
    }
}

--EOF--

一种"无状态"的竞猜游戏设计

场景如下:某个电视节目,共6个选手,每期节目有一个或多个选手为获胜者。现要给这个节目开发一个与节目同步的竞猜游戏,观众参与竞猜谁是节目的获胜者,答对的有红包奖励。节目每周一期,周六21点播出,22:30结束。周四10点至周六22点为观众竞猜时间,周六22点至22:30为等待开奖时间,周六22:30至下个周四10点为领奖时间。然后下个节目周期开始,依此循环。

上述节目有三个时间点,开始竞猜时间(结束领奖时间,周四10点),停止竞猜时间(周六22点),开奖时间(周六22:30),这三个时间点前后会有较大的访问量,其余时间则较为平缓。一种设计方法是给每个用户保存一个状态,定时器在上述三个时间点修改用户的状态,这就是一种有状态的实现方式,优点是直接,用户访问页面后查一次数据库即可知道状态,显示对应的视图即可。缺点是三个时间点附近有大量的数据库读写,特别是开奖时间前后会涌入大量用户,应用服务器一边要等待数据库IO,处理缓存更新,一边要处理用户请求,会给响应时间带来一定影响。而且定时更新状态的任务在应用服务器集群下还要附加额外逻辑,避免不同服务器更新多次用户状态。

另外有一种无状态的设计方式,这里的无状态是指不用将用户的当前状态存储到数据库中。在用户访问应用之前,他的状态是未知的,当用户访问页面后,服务器根据一些参照值实时运算出该用户的当前状态。这种实现方式避免了第一种实现方式的两个缺点,首先三个时间点上不再有涉及全表的数据库操作,它将集中的状态更新操作平摊到每次用户访问页面时,其次应用支持简单的水平扩展。由于需要实时运算用户状态,这种实现方式会额外消耗CPU时间。从业务场景的特点来看,这种交换是值得的。

主要的实现方式比较简单:

1. 定义状态类型,定义以下8种状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 领奖期间的状态。周六22:30至下个周四10:00。
public static final int STATUS_PRIZE_NO_PARTI = 0; // 未参与
public static final int STATUS_PRIZE_PARTI_WIN_DRAW = 1; // 参与,压中,已领奖
public static final int STATUS_PRIZE_PARTI_WIN_NO_DRAW = 2; // 参与,压中,未领奖
public static final int STATUS_PRIZE_PARTI_NO_WIN = 3; // 参与,未压中
 
// 竞猜阶段的状态。周四10:00至周六22:00。
public static final int STATUS_GUESSING_CHOOSE = 4; // 已选择
public static final int STATUS_GUESSING_NO_CHOOSE = 5; // 未选择
 
// 节目播放阶段的状态。周六22:00至周六22:30。
public static final int STATUS_BROADCAST_CHOOSE = 6; // 已选择
public static final int STATUS_BROADCAST_NO_CHOOSE = 7; // 未选择

2. 用户访问页面后,以当期节目的播出时间为参照点,根据相对时间进行运算,确定3个时间点。确定当前时间所在的区间,再根据其他信息确定当前用户的状态,返回即可。

--EOF--