sbt 0.7

前言:sbt是一个用scala写的构建工具,跟maven、gradle差不多,目前最新的版本是0.11。不过本文是讲sbt 0.7的使用方法,sbt继承了scala优良的不兼容传统,从0.7升级到0.10是完全不兼容的两个版本,虽然0.7已经基本没人用了,但还是有一些开源项目还没切换到0.11上。我这篇文章比较早就完成了,但因为各种原因现在才发出来。

sbt 0.7的项目地址是 http://code.google.com/p/simple-build-tool/,官方为了让大家尽快升级到0.10以上的版本,已经将这个地址里面大部分文档都删除了,目前要找到0.7的资料已经很难了,建议大家新项目还是直接用0.11。

先来看一下sbt(没有特殊说明的话,以下sbt都是指sbt 0.7)的配置结构,sbt使用maven的约定目录来放置项目文件,比如说src/main/java、src/main/resources等等,当然这是可以通过配置进行修改。而sbt本身的配置,是放在项目根目录下的project目录下,如果你见到一些scala的开源项目有个project目录不要觉得奇怪,那是sbt的配置。如果你的项目是多个子项目组成的,只需要在总的根目录下进行sbt配置即可,不需要像maven那样每个项目下配置pom.xml。

一般来说,sbt的配置有以下三个文件

/project/build.properties
/project/build/MyProject.scala (文件名随意)
/project/plugins/Plugins.scala (文件名随意)

第一个文件是项目的基本信息配置,这里一个范例

project.organization=org.sparkle
project.name=myproject
project.version=1.0-SNAPSHOT
sbt.version=0.7.7
build.scala.versions=2.9.0-1

前三项对应maven里面的groupId、artifactId、version
后面两项分别定义项目所用的sbt和scala版本

第二个文件是主要的配置文件,得益于scala强大的描述能力,项目配置直接用实际的scala代码来编写,具体的含义直接看代码中的注释

import sbt._
 
class MyProject(info: ProjectInfo) extends ParentProject(info) {
  // 通过at方法定义的变量自动成为额外的jar下载路径
  val mavenLocal = "Local Maven Repository" at "file://" + Path.userHome + "/.m2/repository"
  val central = "central" at "http://192.168.1.88:18081/nexus/content/groups/public/"
 
  // 定义项目路径名、使用的定义类(见下方)、依赖关系(这里web依赖core)
  lazy val myprojectCore = project("myproject-core", "myproject-core", new MyprojectCoreProject(_))
  lazy val myprojectWeb = project("myproject-web", "myproject-web", new MyprojectWebProject(_), myprojectCore)
 
  // 通过类继承来定义通用配置
  abstract class MyprojectBaseProject(info: ProjectInfo) extends DefaultProject(info) {
    // 编译选项
    override def javaCompileOptions = super.javaCompileOptions ++ javaCompileOptions("-encoding", "utf8")
 
    // 直接调整ivy配置
    override def ivyXML =
      <dependencies>
          <exclude module="commons-logging"/>
      </dependencies>
 
    // 直接定义一个sbt指令,如果用maven实现的话新建一个plugin项目啊
    val deployPath = "target" / "deploy"
    val deployClean = task {
      FileUtilities.clean(deployPath, log)
      None
    }
  }
 
  // 以下的代码都是定义项目所依赖的类库
  class MyprojectCoreProject(info: ProjectInfo) extends MyprojectBaseProject(info) {
    val springJdbc = "org.springframework" % "spring-jdbc" % "3.0.5.RELEASE"
    val springContextSupport = "org.springframework" % "spring-context-support" % "3.0.5.RELEASE"
 
    // runtime scope
    val mysqlConnectorJava = "mysql" % "mysql-connector-java" % "5.1.16" % "runtime"
 
    // test scope
    val junit = "junit" % "junit" % "4.8.2" % "test"
  }
 
  class MyprojectWebProject(info: ProjectInfo) extends MyprojectBaseProject(info) {
    val springWebmvc = "org.springframework" % "spring-webmvc" % "3.0.5.RELEASE"
 
    // provided scope
    val jetty6 = "org.mortbay.jetty" % "jetty" % "6.1.26" % "provided"
  }
}

是不是比maven的pom.xml简洁非常多,而且还可以直接inline使用scala代码实现各种额外的功能,maven就要开一个plugin项目才能实现

第三个文件是插件配置,sbt支持插件,可以理解为maven中的插件,大致的写法跟上一个文件很类似

import sbt._
 
class Plugins(info: ProjectInfo) extends PluginDefinition(info) {
  //val junitInterface = "com.novocode" % "junit-interface" % "0.7"
}
Posted in Java | Tagged , | 3 Comments

如何编写maven plugin(三) 测试

