Java编程十五讲

作者: 郭 屹
译者:
编辑: 胡俊英
分类: Java

图书目录:

详情

内容源自Java团队元老级大咖,通过15个讲义的形式,将Java的点点滴滴娓娓道来。让最了解Java的人,给你讲讲关于Java学习的事儿 谨以本书向参与Java研发和推动Java进步的前辈们致敬!

内容提要

本专栏由Java资深元老级大咖创作而成,以15个讲义的形式阐释了Java的点点滴滴。每个讲义围绕一个Java核心知识点进行深度解读,涉及注释、类加载器、反射、事件、泛型、代理、内部类、正则表达式、IO和NIO、Lambda表达式、脚本、多线程、容器框架(第13讲~第15讲)。

本专栏适合有一些Java基础的读者阅读,旨在从Java前辈的角度引导读者走近Java,认识Java的方方面面,从而爱上Java,精通Java的编程技巧。


作者简介

郭屹 南开大学计算机专业毕业,拥有多年软件开发经验,曾是Sun公司技术研发中心Java团队的重要成员,见证并推动了Java的研发,所参与研发的产品多次荣获国际奖项。

图书摘要

版权信息

书名:Java编程十五讲

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    郭 屹

责任编辑 胡俊英

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内容简介

内容源自Java团队元老级大咖,通过15个讲义的形式,将Java的点点滴滴娓娓道来。让最了解Java的人,给你讲讲关于Java学习的事儿 谨以本书向参与Java研发和推动Java进步的前辈们致敬!

一本书,没有大家的批评帮助,是不能完成的,更加没有勇气发布出去。在写作的过程中,我得到了很多人的鼓励和指正,在此一并致谢。自然,所有的不足和错误应由我自己承担。

感谢有赞公司的资深工程师肖雯敏,给了我很多指正,特别是代码方面的建议。

感谢我的老朋友,Sun技术研发中心的技术主管Sean Jiang先生。他慷慨地用不再熟练的中文为本专栏写了序言,并给了我很多溢美之词,我领会了Sean的好意。

Norming软件公司技术总监陶斌通读了本书,并安排在公司内部技术团队讲座,这是对本专栏的明确肯定。

我童年时代的玩伴,现任中南大学教授毕文杰博士,在繁忙的研究生毕业季应允为我的专栏写书评。儿时伙伴长大后能在专业方面互相切磋,这让我体会到了人生的温暖。

红网副总裁孔泽平,我大学的师兄,鼓励我写作并发布专栏。没有他,我是想不到的。

齐军女士用她一贯的仔细,帮我指出了许多词句方面的问题,她仔细的程度令我感动和汗颜。

合肥工业大学计算机系的王楷涵、刘鹏飞诸位同学,他们在学业之余帮助我调试所有代码。感谢你们,希望我们教学相长,都各自取得自己专业上的造诣。

写作过程中用到的几张图都是网络上常见的,找不到原始来源,也要对作者一并致谢。

最后,永远不应该忘记的是感谢我的家人。


20世纪80年代后期至90年代初,个人计算机开始流行。1990 年,Tim Berners-Lee 创建了HTML,随后以开放共享为主题思想的Internet随着静态页面的门户网站开始兴起。这种网络热潮掀起了商务网站(Dot Com)泡沫,一直延续到了2001年。起初的网站直到1995年Java语言的出现之前,都是基于静止网页的单向内容分享。1995年太阳微计算机公司(Sun Micro systems Inc.)发明了Java 编程语言。用Java开发的运行于浏览器(当时流行的浏览器是Netscape)里的Applet, 让网页“活”了起来。这种开放式的、简洁的、面向对象编程语言,并以可移植性(Portability)“Write Once, Run Anywhere”为主旨,迅速风靡起来。Java语言伴随网络泡沫火起来的程度让人难以置信。据传当时北美开出租车的司机都可以经过快速Java培训,立马找到工作。当然Java的迅速流行,离不开Sun 公司对它的开放以及诸多大的软件公司的支持包括IBM和Oracle,从而也让Java在企业计算服务端更显示它的优势。起初James Gosling是为可植入设备(embedded device)创建的Java编程语言。尽管J2ME (Java 2 Platform, Micro Edition) 在手机上取得了一定的成功,但苦于各大手机厂商各自开发自己的硬件和不兼容的操作系统,并没有能在手机市场上统领天下。也可能是当时时机没有成熟(开源Linux还未成熟), 缺少许可费用和厂商的支持。后来,Google走了它自己的路,创建了面向手机的Java的衍生语言Android,同时开始了与Oracle 漫长的关于Java语言版权的法律之争 (2010年Oracle并购了Sun 公司)。

