标签归档:JDK

Linux JRE中文字体支持

默认情况下,无论Oracle JDK还是OpenJDK,Linux JRE都不提供中文字体支持,这会给那些AWT应用开发者或者基于AWT实现的图表库(jfreechart等)使用者带来一些困扰,本来应该显示中文的地方都被方框代替:

fonts

解决方法可以很简单:

1. 拷贝中文字体到JRE目录。以宋体为例,从一台含中文字体的机器上(Mac下字体在/library/fonts目录,Windows下字体在C:\Windows\Fonts目录)拷贝SimSun.ttf文件到目标机器的$JAVA_HOME/jre/lib/fonts。
2. 重启应用(JVM)。

下面这段程序可以查看当前JRE环境支持哪些字体:

import java.awt.Font;
import java.awt.GraphicsEnvironment;

public class FontTest {

    public static void main(String[] args) {
        Font[] fonts = GraphicsEnvironment
                        .getLocalGraphicsEnvironment().getAllFonts();
        for (Font f : fonts) {
            System.out.println("Name:" + f.getFontName());
        }
    }
}

--EOF--

HTTPS和SNI

问题:有两个站点架在同一台服务器上,并且强制HTTPS访问,地址分别为https://sub.domain.com和https://admin.sub.domain.com,HTTPS证书签名的域名分别为*.domain.com和*.sub.domain.com,使用HttpClient(4.1.1版本)去调用一个接口:

1
2
3
4
5
6
7
public class TestGet {
  public static void main(String args[]) throws Exception {
    HttpGet httpGet = new HttpGet("https://sub.domain.com/api/nodes");
    HttpClient httpClient = new DefaultHttpClient();
    httpClient.execute(httpGet);
  }
}

返回 javax.net.ssl.SSLException异常:

Exception in thread "main" javax.net.ssl.SSLException: hostname in certificate didn't match:  != <*.sub.domain.com> OR <*.sub.domain.com>
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:231)
    at org.apache.http.conn.ssl.BrowserCompatHostnameVerifier.verify(BrowserCompatHostnameVerifier.java:54)
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:152)
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:133)
    at org.apache.http.conn.ssl.SSLSocketFactory.verifyHostname(SSLSocketFactory.java:559)
    at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:534)
    ……

问题分析:从异常信息来看,显然是SSL连接握手阶段出现证书不匹配的问题,客户端请求的是sub.domain.com域名,服务器返回了*.sub.domain.com的证书。但是浏览器访问又正常,这是为何?

解决方法:故事要从IPv4的设计开始说起,设计者认为32位够用了,后续的上层协议设计者也纷纷假设一个IP提供一个服务的前提,比如HTTP/1.0、SSL等,一个大坑就这样被埋下。从90年代中期开始,互联网发展太快,人们开始意识到IP资源越来越稀缺,于是着手设计IPv6,但是从IPv4到IPv6需要一个很长的过渡期,远远大于IPv4地址耗尽的时间。于是一些IP复用的方案出现了,CIDR和NAT技术能在一定程度上缓和IPv4资源稀缺的问题,互联网协议也适时进行了升级,比如HTTP,在HTTP/1.1版本中加入Host头,这个头部在虚拟主机(Virtual Host)中非常重要,它支持不同站点架在一个IP上,客户端通过Host头告知服务器需要访问的站点。但是Host头无法解决HTTPS场景中客户端与虚拟主机(多站点共享同一个IP)之间加密连接的建立,因为HTTPS依赖的SSL/TLS协议并没有同步跟进,SSL握手阶段客户端向服务器端发送的信息中未包含Host,所以服务器端也就没法返回正确的HTTPS证书了。这样的背景下,解决HTTPS虚拟主机的方法主要有:

1. 绑定不同的端口。为不同的虚拟主机绑定不同的HTTPS端口(默认端口443)。缺点显而易见,每次访问都需要显式指定端口号。
2. 绑定不同的IP。前面说过,IPv4资源越来越稀缺,而且这种方法也违背了虚拟主机的初衷。
3. 购买泛域名SSL证书(Wildcard Certificate)。
4. 购买多域名SSL证书(Multi Domain Certificate)。

这些方法都只是绕过限制,要根本解决问题还是得修改SSL/TLS协议,所以SNI(Server Name Indication)就适时被提了出来,它扩展了TLS协议(SSL 3.0不支持,在TLS 1.0以后支持,RFC4366RFC6606),在客户端请求的CLIENTHELLO阶段加入Host信息,告知服务器端要与哪个主机建立加密连接。
SNI

图片来源

实际上,SNI需要通信双方(服务端、客户端)都支持,根据Wiki上的总结,目前一些常见浏览器、服务器软件、类库的支持情况如下:

浏览器:

IE 7+
Mozilla Firefox 2.0+
Opera 8.0+
Google Chrome 5.0.342.1+
Safari 3.0+

注:这里的支持度还跟操作系统相关,比如Windows XP上所有IE均不支持,Chrome在不同操作系统下开始支持的版本也不同。

服务器软件:

