【译】Java的通用I/O API

本文内容遵从CC版权协议, 可以随意转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明网址: 转载自 【译】Java的通用I/O API http://www.oldratlee.com/474/tech/java/generic-io-api-in-java.html

原文:A generic input/output API in Java

上周处理了很多数据搬移,有原始byte形式的,也有String形式的,还有SPI和领域级对象形式。这些活让我觉得,以可伸缩、高性能、正确处理错误的方式把数据从一处搬到另一处,是非常有难度。我要一遍又一遍做一些事,比如从文件中读出String。

这让我有了个想法:一定有个通用模式来处理这些事,可以取出来放到库中。“从文本文件中读取lines”应该只做一遍,然后用在各个需要的场景中。让我们看一个读文件然后写入另一个文件的典型场景,看看能发现包含了哪几个部分:

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
1: File source = new File( getClass().getResource( "/iotest.txt" ).getFile() );
1: File destination = File.createTempFile( "test", ".txt" );
1: destination.deleteOnExit();
2: BufferedReader reader = new BufferedReader(new FileReader(source));
3: long count = 0;
2: try
2: {
4:    BufferedWriter writer = new BufferedWriter(new FileWriter(destination));
4:    try
4:    {
2:        String line = null;
2:        while ((line = reader.readLine()) != null)
2:        {
3:            count++;
4:            writer.append( line ).append( '\n' );
2:        }
4:        writer.close();
4:    } catch (IOException e)
4:    {
4:        writer.close();
4:        destination.delete();
4:    }
2: } finally
2: {
2:     reader.close();
2: }
1: System.out.println(count)

行左边的数字是我标识的4个部分。

  1. 客户代码,初始化了传输,要知道输入和输出的源。
  2. 从输入中读的代码。
  3. 辅助代码,用于跟踪整个过程。这些代码我希望能够重用,而不管是传输的类型。
  4. 最后这个部分是接收数据,写数据。这个代码,我要批量读写,可以在第2第4部分修改,改成一次处理多行。

API

一旦明确上面划分的内容,剩下就只是为每个部分整理成一个接口,并保证在各种场景能方便使用。结果如下。 首先要有输入,即Input接口:

1
2
3
4
5
6
public interface Input<T, SenderThrowableType extends Throwable>
{
    <ReceiverThrowableType extends Throwable> 
    void transferTo( Output<T,ReceiverThrowableType> output )
        throws SenderThrowableType, ReceiverThrowableType;
}

Input,如Iterables,可以被多次使用,用于初始化一处到另一处的传输。因为我泛化传输的数据类型为T,所以可以是任何类型(byte[]、String、EntityState、MyDomainObject)。为了让发送者和接收者可以抛出各自的异常,接口上把各自己的异常声明成了类型参数。比如:在出错的时,Input抛的可以是SQLException,Output抛的是IOException。异常是强类型的,并且在出错时发送和接收双方都必须知道的,这使的双方做合适的恢复操作,关闭他们打开了的资源。

在接收端的是Output接口:

1
2
3
4
5
6
public interface Output<T, ReceiverThrowableType extends Throwable>
{
    <SenderThrowableType extends Throwable> 
    void receiveFrom(Sender<T, SenderThrowableType> sender)
            throws ReceiverThrowableType, SenderThrowableType;
}

当receiveFrom方法被Input调用时(通过调用Input的transferTo方法触发),Output应该打开好了它所需要的资源,然后期望数据从Sender发送过来。Input和Output必须要有类型T,两者对要发送的内容达到一致。后面我们可以看到如何处理不一致的情况。

接下来是Sender接口:

1
2
3
4
5
6
public interface Sender<T, SenderThrowableType extends Throwable>
{
    <ReceiverThrowableType extends Throwable>
    void sendTo(Receiver<T, ReceiverThrowableType> receiver)
        throws ReceiverThrowableType, SenderThrowableType;
}

Output调用sendTo方法,传入一个Receiver,Sender使用这个Receiver来发送一个一个的数据。Sender在这个时候发起传输,把类型数据T传输到Receiver,一次一个。Receiver接口如下:

1
2
3
4
5
public interface Receiver<T, ReceiverThrowableType extends Throwable>
{
    void receive(T item)
        throws ReceiverThrowableType;
}

当Receiver从Sender收到数据时,即可以马上写到底层资源中,也可以分批写入。Receiver知道传输什么时候结束(sendTo方法返回了),所以正确写入剩下的分批数据、关闭持有的资源。

这个简单的模式在发送方和接收方各有2个接口,并保持了以可伸缩、高性能和容错的方式传输数据的潜能。

标准化I/O

上文的API定义了数据发送和接收的契约,然后可以制定几个输入输出(I/O)的标准。比如:从文本文件中读取文本行后再写成文本文件。这个操作可以静态方法中,方便的重用。最后,拷贝文本文件可以写成:

1
2
3
File source = ...
File destination = ...
Inputs.text( source ).transferTo( Outputs.text(destination) );

一行代码处理了读文件、写文件、资源清理和其它零零碎碎的操作。真心的赞!transferTo方法会抛出IOException,要向用户显示Error可以catch这个异常。但实际处理这些Error往往是,关闭文件,把没有写成功的文件删除,而这些Input、Output已经处理好了。我们再也不需要关心文件读写的细节!

拦截传输过程

上面处理了基本的I/O传输,我们常常还要做些其它的事。可能要计数一下传输了多少个数据,过滤一下数据,或者是每1000条数据做一下日志,又或者要看一下正在进行什么操作。既然输入输出已经分离,这些事变成在输入输出的协调代码中简单地插入一些逻辑。大部分协调代码有类似的功能,可以放到标准的工具方法中,更方便使用。

第一个标准修饰器是一个过滤器。实现时我用到了Specification。

1
2
3
4
5
6
public static <T,ReceiverThrowableType extends Throwable> 
Output<T, ReceiverThrowableType> filter
( final Specification<T> specification, final Output<T, ReceiverThrowableType> output)
{
   ... create an Output that filters items based on the Specification<T> ...
}

Specification如下:

1
2
3
4
interface Specification<T>
{
     boolean test(T item);
}

有了这个简单部件,我可以在传输时轻松地过滤掉那些不要出现在接收者端的数据。下面的例子删除文件中的空行:

1
2
3
4
5
6
7
8
9
File source = ...
File destination = ...
Inputs.text( source ).transferTo( Transforms.filter(new Specification<String>()
{
   public boolean test(String string)
   {
      return string.length() != 0;
   }
}, Outputs.text(destination) );

第二个常见的操作是把数据从一个类型映射到另一个类型。就是处理要Input和Output的数据类型不同,要有方法把输入数据类型映射成输出的数据类型。下面例子的把String映射成JSONObject,操作方法会是这个样子:

1
2
3
4
public static <From,To,ReceiverThrowableType extends Throwable> 
Output<From, ReceiverThrowableType> map(
   final Function<From,To> function, 
   final Output<To, ReceiverThrowableType> output)

Function定义是:

1
2
3
4
interface Function<From, To>
{
    To map(From from);
}

通过这些,可以把String的Input连接到JSONObject的Output:

1
2
3
Input<String,IOException> input = ...;
Output<JSONObject,RuntimeException> output = ...;
input.transferTo(Transforms.map(new String2JSON(), output);

String2JSON类实现了Function接口,它的map方法把String转换成JSONObject。

到了现在,我们可以实现前面提到数据计数的例子,可以把计数实现成一个通用的映射,转换前后的类型不变,只是维护了一个计数,在每次调用map方法时更新计数。例子代码如下:

1
2
3
4
5
File source = ...
File destination = ...
Counter<String> counter = new Counter<String>();
Inputs.text( source ).transferTo( Transforms.map(counter, Outputs.text(destination) ));
System.out.println("Nr of lines:"+counter.getCount())

Usage in the Qi4j SPI

【译者注,这一节说具体库Qi4j,略过】

 

结论

软件开发时,从一个输入到另一个输出的数据和对象的搬移很常见,可能在中间还要做些转换。通常都是用一些零散代码(scratch)来完成这些事,结果是代码错误和使用不当的模式。通过引入通用I/O API,恰当封闭和隔离,这个任务可以可以更轻松地以伸缩、高性能、无错误的方式完成,并且还可以在在需要额外功能时修饰实现。

这遍文章仅仅勾勒了这种使用方式,API和辅助类可以在Qi4j Core 1.3-SNAPSHOT中有(详见Qi4j的主页)。理想状态是,在整个Qi4j使用中任何使用I/O的地方一开始按这种方式来。

多谢你的阅读,希望你能有所收获 :-)

 

<EOF>

26 次浏览 | 没有评论
2012年5月11日 | 归档于 Java

Shell命令在多个zip文件查找文件名

本文内容遵从CC版权协议, 可以随意转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明网址: 转载自 Shell命令在多个zip文件查找文件名 http://www.oldratlee.com/458/tech/shell/find-file-in-zip.html

开发的Lib目录有多个Jar文件,常常会要查找一下log4j.properties文件中那个Jar文件中。然后看看里面的配置,必要的时候要直接改一下。

一个可用的实现如下:

1
2
find -name '*.jar' -exec sh -c \
    'jar tf $0 | fgrep log4j.properties | xargs -r printf "$0!%s\n"' {} \;

即用Find命令找出Jar文件,用jar命令list中Jar中文件名,grep到log4j.properties后,输出成

1
path/to/file.jar!path/to/log4j.properties

的格式,jar文件和jar里面的文件用!分隔。

 

这里使用jar tf来列出Jar文件中的文件名,也可以使用unzip –l命令。Jar文件即是Zip格式。

但是zip -l列出的输出格式是

1
2
3
4
5
6
7
8
9
10
11
$ unzip -l foo.jar
Archive:  foo.jar
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2012-01-11 18:55   META-INF/
      571  2012-01-11 18:55   META-INF/MANIFEST.MF
        0  2012-01-11 18:55   META-INF/maven/
      165  2012-01-11 18:55   META-INF/INDEX.LIST
......
---------                     -------
     3430                     8 files

我还没有找到不输出头和尾信息的选项。这里要组合一些命令来去了。

 

这个脚本有需要加强的地方:

  • 如果Jar文件中grep的文件名很多,printf命令就会因参数过多而执行失败了。
  • 如何Jar文件名中包含如 %s, printf命令的输出就不对了(需要完整的转义)
  • 可以抽成脚本,方便执行。如,把要找的文件名模式,把哪个文件中找做成脚本参数。

下次再整理一下 :)

35 次浏览 | 没有评论
2012年4月11日 | 归档于 Shell
标签: , , , ,

Sed分享

本文内容遵从CC版权协议, 可以随意转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明网址: 转载自 Sed分享 http://www.oldratlee.com/448/tech/unix/sed-study-and-share.html

,之前一直对sed和awk一知半解,平时有简单的使用,也知道功能强大,一直想好好研究一下。

过年放假看了sed & awk 2nd edtion 这本书。

看完了也写个PPT在组内分享一下 :-)

只写了sed,没有写awk:

  1. awk和Perl等语言类似,共同的东西的比较多,知道的人多;sed则很不一样,显得更有意思。
  2. sed把语法压缩得很简练,学习和使用过程思维不一样,可以作为一种小娱乐~
  3. sed是文本操作的DSL,语法约定很有借鉴意义。

PPT不会有sed详细的语法和命令说明,下面的资料很不错:

173 次浏览 | 没有评论
2012年2月8日 | 归档于 Unix/Linux
标签: , , , ,

Dubbo的扩展点加载(ExtensionLoader)的实现方式

本文内容遵从CC版权协议, 可以随意转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明网址: 转载自 Dubbo的扩展点加载(ExtensionLoader)的实现方式 http://www.oldratlee.com/430/tech/java/dubbo-extension-load-implement.html

Java有几个常用扩展点加载的实现:

标准的Java Service(sun.misc.Service/java.util.ServiceLoader)

Spring classpath*

OSGi

都可以做到新加入一个Extension的Jar在启动时甚至是运行时发现新的扩展点。

Dubbo的扩展点实现方式采用了标准Java Service,使用相同的配置文件,在此之上结合Dubbo的使用方式作一些加强。下面给出说明。