在Java 过去24年的发展过程中,也是Internet、云计算、电子商务、社交媒体和物联网的蓬勃发展时期。纵观当前著名的互联网公司,大部分是在Java出现后诞生的,如eBay(1995)、Netflix(1997)、Google(1998)、腾讯(1998)、阿里巴巴(1999)、百度(2000)和Facebook(2004)等。在过去20多年的从移动设备,云计算,到大数据人工智能的飞跃发展,在很大程度上离不开Java语言和构架,以及基于Java的开源的贡献。这个时代也是市场全球化和共享经济的鼎盛时期。正如Joshua Cooper Ramo 在《第七感官》书中(The Seventh Sense: Power, Fortune, and Survival in the Age of Networks)所说,在当前实体和数字世界紧密连接的网络世界,在世界的某个角落的一个不起眼的小事件,可能对世界的另一端造成很大的影响,而且 这种发展的速度将越来越快。这种紧密连接的网络(Connected Network)效应,以速度和规模对很多基于老旧的运行模式和技术的传统行业产生很大的冲击和瓦解,从而迫使大的传统企业加速进行数字化转型,接受并采纳云计算,精简流程,开发和支持一体化和自动化、人工智能等新技术和方法。这种大的形势和趋势,带动了企业应用计算,编程语言,及架构的变革。原有的基于共享资源为主的复杂的相互依赖的大型的企业应用构架,因其负责性和交错依赖性,难于维护和测试,导致开发周期长,不能满足现在的需求,从而逐渐被微服务架构取代。编程语言也由于分工的精细化和特殊需求,出现了不同的分支和演变,如侧重于数据科学和分析的Python和R语言,侧重于服务器端的非同步网络应用的Node.JS(based on event loop Server-side Javascript)和GO。然而,在面向微服务构架的企业应用中,基于Java的Spring Boot和Pivotal Cloud Foundry,以及结合Kafka event streaming 的组合,依然是云原生(Cloud Native)应用开发环境和平台的一大主流。

1997年 Sun 公司为了加快在大中国区拓广服务器业务的市场,成立了一个老虎计划的项目(Tiger Program)。此项目集结Sun在硅谷本部的和本地的大量人才资源在北京成立了技术开发中心(Technical Development Center)。技术开发中心开始主要侧重Solaris和Java在中文本地化方面,提供技术服务和支持,并负责在中国扩大Java的宣传和推广,从而增加Sun公司在中国的市场影响和服务器的销售。很快随着团队的扩大和技术实力的提高,开始了和Sun本部合作进行更多更底层的产品开发,包括生成JDK底层核心编码(sun.io包)转换器的工具。郭屹当时就和我在这个Java技术团队,负责工具、中间件、参考模型应用实现,中文本地化和游戏的开发和支持。记得在1998年从本部过来的前沿技术布道士(Evangelist)Norman Koo(其实他是Sun Tiger 计划的发起人)对我们说,搜索引擎和电子商务将会带来巨大变革和机遇。我们开发了一个网上商店的Java参考模型应用(Sell Any),当时阿里巴巴的淘宝网前身1688.com刚刚问世。和像郭屹及其他同事在Sun TDC共事的那段时光,是美好和快乐的。我们当时个个意气风发,对技术和工作充满热情。其中,郭屹的儒雅的国学造诣和真诚谦逊的为人和作风,一直为我所钦佩。从他的这个专栏中,可以对他的为人和作风窥见一斑。在Java发展了24年后还来写一本Java语言的书,是需要很大的勇气的。如果没有他对技术的不变初心和一丝不苟的作风,是很难写成这本书的。这本书通过讲座形式,就Java语言的一些核心特点以大量的实例进行一一阐述。它不光对Java语言的新手提供极大的帮助,对于有经验的Java高手也有很大的参考价值。郭屹在书中插入了很多的国学名句和经典,不时地将读者带入了不同的境界,从而增加了生动性和趣味性。