写完一个插件之后,我们就需要对他进行测试
如果我们用人工测试的话,将会非常麻烦,因为maven插件本身的发布流程就非常复杂
你需要编译打包你的maven插件,然后安装到本地库(或远程私库中),然后写一个sample project,再运行,看一下是否正确
当然你也可以用单元测试来解决一些问题,但是单元测试比较难保证插件最终正确,而且maven插件很经常是跟文件打交道

这里我们就需要对maven插件进行自动化的集成测试
maven的生命周期是包含集成测试的,默认是没有绑定任何功能。不过如果你google一下,会找到一些maven进行集成测试的例子,大致就是启动jetty,然后通过http访问验证,再关闭jetty

我们这里使用的是maven-invoker-plugin,它就是用于maven插件的集成测试

先在pom.xml中加入

<build>
	<plugins>
		<plugin>
			<artifactId>maven-invoker-plugin</artifactId>
			<configuration>
				<cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
			</configuration>
			<executions>
				<execution>
					<id>integration-test</id>
					<goals>
						<goal>install</goal>
						<goal>run</goal>
					</goals>
				</execution>
			</executions>
		</plugin>
	</plugins>
</build>

cloneProjectsTo是先将测试案例拷贝出来再运行
execution段的设定是把maven-invoker-plugin的两个goal绑定到integration-test上
integration-test这个生命周期会在mvn install之前调用

集成测试的内容放在 src/it 目录下,每新建一个目录代表一个独立的测试,里面放一个完整的maven项目,当然你在这个项目里面需要引入自己编写的maven插件并且运行
另外还需要一个postbuild.groovy文件,放在测试案例的根目录,这个脚本的用处是检查运行后的maven项目是否达到自己要的效果。很明显,看名字就知道用groovy来写,一般我们会检查一下,是否产生了某某文件等等来判定,如果不正确的话抛出异常

然后我们在maven插件目录运行mvn integration-test就能进行集成测试了

Posted in Java | 1 Comment

如何编写maven plugin(二) 注入

Mojo是一个很简单的Java Bean模式的类,你会发现Mojo所继承的AbstractMojo里面之后非常少的方法。那我们需要在Mojo.execute里面获取当前运行中的上下文如何处理呢?答案是注入,就是跟spring ioc差不多的注入方式。

常用的注入主要有两种,第一种是xml配置中的额外设置
比如说我们有这么一个plugin的配置

<plugin>
	<artifactId>maven-eclipse-plugin</artifactId>
	<configuration>
		<downloadSources>true</downloadSources>
	</configuration>
</plugin>

所有写在configuration里面的属性都可以注入到Mojo中,比如说以下代码

/**
 * @parameter
 */
private boolean downloadSources;

就可以通过downloadSources变量获得配置中的值
值得注意的是,这里是不用生成完整的JavaBean模式的get/set的,并且private是有效的

javadoc里面还可以加入其他属性,比如说

/**
 * @parameter default-value="true"
 * @readonly
 */
private boolean downloadSources;

就是默认为true,并且不能通过配置修改(当然我们这里肯定不会有这样的需求)
更多的javadoc可以参看官方文档中的说明

第二种注入的数据就是上下文,跟HttpServlet.getServletContext这种写法不一样,如果我们需要Mojo运行期的上下文,也是通过注入获得的

/**
 * @parameter expression="${project}"
 * @readonly
 */
private MavenProject project;
 
/**
 * @component
 * @readonly
 */
private ArtifactFactory artifactFactory;

例如这里我们就能获得ArtifactFactory和MavenProject
需要注意的是这里可能有两种方法,第一种跟xml配置获得的方法差不多,通过expression指定名字
实际上,你在xml里面,也可以通过${project}获得相应的东西进行一些简单的操作(当然xml里面只能文本描述,这里是一个类)

另外一种就是使用@component这个标注,可以获得一些基本的组件实例

Posted in Java | Leave a comment

如何编写maven plugin(一) 基础

当maven内置的功能不能满足需求的时候怎么办,那就只能给它写插件了。
(话说回来,给maven扩展只能写一个很完整的插件,而不能是一个简单的script,真的是太笨重了)

网络上很多maven的文章,但基本很少谈及如何给它写插件,即使你搜索maven plugin,也只是给你返回一堆如何使用maven插件的文章。希望这边文章能给一些maven使用者带来帮助。

我在这里先假设你已经懂得使用maven,我不会贴出完整的pom.xml文件

首先,你需要创建一个maven项目,插件是一种特殊的maven项目
然后修改pom.xml,将packaging改为maven-plugin

<packaging>maven-plugin</packaging>

通过properties定义maven的版本

<properties>
	<maven.version>2.2.1</maven.version>
</properties>