一、Java的ServiceLoader

标准的Java的Service的说明在这里有说明:
http://download.oracle.com/javase/1.5.0/docs/guide/jar/jar.html#Service Provider

JDK5类在对应的是 sun.misc.Service ,到了JDK6后移到了 java.util.ServiceLoader ,成为了标准Java API了。源代码 http://www.docjar.com/html/api/java/util/ServiceLoader.java.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 常量及设置好ServiceLocator属性
final String PREFIX = "META-INF/services/";
Class<t> service = ...; // Service接口
ClassLoader classloader = ...; // 所用的ClassLoader
 
String fullName = PREFIX + service.getName();
Enumeration<url> configs = classloader.getResources(fullName);
Set<String><string> providerClassNames = new HashSet<string>();
while (configs.hasMoreElements()) {
     URL u = configs.nextElement();
 
     ArrayList<string> names = new ArrayList<string>();
     InputStream in = u.openStream();
     BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"));
 
     ... // 从取每一行(Service Provider的类名)放入 Set providerClassNames 
}
 
// 拿到providerClassNames后,通过 Class.forName(classname, true, loader).newInstance()方法,
// 加载类Provider类,并返回Service的Provider类的各个Instance

 

【未完待续ing…】

475 次浏览 | 没有评论
2011年10月21日 | 归档于 Java

Java获取当前时间方法的比较以及时间的处理

本文内容遵从CC版权协议, 可以随意转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明网址: 转载自 Java获取当前时间方法的比较以及时间的处理 http://www.oldratlee.com/410/tech/java/the-comparation-between-methods-of-getting-current-time-in-java-and-the-process-of-time.html

一、获取当前时间

在Java中获取当前时间我一般总是使用long java.lang.System.currentTimeMillis()方法,Long类型是基本类型,运算(如比较时间的前后)、传输和存储都很方便。

当然,如果有的方法要使用java.util.Date类型,可能通过构造函数new Date(long date)来切换类型。java.util.Date.getTime()方法,则把Date类型切换成Long类型。当然使用Date的缺省构造函数new Date()来获得当前的时间的Date。

查看国际站的代码你会看到不少代码使用了java.util.Calendar.getInstance()方法。再通过java.util.Calendar.getTime()来获取Date,或是通过java.util.Calendar.getTimeInMillis()来获取Long。

或是Google一下“Java获取当前时间”,会看到很多人也是推荐使用Calendar,说Date很多方法已经@Deprecated。

我个人不赞同这一点,原因如下:

  • 简单方法能解决问题。Calendar显然概念上复杂很多,另两种用法用意也是非常的清晰。
  • 性能问题。看下面运行数据。
    (不要一开始就考虑性能问题,性能问题往往不会成为问题;性能问题往往也是由上一点复杂性引起的)

三个方法运行10M次所用时间:(在我开发机上的运行时间,只是示意一下比例)

1
2
3
System.currentTimeMillis() 10M times: 169ms
new Date() 10M times: 283ms
Calendar.getInstance() 10M times: 6625ms

# java.util.Calendar.getInstance()方法消耗时间 约是 new Date() 的 20倍。

二、时间的处理

上面说了获取当前时间不要用Calendar类,那什么时候使用Calendar类呢?

Long类型,表示了时间的值;Date类型,提供了几个获取年月日的方法、字符串,还有对象的标准方法()。

时间的操作:

  • 比较时间前后、计算两个时间差值。
  • 计算出时间对应的的年月日、所在的周数,等等。

    这些计算涉及其它的信息:所在时区(不同时区几点是不一样的),本地化信息(输出成中文的星期一、还是英文的Monday)

    做这些计算正是实现复杂的Calendar类要解决的问题。

  • 常用的时间日期打印操作,比如“2012年3月4日 15:16:17”,显然有年月日的计算,涉及了时区、本地化。
    记录、存储、传输时间,使用long类型(只需4字节存储空间)、Date类型。时间计算、显示时间使用Calendar类提供的方便方法。
700 次浏览 | 没有评论
2011年9月2日 | 归档于 Java