Sean Jiang

2019.05.22 于多伦多

Sean Jiang,于1997年4月至1999年12月在Sun TDC 和武春雷一起担任Java 团队的技术领队。2000年至今移居加拿大多伦多,一直从事软件产品和企业应用开发和构架的工作。现在就职于银行从事实时支付的构架工作。


每年技术媒体都会评选最受欢迎的编程语言,Java总是高居前位。自然,没有办法说一个语言绝对比另一个语言好,这个话题一如既往地会引起大家无谓的争论不休。对别的行业的人来讲也许会觉得莫名其妙,但是对我们程序员来讲,捍卫某种语言是一件很动感情的事情。就跟捍卫自己的母校一样,只能自己说不好,别人不能说不好的。

Java从正式出生以来(1995.05.23),已经过了24年时间了,它现在仍在全世界广泛被使用,拥有大量程序员和工作机会,这一个事实毫无疑义地证明了Java的成功。

但是,大家切记的是,讨论Java,不能当它只是一门编程语言,它还是一个平台,同时是一个生态。我20多年前在Sun公司技术研发中心的时候,工作任务之一就是告诉大家Java不仅仅是一门语言。我们的技术总监K.J.Gao曾经说,如果一定要说Java是一门编程语言,可以说“Java是服务端的语言”。我把这个历史说出来,就是让大家在学习语言的时候记住Java有其更加广阔的空间。

不过,本专栏又确实主要是从语言本身的层面介绍Java。针对的对象是初步了解Java语言的程序员,如本专业的大学毕业生或者是从事编程工作一年左右的程序员们,希望通过本专栏能对Java语言有一个进阶的理解。

我本人曾经身在Sun Java团队之中,虽然没有为Java发展做出什么贡献,没有开辟新大陆,但就像是一个跟随哥伦布船队的小船员,也算是见证了一段历史,而对新大陆热爱的心却一直在澎拜。希望这个专栏为Java布道,对后来的年轻人能有所帮助。

就跟世界上任何自然语言一样,人为发明约定的编程语言也是不断与时皆进的。从一个婴儿慢慢长大成人。

我们来看看Java的成长历史。

1990年,Sun公司启动“Green计划”,旨在开发智能家电的嵌入式控制系统。

1991年,Green项目组发现家用设备太多样化,C++移植过于烦琐,无法统一编程,James Gosling决定开发一种新的可移植语言,开头想叫C-,后来命名为Oak。名字来自于办公室窗口的一棵树。

1994年,Gosling将Oak更名为Java,这个名字来自于工作间隙Gosling去饮水机冲咖啡时候的一闪念。接着团队完成JVM实现,完成第一个Java编译器,完成Java实现的浏览器WebRunner(后来的HotJava)。这个团队总共3个人。

这就是创世时刻的样子。

1995年5月23日,Sun公司正式发布Java语言,这一天是Java的生日。

1996年,JDK1.0发布,这是个重要的里程碑,标志着它成为一种独立的开发工具。在1996年5月底,Sun公司于美国旧金山举行了首届JavaOne大会,这成为历史上传统的经典盛会。

1998年12月,第二代Java平台的企业版J2EE发布。