maven3已经出了很久,并且兼容maven2,因此我们团队内部都是统一使用maven3,但是我这里编写插件使用的是maven2,可以同时在maven2和maven3下使用,不过其实这个原因并不重要,真正的原因是因为maven3的代码实在太烂了,最初的时候我用maven3的api lib来写,发现里面很多代码根本没有注释,而且很多代码已经废弃,但是并没有明确说明究竟用什么方法代替。最后我使用了maven2中被maven3废弃的api来完成我的功能,跑的挺好的,就是有时可能会有一些使用准备废弃的api的提醒而已。

接着添加依赖

<dependencies>
	<dependency>
		<groupId>org.apache.maven</groupId>
		<artifactId>maven-plugin-api</artifactId>
		<version>${maven.version}</version>
	</dependency>
	<dependency>
		<groupId>org.apache.maven</groupId>
		<artifactId>maven-core</artifactId>
		<version>${maven.version}</version>
	</dependency>
</dependencies>

然后开始创建Mojo类,maven插件里面每一个具体的功能都是一个Mojo
比如说eclipse:clean和eclipse:eclipse就是两个Mojo

/**
 * @goal helloWorld
 */
public class HelloWorldMojo extends AbstractMojo {
    public void execute() throws MojoExecutionException
    {
        getLog().info("Hello, world!");
    }
}

首先继承AbstractMojo,并且实现execute()方法,这个就是每次调用进入的地方
然后需要在类的Javadoc上定义,这是一个annotation出来之前常用的定义方法(或许未来maven会将它改成annotation,那就能提供编译校验和IDE校验)。我们必须定义@goal,代表运行目标,简单来说就是eclipse:clean中的clean
Mojo写在哪个package底下都是可以的

这样,我们就完成了一个简单的maven plugin,然后我们需要一个简单的测试来确定他正确运行
先通过maven install将它安装到本地仓库

然后打开任意maven的项目(比如说我们原来已经在用maven的项目),在pom.xml增加一个plugin

<build>
    <plugins>
      <plugin>
        <groupId>xxx</groupId>
        <artifactId>xxx</artifactId>
        <version>xxx</version>
        <executions>
          <execution>
            <phase>compile</phase>
            <goals>
              <goal>helloWorld</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

需要留意的是phase部分,我们将这个plugin绑定到compile这个周期
然后我们运行mvn compile,就能成功看见Hello, world!输出
(当然你也可以直接通过命令行运行,需要带上完整的groupId和artifactId才能调用)

Posted in Java | Leave a comment

使用maven进行scala项目的构建