Apache 2.2.12+
Nginx(依赖支持sni的openssl库)
Apache Traffic Server 3.2.0+
HAProxy 1.5+

类库:

OpenSSL: 0.9.8f(compiled in with config option '--enable-tlsext'), 0.9.8j+
libcurl / cURL 7.18.1+
Python 3.2
Qt 4.8
Oracle Java 7 JSSE
Apache HttpComponents 4.3.2
wget 1.14
Android 4.2 (Jellybean MR1)
Go (client and server)

Java是在JDK1.7里才支持SNI的,因此要在Java应用里使用,前提就是将OpenJDK或者Oracle JDK升到1.7,如果同时使用HttpClient进行HTTP接口调用,那么还必须将HttpClient版本升到4.3.2及以后,这个JIRA单描述了如何支持SNI(代码设计层面)的前因后果。

回到开头的异常问题,可以通过以下步骤修复:

1. 升级JDK至少到1.7。
2. 升级HttpClient至少到4.3.2。
3. 更新少量代码如下:

1
2
3
4
5
6
7
8
public class TestGet {
  public static void main(String args[]) throws Exception {
    HttpGet httpGet = new HttpGet("https://sub.domain.com/api/nodes");
    //HttpClient httpClient = new DefaultHttpClient();
    CloseableHttpClient httpClient = HttpClients.createDefault();
    httpClient.execute(httpGet);
  }
}

--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--

利用JDK实现观察者模式

JDK自1.0开始就支持观察者模式,java.util包提供了观察者模式结构中的Subject(被观察者,目标)和Observer(观察者)的定义和实现,分别对应java.util.Observable类和java.util.Observer接口。

Observable类已实现的方法包括注册观察者addObserver、移除观察者deleteObserver、设置状态改变标记setChanged,以及通知观察者类刷新数据的notifyObservers方法。Observer接口声明了update方法,当接到通知时用于刷新数据。

以下是利用Observable类和Observer接口实现的观察者模式:
1. 观察者ConcreteObserver类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.edu.zju.dp.observer;
 
import java.util.Observable;
import java.util.Observer;
 
/**
 * @author Feng
 * @version $Id: ConcreteObserver.java, v 1.0 2013-2-28 下午07:54:56
 */
public class ConcreteObserver implements Observer {
 
    @Override
    public void update(Observable o, Object arg) {
        System.out.println("Observerable: " + ((ConcreteObservable) o).getName() 
                + ", Params: " + arg);
    }
}

2. 被观察者(目标)ConcreteObservable类

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
26
27
package cn.edu.zju.dp.observer;
 
import java.util.Observable;
 
/**
 * @author Feng
 * @version $Id: ConcreteObservable.java, v 1.0 2013-2-28 下午07:58:00
 */
public class ConcreteObservable extends Observable {
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName1(String name) {
        this.name = name;
        setChanged(); //设置状态变更标记
        notifyObservers(); //通知所有观察者。
    }
 
    public void setName2(String name) {
        this.name = name;
        setChanged(); //设置状态变更标记
        notifyObservers(name);//通知所有观察者,同时向所有观察者传递自定义消息。
    }
}

3. 测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.edu.zju.dp.observer;
 
/**
 * @author Feng
 * @version $Id: ObserverTest.java, v 1.0 2013-2-28 下午08:01:29
 */
public class ObserverTest {
 
    public static void main(String[] args) {
        ConcreteObserver observer = new ConcreteObserver();
        ConcreteObservable observable = new ConcreteObservable();
        //注册观察者,当observable状态更新时,observer会收到通知。
        observable.addObserver(observer);
 
        observable.setName1("foo");//输出:Observerable: foo, Params: null
        observable.setName2("bar");//输出:Observerable: bar, Params: bar
    }
}

运行结果如下:

1
2
Observerable: foo, Params: null
Observerable: bar, Params: bar

被观察者ConcreteObservable类数据更新(setName)后,调用setChanged设置内部标记,表示状态更新(此时调用基类的hasChanged方法返回true),同时调用notifyObservers方法通知注册过的观察者。观察者类收到通知后,调用update方法刷新数据。update方法有类型分别为Observerable和Object的两个参数,前者是发出通知的被观察者(目标)类的引用,后者方便被观察者类向观察者发送通知时指定参数。可以理解为:Observerable参数支持观察者类从被观察者类“拉”数据,Object参数支持被观察者类向观察者类“推”数据。

JDK为什么把Observable设计成类,Observer设计成接口呢?知乎上有一个问答(『Java 的jdk中 为什么Observer 是接口,Observable 是类?』)靠谱:为所有被观察者对象设置内部状态变更标记,注册、移除、通知观察者的方法是相同的,将它们封装成类可以极大简化编写观察者模式程序,用户只需在Observable的子类中根据业务逻辑需要合理调用上述方法即可。而对于观察者而言,update方法中实现的是具体业务逻辑,例如可能刷新图表、更新表单数据、启动定时任务等,因此必须作为一个接口提供,由用户实现。

--EOF--