1999年6月,Java 2被分成3个版本:J2SE(标准版)、J2EE(企业版)和J2ME(微型版),这是又一个重要的里程碑,标志着Java的应用开始普及。

2001年9月24日,J2EE 1.3发布。

2002年2月,J2SE 1.4发布,各种开源框架大量出现。

2004年9月30日18:00,J2SE 1.5发布,成为Java语言发展史上的又一里程碑。

2005年6月,Java SE 6.0发布,3个版本分别改为:Java SE、Java EE、Java ME。

2009年4月20日,Oracle以74亿美元收购Sun公司,并取得了Java的版权。

2011年7月,Java SE 7发布。

2014年3月,Java SE 8发布。

2018年9月,发布Java 11。

1996年,JDK 1.0主要包括的特性:Applet、AWT等。在网页上动态变换的小动画震惊了整个Internet技术圈。

1997年,JDK 1.1引入了一些后来常用的技术点,如JAR文件格式、JDBC、JavaBeans、RMI、Inner Class和Reflection。

1998年12月4日,史诗巨献般的JDK 1.2发布,包含的主要技术有EJB、Java Plug-in、Java IDL、Swing等以及后来常用的Collection容器类。

2000年中,JDK 1.3发布,主要改进和引入了一些类库上,如数学运算和新的Timer API、JNDI服务、CORBA IIOP、Java 2D。

2002年,JDK 1.4发布,标志着Java的成熟,引入了一些新技术特性,如正则表达式、NIO、日志类、XML解析器等。

2004年,JDK 1.5发布,改进包括自动装箱、泛型、动态注解、枚举、可变长参数、foreach循环,还提供了java.util.concurrent并发包等。

2006年12月11日,JDK 1.6发布,并改用Java SE 6、Java EE 6、Java ME 6的命名方式。重大的改进包括动态语言支持、提供编译API等。虚拟机层面,包括锁与同步、垃圾收集、类加载等方面的算法都有很多改动。

后来,由于经济危机和收购案,Java发展遇到诸多挫折,Java 7难产。

2011年Oracle发布的Java 7采用了B计划,没有按照原先的设计推出。重要的改进包括动态类型语言在 JVM 上的执行效率得到大幅提升;G1 垃圾回收器性能提升, 主要用于 Server 端;核心类库改进,如ClassLoader、URLClassLoader、Concurrent、i18n等。

2014年,发布Java 8,主要的改进是终于引入了Lambda表达式。

2018年,发布Java 11,主要是增强如下功能:本地变量类型推断,集合加强,Optional 加强,HTTP Client API。化繁为简,一个命令编译运行源代码。这是最近的一个LTS,将支持到2026年。

Java是一个划时代的跨平台产品,这个最基础、最根本的设计初衷已经完全达到;

Java衍生出来的框架及其生态是世界上最成功的技术平台之一,这一点超出了设计初衷。

Java最功败垂成的设计是EJB,这是Java企业版本中最重大、最着力的技术革新,由于设计和实现的复杂,推广困难,被悉尼大学的Rod Johnson这个音乐学博士单枪匹马挑下马,最后完败于轻量级框架。

Java是Sun公司及James Gosling对人类的伟大贡献,而没有获取到什么利益。

本专栏我打算讲以下一些主题:

编程,不仅仅是学,更重要的是做。说到底它是一个手艺活。曾经有好些入门的年轻人问我如何编得一手好程序?我的回答总是“无他无他,惟手熟尔。”要想提高编程水平,唯一的方法就是动手去写。只要结合“眼”“脑”“手”者,不断学,不断想,不断做,就能大有成效。坚持两三年,迈上一个崭新的台阶,到了那时,定会体会到“虽人不我知,而胸中自有沟壑”的美妙境界。

荀子云:学不可以已。


在编Java程序的时候,我们经常会碰到注解。比如:

@Override 我们在写子类继承父类的时候,会经常用到这个注解。它告诉编译器这个方法是要覆盖父类的方法的。