目标:
1、命令行用maven进行scala项目构建
2、产生eclipse项目文件

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/maven-v4_0_0.xsd">
	<groupId>com.xxx</groupId>
	<artifactId>xxx</artifactId>
	<version>1.0-SNAPSHOT</version>
	<modelVersion>4.0.0</modelVersion>
 
	<properties>
		<scala.version>2.8.1</scala.version>
	</properties>
 
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-eclipse-plugin</artifactId>
				<configuration>
					<downloadSources>true</downloadSources>
					<buildcommands>
						<buildcommand>org.scala-ide.sdt.core.scalabuilder</buildcommand>
					</buildcommands>
					<projectnatures>
						<projectnature>org.scala-ide.sdt.core.scalanature</projectnature>
						<projectnature>org.eclipse.jdt.core.javanature</projectnature>
					</projectnatures>
					<classpathContainers>
						<classpathContainer>org.eclipse.jdt.launching.JRE_CONTAINER</classpathContainer>
						<classpathContainer>org.scala-ide.sdt.launching.SCALA_CONTAINER</classpathContainer>
					</classpathContainers>
					<sourceIncludes>
						<sourceInclude>**/*.scala</sourceInclude>
					</sourceIncludes>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.scala-tools</groupId>
				<artifactId>maven-scala-plugin</artifactId>
				<executions>
					<execution>
						<goals>
							<goal>compile</goal>
							<goal>testCompile</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>build-helper-maven-plugin</artifactId>
				<executions>
					<execution>
						<id>add-source</id>
						<phase>generate-sources</phase>
						<goals>
							<goal>add-source</goal>
						</goals>
						<configuration>
							<sources>
								<source>src/main/scala</source>
							</sources>
						</configuration>
					</execution>
					<execution>
						<id>add-test-source</id>
						<phase>generate-sources</phase>
						<goals>
							<goal>add-test-source</goal>
						</goals>
						<configuration>
							<sources>
								<source>src/test/scala</source>
							</sources>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>

要点分析

	<properties>
		<scala.version>2.8.1</scala.version>
	</properties>

通过properties定义scala的版本
因为scala的版本是不兼容的,比如说2.8编译的class文件不能跟2.9的类库一起使用
这个定义同时会影响maven-scala-plugin中使用的scala版本
当然你也可以在dependencies中通过${scala.version}使用这个版本号

maven-eclipse-plugin部分是产生eclipse项目,对应使用scala-ide
打开downloadSources下载类库源代码,可以在eclipse中直接查看
sourceIncludes段必须加入,不然会出现代码目录中看不到scala文件的情况

maven-scala-plugin段用于命令行用maven进行构建

build-helper-maven-plugin段用于引入额外的代码目录
这个配置同时对命令行构建和eclipse项目生成有效
我这个是一个mixed Java/Scala项目

Posted in Java | Leave a comment

在Sandy Bridge上安装Snow Leopard

把家里的主机从AMD平台转到Intel平台之后,就一直想在上面装一个Snow Leopard来使用,虽然曾经拥有过一台最早的Intel MacBook,不过已经有一段时间没有使用Mac OS了。坦白说,三大操作系统(Windows、Linux、Mac OS)里面,Mac OS同时拥有对程序员良好的*NIX内核,比美Windows的操作界面(甚至更好),可能是最好的开发平台。

Mac的笔记本不算太贵,个人比较推荐,至于台式机部分,Mac mini有点太弱(不过外观相当好),iMac也相当划算(前提是你还没买显示器),我已经有了自己的主机,所以打算在上面装黑苹果。不过,如果你想用Mac OS来开发的话,其实最好买一台真正的Mac,这样可以减少你折腾的时间,把时间花在使用上。

先来说我的机器主要配置:
华擎 H67M + i5 2300 + 华硕 GT240(后加)

如果你不是太新或者太旧的Intel CPU的话,基本上都能支持,因为Mac用过这些CPU,所以AMD比较悲剧了,不过你依然能找到一些被修改过的kernel来使用。Sandy Bridge的CPU属于太新的CPU,虽然最新的MacBook Pro是使用这个CPU,但不知道为什么苹果是给他做了一个单独的10.6.7的更新包,也就是说默认的10.6.7更新包里面所包含的kernel是不支持Sandy Bridge的,有人从专用更新包里面提取了kernel出来,但是不知道为什么在很多主板上,这个kernel不能启动,我的主板就是这样的情况,因此我最后只能使用修改过的kernel。

一般来说,主板和CPU是决定你是否能够启动Snow Leopard的主要因素。不过,决定你的黑苹果是否好用的就是能否找到外设的驱动了,比如说网卡、声卡、显卡等等。目前黑苹果已经很成熟了,能找到很多爱好者写的驱动。我的主板上的外设基本上都被支持,可惜i5 2300的核显暂时还没有解决方案(虽然最新版的MacBook Pro就是使用这个显卡,理论上苹果已经提供了它的驱动,可惜目前还没有办法在黑苹果上使用它),于是上淘宝买了一张GT240,完美支持。

安装方法有几种,一个就是用一些已经打包好的整合碟,里面有破解内核和第三方驱动等,如果你是AMD的CPU最好用这些碟来安装,另一个就是用iBoot+MultiBeast和原版碟来安装,只支持Intel CPU,如果你是Intel CPU的话,推荐用这个方案,装出来的系统比较纯正一点。

网上已经有一些iBoot+MultiBeast的安装方法,大家可以搜索参考一些,这里就不说一些细节,重点是在Sandy Bridge的问题上。到http://www.tonymacx86.com/下载iBoot,必须注意的是必须使用Legacy版,因为Sandy Bridge CPU未被支持的缘故。刻碟启动,然后换Snow Leopard安装碟,安装。装完之后再用iBoot启动,就可以引导硬盘中的系统。

启动成功后,先安装10.6.7更新,不要重启,下载MultiBeast,安装。虽然DSDT的方法是最完美的,不过也很麻烦,需要导出主板等的BIOS来修改。我这里就用EasyBeast的方法来安装,必选EasyBeast、System Utilities,下面的驱动就看自己需要选择。声卡部分,我用ALC8xxHDA(优先选择,不行的话可以尝试VoodooHDA),还需要选上AppleHDA Rollback,这里还需要把自己的声卡ID写进DSDT里面,因为我不用DSDT,所以还需要勾上Non-DSDT HDAEnabler里面相应的声卡型号,显卡部分直接勾上NVEnabler就行了。

接下来是针对Sandy Bridge的处理,官方给出的方法是一个BridgeHelper的软件,其实就是安装最新的MacBook Pro里面的kernel,你可以先试一下这个方案,如果可以启动系统的话,优先选择这个方案。可惜我的主板不行,于是我从iBoot Legacy里提取了kernel,替换系统的。

另外,我还遇到一个问题是鼠标的移动会很卡的情况,好像是因为时钟频率不正确的缘故,需要在启动参数里面增加busratio=28(28是我的CPU的实际倍频数)

这样,我的黑苹果就装好了。虽然还有些小问题,不过我暂时不打算处理了,能正常使用就行了。

下一步需要处理的问题有
1、休眠。需要相应kernel的sleep enabler。由于我的是台式机,我觉得休眠不是很重要就不处理了。
2、HDMI输出,并且带上声音。是的,黑苹果也能这样。tonymacx86上已经提供了解决方案
3、DSDT,为了让系统属性里面正确显示各种硬件信息
4、真正的Sandy Bridge支持。CPU和显卡

Posted in Uncategorized | Leave a comment

MongoDB介绍

2010-11-20在广州小沙龙的ppt,时间比较匆忙写的比较简单
View more presentations from popeast.
Posted in Uncategorized | 1 Comment

基于MINA构建简单高性能的NIO应用-优化指南

本文为Sparkle发于《程序员》2008年2月刊的文章,与《程序员》的协议,可以在个人博客中发布,转载请保留出处。

优化指南
MINA默认配置的性能并不是很高的,部分原因是MINA目前还保留初期版本的架构,另外一个原因是因为JVM的发展。

首先我们关闭默认的ThreadModel设置

IoAcceptor acceptor = ...;
IoServiceConfig acceptorConfig = acceptor.getDefaultConfig();
acceptorConfig.setThreadModel(ThreadModel.MANUAL);

ThreadModel是一个很简单的线程实现,用于IoService。但是它实在太弱,以至于在并发环境产生大量问题。在MINA 2.0中,ThreadModel直接被取消。你应该使用ExecutorFilter来实现线程。

然后我们增加I/O处理线程(Article by Sparkle)
每一个Acceptor/Connector都使用一个线程来处理连接,然后把连接发送给I/O processor进行读写操作,我们只可以修改I/O processor使用的线程数,用以下代码设置

IoAcceptor acceptor = new SocketAcceptor(Runtime.getRuntime().availableProcessors() + 1, Executors.newCachedThreadPool());

当然是要将ExecutorFilter加入,上文已经很详细地描述了

acceptor.getDefaultConfig().getFilterChain().addLast("threadPool", new ExecutorFilter(Executors.newCachedThreadPool());

笔者在开发过程中,多次遇到OutOfMemoryError,经过研究之后才发现原因。MINA默认是使用direct memory实现ByteBuffer池的方案(以下简称direct buffer),通过JNI在内存开辟一段空间来使用,该方案在早期的MINA版本中是一个非常好的特性,那是因为MINA开发初期,JVM并没有现在的强大,带有池效果的direct buffer性能比较好。但是当我们使用-Xms -Xmx等指令增加JVM可使用的内存,那仅仅增加了堆的内存空间,而direct memory的空间并没有增加,导致MINA实际使用的时候经常出现OutOfMemoryError。如果你的确想使用direct memory,可以通过-XX:MaxDirectMemorySize选项来设置。不过笔者不建议这样做,因为最新的测试表明,在现代的JVM里面,direct memory比堆的表现更差。这里可能有读者会觉得奇怪,为什么不用池,而要用堆呢,而且还需要gc。那是因为现在的JVM gc能力已经很强了,而且在并发环境里面,pool的同步也是一个性能的问题。我们可以通过这样的代码进行设置(Article by Sparkle)

ByteBuffer.setUseDirectBuffers(false);
ByteBuffer.setAllocator(new SimpleByteBufferAllocator());

MINA 2.0已经默认把直接内存分配改成堆,为了提供最好的性能和稳定性。

最后一条优化技巧就是,把你的应用部署在Linux上,并且打开Java NIO使用Linux epoll的功能。可能你还没听过epoll,但是你应该听过Lighttpd、Nginx、Squid等,得益于epoll,它们提供很高的网络性能,还占用非常少的系统资源。JDK6已经默认把epoll配置打开,因此笔者建议把你的应用部署在JDK6上面,也同时因为JDK6还有别的优化特性。如果你的应用必须部署在JDK5上,你也可以通过参数把epoll支持打开。

文章快速索引

  1. 前言
  2. 一个简单的例子
  3. MINA架构
  4. 优化指南
Posted in Java | Tagged , | 1 Comment

基于MINA构建简单高性能的NIO应用-MINA架构

本文为Sparkle发于《程序员》2008年2月刊的文章,与《程序员》的协议,可以在个人博客中发布,转载请保留出处。

MINA架构

这里,我借用了一张Trustin Lee在Asia 2006的ppt里面的图片来介绍MINA的架构。

(图略)

Remote Peer就是客户端,而下方的框是MINA的主要结构,各个框之间的箭头代表数据流向。
大家可以对比刚刚的例子来看这个架构图,IoService就是整个MINA的入口,负责底层的IO操作,客户端发过来的消息就是由它处理。刚刚我们使用的IoAcceptor就是一个IoService,之所以抽象成IoService,是因为MINA用同样的架构来处理服务器和客户端编程,IoService的另一个子类就是IoConnector,用于客户端。不过根据笔者的使用经验,使用非阻塞的模型进行客户端编程非常的不方便,你最好寻求其他的阻塞通讯框架。
IoService把数据转化成一个一个的事件,传递给IoFilterChain。你可以加入一连串的IoFilter,进行各种功能。笔者的建议是将一些功能性的,业务不相关的代码,用IoFilter来实现,使得整个应用结构更清晰,也方便代码重用。(Article by Sparkle)
被IoFilter处理过的事件,发送给 IoHandler,然后我们在这里实现具体的业务逻辑。这个部分很简单,如果你有Swing的使用经验的话,你会发现它跟Swing的事件非常相像,你要做的事情,仅仅是重载你需要的方法,然后编写具体的业务功能。在这其中,最重要的一个方法就是messageReceived了。
值得留意的是一个IoSession的类,每一个IoSession实例代表这一个连接,我们需要对连接进行的任何操作都通过这个类来实现。
从IoHandler通过调用IoSession.write等方法向客户端发送的消息,会通过跟输入数据相反的次序依次传递,直至由IoService负责把数据发送给客户端。
这就已经是MINA的全部,是不是很简单。

接下来,我会详细介绍我们编写具体代码的时候主要涉及到的三个类,IoHandler、IoSession和IoFilter。

IoHandler

public interface IoHandler {
  void sessionCreated(IoSession session) throws Exception;
  void sessionOpened(IoSession session) throws Exception;
  void sessionClosed(IoSession session) throws Exception;
  void sessionIdle(IoSession session, IdleStatus status) throws Exception;
  void exceptionCaught(IoSession session, Throwable cause) throws Exception;
  void messageReceived(IoSession session, Object message) throws Exception;
  void messageSent(IoSession session, Object message) throws Exception;
}

MINA的内部实现了一个事件模型,而IoHanlder则是所有事件最终产生响应的位置。每一个方法的名字很明确表明该事件的含义。messageReceived是接收客户端消息的事件,我们应该在这里实现业务逻辑。messageSent是服务器发送消息的事件,一般情况下我们不会使用它。sessionClosed是客户端断开连接的事件,可以在这里进行一些资源回收等操作。值得留意的是,客户端连接有两个事件,sessionCreated和sessionOpened,两者稍有不同,sessionCreated是由I/O processor线程触发的,而sessionOpened在其后,由业务线程触发的,由于MINA的I/O processor线程非常少,因此如果我们真的需要使用sessionCreated,也必须是耗时短的操作,一般情况下,我们应该把业务初始化的功能放在sessionOpened事件中。(Article by Sparkle)
细心的读者可能会发现,我们刚刚的例子继承的是IoHandlerAdapter,IoHandlerAdapter其实就是一个IoHanlder的空的实现,这样我们就可以不用重载不感兴趣的事件。

IoSession
IoSession是一个接口,MINA里很多的地方都使用接口,很好地体现了面向接口编程的思想。它提供了对当前连接的操作功能,还有用户定义属性的存储功能,这点非常重要。IoSession是线程安全的,也就是我们能够在多线程环境中随意操作IoSession,这点给开发带来很大的好处。我们来看看具体提供的方法,笔者列举一些比较常用和重要的方法

WriteFuture write(Object message)
CloseFuture close();
 
Object getAttribute(String key);
Object setAttribute(String key, Object value);
Object removeAttribute(String key);
Set getAttributeKeys();
 
boolean isConnected();
boolean isClosing();
SocketAddress getRemoteAddress();
boolean isIdle(IdleStatus status);

在这里,笔者把IoSession的方法大致分成三类
第一类,连接操作功能。
最主要的方法有两个,向客户端发送消息和断开连接。可以看的出,write接受的变量是一个Object,但是实际上应该传入什么类型呢?具体还得看你是否使用了ProtocolCodecFilter(下面会详细介绍),如果使用了ProtocolCodecFilter,那这个message将可能是一个String,或者是一个用户定义的JavaBean。默认的情况,message是一个ByteBuffer。ByteBuffer是MINA的一个类,跟java.nio.ByteBuffer类同名,MINA 2.0将会将它改成IoBuffer,以避免讨论上的误会。(Article by Sparkle)
另一个值得留意的是Future类,MINA是一个非阻塞的通信框架,其中一个明显的体现就是调用IoSession.write方法是不会阻塞的。用户调用了write方法之后,消息内容会发到底层等候发送,至于什么时候发出,就不得而知了。当然,实际上调用了write之后,数据几乎是立刻发出的,这得益与NIO的高性能。但是,如果我们必须确认了消息发出,然后进行某些处理,我们就需要使用Future类,以下是一个很常见的代码

IoSession session = ...;
WriteFuture future = session.write(...);
// Wait until the message is completely written out to the O/S buffer.
future.join();
if( future.isWritten() )  {
  // The message has been written successfully.
}  else  {
  // The messsage couldn't be written out completely for some reason.
  // (e.g. Connection is closed)
}

通过调用future.join,程序就会阻塞,直至消息处理结束。我们还能通过future.isWritten得知消息是否成功发送。
在这里,笔者顺便说一个实际使用的发现,消息发送是会自动合并的,简单来说,如果在很短的时间里,对同一个IoSession进行了两次write操作,客户端有可能只收到一条消息,而这条消息就是服务器发出的两条消息前后接起来。这样的设计可以在高并发的时候节省网络开销,而笔者的实际使用过程中,效果也相当好。但是如果这样行为会导致客户端工作不正常,你也可以通过参数关闭它。

第二类,属性存储操作。
通常来说,我们的系统是有用户状态的,我们就需要在连接上存储用户属性,IoSession的Attribute就是这样一个功能。例如两个连接同时连入服务器,一个连接是用户A,用户ID是13,另一个连接是用户B,用户ID是14,我们就可以在用户登录成功之后,调用IoSession.setAttribute(“login_id”,13),然后在其后的操作中,通过IoSession.getAttribute(“login_id”)获得当前登录用户ID,并进行相应的操作。简单来说,就是一个类似HttpSession的功能,当然具体的实现方法不一样。(Article by Sparkle)

第三类,连接状态。
这里就不多说了,从方法名上我们就能知道它具体的功能。

IoFilter
过滤器是MINA的一个很重要的功能。IoFilter也是一个接口,但是相对比较复杂,这里就不列举它的方法了。简单来说IoFilter就像ServletFilter,在事件被IoHandler处理之前或之后进行一些特定的操作,但是它比ServletFilter复杂,可以处理很多种事件,除了包括IoHandler的7个事件以外,还有一些内部的事件可以进行操作。
MINA提供了一些常用的IoFilter实现,例如有LoggingFilter(日志功能)、BlacklistFilter(黑名单功能)、CompressionFilter(压缩功能)、SSLFilter(SSL支持),这些过滤器比较简单,通过阅读它们的源代码,能够更进一步理解过滤器的实现。笔者在这里要重点介绍两个过滤器,ProtocolCodecFilter和ExecutorFilter

ProtocolCodecFilter
网络传输的内容其实本质是一个二进制流,但是我们的业务功能不会,或者说不应该去直接操作二进制流。MINA默认向IoHandler传入的message是一个ByteBuffer,如果我们直接在IoHandler操作ByteBuffer,会导致大量协议分析的代码和实际的业务代码混杂在一起。最适合的做法,就是在IoFilter把ByteBuffer转换成String或者JavaBean,ProtocolCodecFilter正是这样的一个功能的过滤器。
使用ProtocolCodecFilter很简单,我们只要把ProtocolCodecFilter加入到FilterChain就可以了,但是我们需要提供一个ProtocolCodecFactory。其实ProtocolCodecFilter仅仅是实现了过滤器部分的功能,它会将最终的转换工作,交给从ProtocolCodecFactory获得的Encode和Decode。如果我们需要编写自己的ProtocolCodec,就应该从ProtocolCodecFactory入手。MINA内置了几个ProtocolCodecFactory,比较常用的就是ObjectSerializationCodecFactory和TextLineCodecFactory。(Article by Sparkle)
ObjectSerializationCodecFactory是Java Object序列化之后的内容直接跟ByteBuffer互相转化,比较适合两端都是Java的情况使用。TextLineCodecFactory就是String跟ByteBuffer的转化,说白了就是文本,例如你要实现一个SMTP服务器,或者POP服务器,就可以使用它。而笔者的实际使用,大多数情况都是使用TextLineCodecFactory。
这里提及一下IoFilter的顺序问题,IoFilter是有加入顺序的,例如,先加入LoggingFilter再加入ProtocolCodecFilter,和先加入ProtocolCodecFilter再加入LoggingFilter的效果是不一样的,前者LoggingFilter写入日志的内容是ByteBuffer,而后者写入日志的是转换后具体的类,例如String。实际使用的时候,一定要处理好过滤器的顺序。

ExecutorFilter
另一个重要的过滤器就是ExecutorFilter。这里,我需要先说明一下MINA的线程工作模式,MINA默认是单线程处理所有客户端的消息,也就是说,即使你在一台8CPU的机器上面跑,可能也只用到一个CPU,另外,如果某次消息处理太耗时,就会导致其他消息等待,整体的吞吐量下降。很多朋友抱怨MINA的性能差,其实是因为他们没有加入ExecutorFilter的缘故。ExecutorFilter设计的很精巧,大家可以仔细阅读一下源代码,它会将同一个连接的消息合并起来按顺序调用,不会出现两个线程同时处理同一个连接的情况。(Article by Sparkle)
这里再次提及IoFitler的顺序问题,一般情况下,我们会将ExecutorFilter放在ProtocolCodecFilter之后,因为我们不需要多线程地执行ProtocolCodec操作,用单一线程来进行ProtocolCodec性能会比较高,而具体的业务逻辑可能还设计数据库操作,因此更适合放在不同的线程中运行。

文章快速索引

  1. 前言
  2. 一个简单的例子
  3. MINA架构
  4. 优化指南
Posted in Java | Tagged , | 4 Comments

基于MINA构建简单高性能的NIO应用-一个简单的例子

本文为Sparkle发于《程序员》2008年2月刊的文章,与《程序员》的协议,可以在个人博客中发布,转载请保留出处。

一个简单的例子

MINA使用非常简单,笔者以前做过一段时间传统的Java Socket开发,不过一直对Java NIO不是很理解,但是MINA很快就上手了,MINA封装了NIO繁琐的部分,使你可以更专注于业务功能实现。话不多说,让我们来看一个简单的例子,一个很常见的例子,时间服务器。(Article by Sparkle)
我们的实现目标是一个能响应多个客户端的请求,然后返回服务器当前的系统时间的功能。传统的Java Socket程序,我们需要每accept一个客户端连接,就创建一个新的线程来响应,这会令到系统整体负载能力有较大的限制,而且我们必须手工编写连接管理等代码。让我们来看看MINA是怎么处理的。

首先我们从官方网站下载MINA 1.1,这里我们假设JDK为1.5以上的版本,如果你使用的是JDK 1.4,请下载MINA 1.0,MINA 1.0跟1.1几乎一样,但是强烈建议使用JDK 1.5以上以获得更好的性能。
解开压缩包之后,能看见很多jar包,这里暂不介绍每个包的具体作用,可以把所有包都导入项目。值得留意的是MINA使用了一个slf4j的日志库,该日志库大有取缔common-logging之势。

public class TimeServer {
  public static void main(String[] args) throws IOException {
    IoAcceptor acceptor = new SocketAcceptor();
 
    SocketAcceptorConfig cfg = new SocketAcceptorConfig();
    cfg.getFilterChain().addLast( "logger", new LoggingFilter() );
    cfg.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory()));
 
    acceptor.bind( new InetSocketAddress(8123), new TimeServerHandler(), cfg);
    System.out.println("Time server started.");
  }
}

这里是我们的主程序,非常简单。
首先我们需要一个IoAcceptor,这里我们选择了一个SocketAcceptor,也就是TCP协议。
然后,我们给应用加上日志过滤器和协议编码过滤器。(Article by Sparkle)
最后,我们把acceptor bind到本机的8123端口,并且使用TimeServerHandler来实现协议。

TimeServerHandler是我们实现具体业务功能的地方。

public class TimeServerHandler extends IoHandlerAdapter {
  public void messageReceived(IoSession session, Object msg) throws Exception {
    String str = (String) msg;
    if( "quit".equalsIgnoreCase(str) ) {
    session.close();
    return;
  }
 
  Date date = new Date();
  session.write( date.toString() );
  System.out.println("Message written...");
  }
 
  public void sessionCreated(IoSession session) throws Exception {
  System.out.println("Session created...");
  }
}

IoHandlerAdapter提供了7个事件方法,我们要做的事情仅仅是挑选我们需要做出响应的事件进行重载。在我这个例子了,我重载了两个方法。sessionCreated会在客户端连接的时候调用,通常我们会在这里进行一些初始化操作,我这里仅仅是打印一条信息。messageReceived就是整个Handler的中心部分,每一个从客户端发过来的消息都会转化成对该方法的调用。由于我们加入了协议编码过滤器,因此这里获得的Object msg是一个String,而不是默认的ByteBuffer(下文会详细介绍ProtocolCodecFilter)。这里我们实现了一个很简单的业务功能,如果用户输入的是quit,就断开连接,否则就输入当前时间。可以看出,IoSession封装了对当前连接的操作。

至此,我们就实现了一个时间服务器。

文章快速索引

  1. 前言
  2. 一个简单的例子
  3. MINA架构
  4. 优化指南
Posted in Java | Tagged , | 1 Comment