@WebServlet("/myservlet") 在进行Web程序开发的时候,我们用这个注解表示这个类是一个servlet。Web容器会识别这个注解,在运行的时候调用它。

很多人说注解是注释,初看起来有一点像,它对程序的编写和编译似乎没有什么影响,只是给人看的一个对程序的附注。从这点上,确实有一点像注释。不过,它跟注释不同的是,它会影响程序的运行。比如,上面提到的@Override,如果实现的时候没有覆盖父类的方法,编译器会给出错误提示;再比如,上面的@WebServlet,如果没有这个注解,程序是运行不起来的。

由此看来,注解并不是注释,注释是给人看的,并不影响程序的编译和运行时候的行为。注解其实不是给人看的,那么它是给谁看的呢?它被设计出来,是用于给另外的程序看的,比如编译器,比如框架,比如Web容器。

这些外在的程序通过某种方式查看到这些注解后,就可以采取相应的行为。下面我具体解释一下。

假如我们要做一个Web容器,类似于Tomcat这种的。它的一个基本功能就是加载servlet。按照Java EE的规范,容器需要管理servlet的生命周期,第一件事情就是要识别哪些类是servlet。那么,容器启动的时候,可以扫描全部类,找到包含@WebServlet注解的,识别它们,然后加载它们。那么,这个@WebServlet注解就是在运行时起作用的,Java里面把它的作用范围规定为RUNTIME。

再看@Override,这个是给编译器看的。编译程序读用户程序的源代码,识别出有@Override注解的方法,就去检查上层父类相应方法。这个@Override注解就是在编译的时候起作用的,编译之后,就不存在了。Java里面把它的作用范围规定为SOURCE。

类似的注解还有@Test,程序员写好了程序,想交给测试框架去测试自己写的方法,就可以用这个注解。测试框架会读取源代码,识别出有@Test注解的方法,然后生成测试代码就可以进行测试了。

平时,我们可能是用别人提供的注解来工作。注解的使用其实相当简单,我不打算在专栏中特意讲解,因为这是一个进阶的讲座。我们要研习的东西更加深一点。

接下来,我们自己动手做一个注解看看效果加深理解。

我们想做的例子是一个运行时框架加载别的客户类,并运行其中的初始化方法。作为框架,我们可以提供一个@InitMethod注解给客户程序员。客户类代码如下:

public class InitDemo {
    @InitMethod
    public void init(){
        System.out.println("init ...");
    }
}

客户类程序员在init()方法上标注了@InitMethod注解,声明这就是本类的初始化方法。框架程序利用这个注解识别它,并调用它。

接下来我们看怎么提供这个注解的实现。代码如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InitMethod {
}

第一次看到这个注解的实现的时候,人们都会大吃一惊,觉得很像是在定义一个接口。的确是很像,Java 5之后,提供了这样的手段,让人定义注解。上面就声明了有一个叫InitMethod的注解,它是修饰方法的,在运行时可见。

问题来了,上面这一段并不是真正的实现,只是一个定义或者声明。那真正的实现怎么做呢?这些当然要定义者来提供实现的。我们作为框架程序的作者,既然提供了这个注解,就有责任实现它,代码如下:

public class InitProcessor {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,IllegalArgumentException,InvocationTargetException, InstantiationException {
        Class<?> clazz = Class.forName("InitDemo");
        Method[] methods = clazz.getMethods();
        if(methods!=null){
            for(Method method:methods){
                boolean isInitMethod = method.isAnnotationPresent(InitMethod.class);
                if(isInitMethod){
                    method.invoke(clazz.newInstance(), null);
                }
            }
        }
    }
}

让我稍微解释一下上面的代码。

为了从客户类InitDemo里面读出注解信息,需要用到反射机制。先通过Class.forName()加载类拿到Class信息;然后通过getMethods()拿到所有public的方法(包含从上层父类继承下来的公共方法);接下来是重点:method.isAnnotationPresent(InitMethod.class),这一行判断一个方法是否标记为InitMethod;如果是,则创建一个对象并调用。这样在框架中实现了对类的初始化方法进行调用。

运行上面的程序,就能看到确实调用了初始化方法。我们定义的注解工作了。

注解基本的使用就是这样的,一点也不神秘。

下面介绍更多的一些特性。注解的基本定义如下:

@Target(ElementType.xxxxxx)
@Retention(RetentionPolicy.xxxxxx)
[Access Specifier] @interface <AnnotationName> {         
    DataType <Method Name>() [default value];
}

注解里面的Method其实是客户程序在使用该注解的时候的参数定义,如@WebServlet(urlPatterns=”/abc”,loadOnStartup=1),其中urlPatterns和loadOnStartup就是参数,定义的时候用类似于方法定义的方式。如:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface WebServlet {         
     String urlPatterns() default "/";  
     int loadOnStartup() default 0; 
}

这种定义方式也有点奇怪,对使用者有些费解。不过从实现者的角度,倒是比较正常。因为实现注解的程序员是这么写的,代码如下:

Annotation annotation=clazz.getAnnotation(WebServlet.class);
WebServlet ws = (WebServlet)annotation;
System.out.println(ws.loadOnStartup());

这一下子就可以看出来了,实现者确实是把它当成方法调用的。

@Target指定该注解针对的地方,有几种ElementType:
    ElementType.TYPE,               - 类、接口 
    ElementType.FIELD,              - 字段
    ElementType.METHOD,             - 方法
    ElementType.PARAMETER,          - 参数
    ElementType.CONSTRUCTOR,        - 构造方法
    ElementType.LOCAL_VARIABLE,     - 局部变量
    ElementType.ANNOTATION_TYPE,    - 注解
    ElementType.PACKAGE             - 包

@Retention指定注解的保留域,有3种RetentionPolicy:

RetentionPolicy.SOURCE,           - 注解信息仅存在于源代码级别,由编译器处理,处理完之后就不保留了
RetentionPolicy.CLASS,             - 注解信息保留于类对应的class文件中
RetentionPolicy.RUNTIME          - 注解信息保留于class文件中,并且可由JVM读入,运行时使用

SOURCE选项常用于代码自动生成。但是需要注意Spring中常见的@Autowired注解,可以用来省去类成员变量的set 和get方法,却不是SOURCE级别的。

RetentionPolicy里的CLASS选项让人不好理解。它的含义是说在编译的CLASS文件中记录了这个注解,但是JVM获取不到。那这有什么用处?这确实是一种很少见的应用场景,比如一些直接通过字节码方式运行的程序,如ASM,它直接读CLASS字节码,并不通过JVM去装载类,这个情况下就需要用到这个选项。不过有的编译器会把CLASS处理成RUNTIME,通过反射一样可见,因此有人宣称两者一样的,事实上这不符合Java官方文档说明。

我们普通应用程序的开发工作中,CLASS选项用得极少,一般情况下,就用RUNTIME选项。

讲解了这些之后,我们可以把上面的例子完整写下来。

InitMethod.java
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.RetentionPolicy;

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface InitMethod {
        String flag() default "1";
}

InitDemo.java
    public class InitDemo {
        @InitMethod(flag="1")
        public void init(){
            System.out.println("init ...");
        }
}

InitProcessor.java
    import java.lang.annotation.Annotation;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;

    public class InitProcessor{
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
            Class<?> clazz = Class.forName("InitDemo");
            Method[] methods = clazz.getMethods();
            if(methods!=null){
                for(Method method:methods){
                    boolean isInit = method.isAnnotationPresent(InitMethod.class);
                    if(isInit){
                        method.invoke(clazz.newInstance(), null);
                        Annotation annotation=method.getAnnotation(InitMethod.class);
                        InitMethod im = (InitMethod)annotation;
                        System.out.println(im.flag());
                    }
                }
            }
        }
    }

上面的InitProcessor程序相当于一个框架,对客户程序进行管理并自动调用初始化方法。而站在客户化程序员的角度,就是要遵守某种协议规范,写的程序就能按照预想的正确运行起来,简单易用,自己少写很多代码。

不过“一物有二柄”,凡事都有一个优缺点。这么写程序,对客户程序员来讲,获得简单的同时,却丢失了全局观。很多程序员都不知道这些程序究竟是怎么运行起来的,有点失控的感觉。这也是学习框架过程中最经常的困惑。

当代编程的范式,已经全部转换成基于框架的编程了。不少程序员面临着知其然而不知其所以然的问题,懵懵懂懂编了几年程序也没有甚解。这种情况下,为了真正理解程序结构,自己动手实现某种框架机制就显得格外重要。

下面,为了加深对注解的了解,我们再试着编写一个自动生成代码的例子。

我们想做这么一件事情,模仿JUnit,应用程序员写了一个类,我们自动生成测试类。这个就可以使用到作用于SOURCE级别的注解来实现。

我们先提供一个@UnitTest注解给客户程序员使用。这个注解的作用就是自动生成一个测试类,把客户程序里面的方法都调用一次。

同样的,我们先定义注解,代码如下(UnitTest.java):

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface UnitTest {
  String prefix() default "Test";
}

这个定义没有什么特别的,跟上述讲解的不同在于使用了RetentionPolicy.SOURCE,表示这个注解作用于源代码层面,在编译时使用。

使用层面就更加简单了,代码如下(Tool.java):

@UnitTest
public class Tool {
    public void check(){
    System.out.println("check");
    }
}

根据规定,对这类注解的实现类要继承AbstractProcessor抽象类,这个抽象类是由javax里面给出的,所以需要先import javax.annotation.processing.AbstractProcessor;

在类里面,主要是要覆盖一个方法:process(),好,我们直接看代码(UnitTestProcessor.java):

import java.io.IOException;
import java.io.Writer;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.tools.JavaFileObject;
import javax.lang.model.SourceVersion;

@SupportedAnnotationTypes({"UnitTest"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class UnitTestProcessor extends AbstractProcessor{
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for(Element clz: roundEnv.getElementsAnnotatedWith(UnitTest.class)){
          UnitTest ut = clz.getAnnotation(UnitTest.class);

          String newFileStr = "public class "+ut.prefix() + clz.getSimpleName()+" {\n\n";

          //add constructor
          newFileStr += "public " + ut.prefix() + clz.getSimpleName() + "() {\n";
          newFileStr+="}\n\n"; //end of constructor

          //add main()
          newFileStr += "public static void main(String[] args) {\n";

          //new instance
          newFileStr += clz.getSimpleName() + " clz = new "+clz.getSimpleName()+"();\n";

          //add test method
          for(Element testmethod : clz.getEnclosedElements()){ 
            if(!testmethod.getSimpleName().toString().equals("<init>") && testmethod.asType() instanceof ExecutableType){ //add test method
              newFileStr += "clz."+testmethod.getSimpleName()+"();\n";
            }
          }

          newFileStr+="}\n"; //end of main()
          newFileStr+="}\n"; //end of class

          try {
            JavaFileObject jfo = processingEnv.getFiler().createSourceFile(ut.prefix() + clz.getSimpleName(), clz);
            Writer writer = jfo.openWriter();
            writer.append(newFileStr);
            writer.flush();
            writer.close();
          } catch (IOException ex) {
          }
        }

        return true;
      }

   }

下面把这个程序解释一下。

Process()带两个参数:一个是本轮要处理的注解集合,另一个是本来相关的环境参数。

原理上process()的主体是对集合中的每一个注解进行处理。处理之后新生成的一些程序或许还会包含有同样的注解,就需要递归处理,所以有一个round的概念,需要一轮一轮处理完毕。不过,我们这个是教学,只是为了演示,我们就简化处理。

通过roundEnv.getElementsAnnotatedWith(UnitTest.class),我们拿到包含有UnitTest注解的全部类,因为@UnitTest是作用于类之上的。

通过clz.getAnnotation(UnitTest.class),我们可以把类上的注解拿到,然后从注解中获取信息。

我们的任务比较简单,只是要生成一个测试类,所以我们用newFileStr拼字符串,写这个文件。先写类定义:

newFileStr += "public class "+ut.prefix() + clz.getSimpleName()+" {\n\n"

如果类的名字叫Tool,那么我们自动生成的测试类就叫TestTool。

然后定义构造函数

newFileStr += "public " + ut.prefix() + clz.getSimpleName() + "() {\n"

再写main(),作为测试类的入口     

newFileStr += "public static void main(String[] args) {\n"

main()里面的结构,就是新建对象,然后调用方法     

newFileStr += clz.getSimpleName() + " clz = new "+clz.getSimpleName()+"();\n";

检查类里面的每一个元素,挑出普通方法,进行调用

for(Element testmethod : clz.getEnclosedElements()){ 
   if(!testmethod.getSimpleName().toString().equals("<init>") && testmethod.asType() instanceof ExecutableType){ //add test method
      newFileStr += "clz."+testmethod.getSimpleName()+"();\n";
   }
}

程序片段中,clz.getEnclosedElements()用来找出类里面包含的元素,包括构造函数、方法、属性定义等。我们简化处理,只是处理了构造函数。

程序字符串准备好之后,就直接写文件:

JavaFileObject jfo = processingEnv.getFiler().createSourceFile(ut.prefix() + clz.getSimpleName(), clz);
Writer writer = jfo.openWriter();
writer.append(newFileStr);

注:

自定义注解不困难。不过在build的时候要费一点工夫。因为这个是影响编译的行为,所以要向编译器javac声明:

先编译好UnitTestProcessor,然后javac -processor UnitTestProcessor Tool.java。

如果是IDE环境,就要进行配置,如果在eclipse中,则要在Project Properties ->Java Compiler->Annotation Processing中启用annotation processing,并在Factory Path中把上述编译好的处理器Jar包登记进来,并选择run这个jar包中包含的哪个processor.

因此,我们就要先打一个jar包。新建一个处理器工程,包含UnitTest.java, UnitTestProcessor.java, 还要准备一个meta文件,在工程的resources目录下建一个文件:META-INF/services/javax.annotation.processing.Processor,文件中写上处理器的名字:UnitTestProcessor。这一套做法是Java的JAR规范中规定的。

这个独立的jar包的作用就是为客户程序员自动生成测试类代码,这是一个有用的工具包。

有了这个jar包之后,在客户工程里面引入即可。

Build项目的时候,自动生成了测试类,代码如下(TestTool.java):

public class TestTool {
public TestTool() {
}
public static void main(String[] args) {
Tool clz = new Tool();
clz.check();
}
}

大功告成。

到此,我们就能看到,用了注解技术,我们能做很多非平凡的工作。以后自己写工具,写框架,就会有一些头绪了。

作为学习者,最先了解的是注解的概念,学习使用现成的注解,这是第一步;接下来就要自己写RUNTIME类型的注解,实现一些框架的效果;进一步就是自己写SOURCE类型的注解,提供各种源代码级别的工具。学习的进路,就这么一步步深入下去。掌握了后,就有拨开丛林,见到本尊的愉悦,体会获得知识的愉悦感。

耳边总是传来“不要重新造轮子”的声音,但是,对于学习者,就应该重新造轮子。只有在学习重新造轮子的过程中,我们才能更加深刻理解技术概念。


相关图书

Effective Java中文版(原书第3版)
Effective Java中文版(原书第3版)
Java核心技术速学版(第3版)
Java核心技术速学版(第3版)
Java编程动手学
Java编程动手学
Java研发自测入门与进阶
Java研发自测入门与进阶
Java开发坑点解析:从根因分析到最佳实践
Java开发坑点解析:从根因分析到最佳实践
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)

相关文章

相关课程