ava是一门功能强大的多用途编程语言,也是全球最流行的开发语言之一。它是面向对象编程语言的代表,集跨平台、健壮性、高性能等诸多优点,广泛应用于Web后端开发、移动端开发、大数据分析、人工智能等热门领域,在互联网行业占据十分重要的地位。目前,全球有超过500万的专业开发者在使用Java语言,Java程序运行在全球数十亿台设备上。作为全书开篇,本章将对Java概述、开发环境搭建、程序开发步骤、虚拟机与垃圾回收、开发工具等内容进行讲解,带领带领大家进入Java世界。
Java是一种高级计算机语言,它是由Sun公司(2009年4月20日被Oracle公司收购,2010年完成合并)于1995年5月推出的一种用来编写跨平台应用软件、完全面向对象的程序设计语言。Java语言简单易用、安全可靠,自从问世以后,受到了市场的大力追捧。在PC、移动设备、家用电器等领域,Java技术无处不在。
Sun公司在1995年推出Java语言以后,吸引了编程世界的广泛关注。那么,Java到底有什么魔力呢?1990年末,Sun公司预测嵌入式系统将会在家电领域大显身手,于是在1991年6月启动了“Green计划”,由詹姆斯·高斯林(James Gosling)、迈克·谢里丹(Mike Sheridan)等人带领的开发团队负责,准备开发一种能够在各种消费性电子产品(如机顶盒、冰箱、收音机等)上运行的程序架构,以便于人们与家用电器进行信息交流与控制。因为家用电器的计算处理能力和内存都非常有限,所以要求语言必须非常小且能够生成非常紧凑的代码,这样才能在这样的环境中执行。另外,不同的家用电器使用的CPU不同,因此要求该语言必须是跨平台的。开发团队最初考虑使用C++语言,但是C++太过复杂,以致很多开发者经常错误使用,而且项目面向的是嵌入式平台,可用的系统资源十分有限,所以Sun公司创始人之一的比尔·乔伊(Bill Joy)决定开发一种新语言,他提议在C++的基础上开发一种面向对象的环境。Java便由此而问世,詹姆斯•高斯林最初将其命名为Oak(橡树)。遗憾的是,当时由于这门语言只能为家用电器提供一个通用环境,且受到诸多因素的限制,Oak语言没有得到迅速推广。1994年夏天,随着Innernet的迅猛发展,浏览器的出现,枯燥乏味的信息文档已经不满足人们的需求,这给Oak语言带来了新的生机。詹姆斯·高斯林立刻意识到这是一个机会,于是对Oak进行了小规模的改造。之后,开发团队的其他成员完成了第一个基于Oak语言的网页浏览器WebRunner,从而让浏览器具有了在网页中执行内嵌代码的能力,可以创造含有动态内容的网页。1995年,Sun公司将Oak更名为Java,并将其直接发布在互联网上,免费开源给大家使用,获得了广大开发人员的青睐。之后,Java开始走红,成为一门广为人知的编程语言,被用于开发Web应用程序。
Java一开始具有吸引力,是因为Java程序可以在Web浏览器中运行,随着Internet普及和迅猛发展,以及Web技术的不断更新,Java语言与时俱进、推陈出新,使Java语言在现在社会经济发展和科学研究中,占据了越来越重要的地位。在最流行的语言流行指数Tiobe,RedMonk和PyPL中均长期排名前三,且多年是Tiobe排行榜中排名第一的语言。从手机软件到企业级应用、从无人驾驶汽车到线上支付、从Minecraft(我的世界)游戏娱乐到火星探测器太空探索,Java语言的使用场景非常广泛。现在,Java广泛应用于开发服务器端的应用程序,截止到2021年,Java开发占据了服务器端后台开发80%以上的市场份额。
Java语言目前主要应用于如下领域:
从Java编程语言本身角度来讲,其严谨的结构,易懂的语法加上简易的编写为其之后的发展及革新提供了良好的保障。
注意:Java是印度尼西亚爪哇岛的英文名称,因盛产咖啡而闻名。
Java语言是一门跨平台的适用于移动端、服务器领域、分布式环境的面向对象程序设计语言,它之所以能从众多编程语言中脱颖而出,成为最流行的服务端开发语言之一,是因为具备如下显著特点:
想一想:你了解哪些语言?Java语言在众多编程中脱颖而出的原因有哪些?
Java开发工具包(JavaSE Development Kits,简称JDK)是一套独立程序构成的集合,用于开发和测试Java程序,是Java程序开发的首要工具。
JDK由Java API、Java工具和Java基础的类库等组成,其核心是Java API,API(Application Programming Interface,应用程序编程接口)是Java提供的供编程人员使用的标准类库,开发人员可以用这些类库中的类来实现Java程序的各种功能,从而免去自行设计很多常用类的繁重工作,极大地提高开发效率。另外,Java API还包括一些重要的语言结构以及基本图形、网络和文件I/O等。
本书中使用的是JDK15版本,与之前的版本相比,JDK 15 为用户提供了14项主要的增强(JEP),同时新增了1个孵化器模块、3个预览功能、2个不推荐使用的功能,并删除了2个淘汰的功能。
知识点拨:增强(JEP)、孵化器模块(Incubator)和预览特性(Preview)的具体含义:
增强:英文全称为JDK Enhancement Proposals ,简称 JEP,是JDK 增强建议,主要包括新增特性和改进提案。
孵化器:实际上就是实验版,主要从Java社区收集意见、反馈,稳定性差,后期可能有比较大的变动,称之为尚未定稿的API/工具。
预览特性:规格已经成型,实现基本确定,但是最终未定稿,这些特性,还可能被移除,但可能性比较小,一般都会定下来。
编写Java程序,首先要下载JDK安装程序,读者可以直接从Oracle公司的官方网站下载。
通过浏览器打开Oracle官网(http://www.oracle.com/technetwork/java/javase/ downloads/index.html),根据提示进入下载页面,找到与自己的计算机操作系统对应的JDK安装文件下载链接,点击下载即可。网页内容可能因版本或Oracle公司规划而有所不同,用户可以根据需要选择所需要的JDK版本。
JDK安装文件下载成功后,就可以安装了。本书使用的是64位的Windows10环境,接下来详细演示Windows 64位平台下JDK15的安装过程,具体步骤如下:
(1)双击从Oracle官网下载的JDK安装文件,进入JDK安装界面,如图1.1所示。
(2)单击图1.1中的“下一步”按钮,进入到JDK自定义安装界面,如图1.2所示。
图1.1 JDK安装界面 图1.2 JDK默认安装路径
(3)建议选择直接安装到默认目录,单击“下一步”按钮即可进行安装,如图 1.3所示。也可以点击“更改”按钮,自行选择安装目录。
(4)安装完毕之后,弹出如图1.4所示的界面,点击“关闭”按钮即可。
图1.3 等待安装界面 图1.4 安装完毕界面
在使用Java来编译和运行程序之前,必须先设置好环境变量。所谓环境变量,就是在操作系统中定义的变量,可供操作系统上的所有应用程序使用。Path环境变量的作用是设置一个路径,由操作系统去寻找该路径下的文件(如.bat、.ext、.com等),对Java来说就是Java的安装路径。
下面以Windows 10操作系统为例说明。具体步骤如下:
(1)选择“控制面板”→“系统和安全”→“系统”(也可以在桌面上右键单击“此电脑”或“我的电脑”,选择“属性”选项),进入到系统窗口,如图1.5所示。
单击“高级系统设置”
图1.5 Windows 10系统窗口界面
(2)单击“高级系统设置”选项,弹出“系统属性”窗口,如图1.6所示。
(3)单击“环境变量(N)…”按钮,弹出 “环境变量”窗口,如图1.7所示。
图1.6 “系统属性”窗口界面 图1.7 “环境变量”窗口界面
(4)在“环境变量”窗口的“系统变量(S)”区域中,单击“新建(W)…”按钮,打开“新建系统变量”窗口。并在“变量名(N)”文本框中输入“JAVA_HOME”,在“变量值(V)”文本框中输入JDK安装目录。笔者此时的安装目录为“C:\Program Files\Java\jdk-15”,如图1.8所示。单击“确定”按钮,完成JAVA_HOME环境变量的配置。
图1.8 “新建系统变量”窗口界面
(5)在“环境变量”窗口的“系统变量(S)”区域中,选中系统变量“Path”,如图1.9所示。
图1.9 “环境变量”窗口需按照Path变量界面
(6)在图1.9所示的界面单击“编辑(I)…”按钮,打开“编辑环境变量”窗口,点击“新建(N)”按钮,在编辑页面的文本框中添加“%JAVA_HOME%\bin”,如图1.10所示。然后,单击窗口的“确定”按钮,保存环境变量,完成配置。
图1.10 “编辑系统环境变量”窗口界面
注意:在配置Path环境变量时,JAVA_HOME环境变量并不是一定需要配置的,我们也可以直接将JDK的安装路径(C:\Program Files\Java\JDK-15\bin;)添加到Path环境变量中。这里配置JAVA_HOME的好处是,当JDK的版本或安装路径发生变化时,只需要修改JAVA_HOME的值,而不用修改Path环境变量的值。
个别图书中会提到Classpath环境变量,Classpath环境变量的作用与Path环境变量的作用类似,它是JVM执行Java程序时搜索类的路径的顺序,以最先找到为准,JDK1.5之后,如果没有设置Classpath环境变量,则Java解释器会在当前路径下搜索Java类,故本书不再赘述。
JDK配置完成后,需要测试JDK是否能够在计算机上运行,具体步骤如下:
(1)同时按下键盘 Window键和R键,调出Dos命令行运行窗口,在搜索框输入cmd,如图 1.11所示。
(2)单击“确定”按钮,进入命令行窗口,如图1.12所示。
图1.11 运行窗口 图1.12 命令行窗口
(3)在命令行窗口中输入“javac”命令,并按Enter键,系统会输出javac的帮助信息,如图1.13所示,说明JDK已经成功配置,否则需要仔细检查JDK环境变量的配置是否正确。
图1.13 命令行信息
JDK安装成功后,系统会自动在我们的安装目录下生成一个目录,称为JDK目录,如图 1.14所示,我们必须熟悉JDK目录各个文件夹的作用才能更好的学习与编写代码。
图1.14 JDK目录
接下来,简单介绍一下JDK目录及其子目录的含义和作用:
注意:自JDK9 以后,就取消了目录中的jre目录,将之前jre目录里面的内容分散到其他各个目录了。
在我们为电脑配好Java开发环境以后,也就代表着我们可以开始实现我们的Java开发之旅了,现在我们自己来动手编写一个Java程序,亲自感受一下Java语言的编写规范。
下面将编写第一个Java程序,其功能是控制台输出“有梦想,一起实现!”,如例1-1所示。
例1-1 HelloDream.java
在开始编写代码之前,先在电脑D盘(本书使用D盘)中创建一个新的目录及子目录:“d:\javaCode\demo01”,然后在demo01下创建一个文本文件,并重命名为“HelloDream.java”,使用记事本打开,编写如下程序代码:
例1-1是程序的源代码,下面针对逐条语句进行详细的讲解,如图1.15所示。
class是Java关键字,用来声明该文件是一个类。类作为程序的基本单元存在,所有Java代码必须写在类里面
我们自己定义的类名,方便识别,类名必须与文件名一致
输出语句,把我们要展示的内容在控制台输出来
这句声明了一个main方法,作为Java程序的执行入口,需要执行的代码都要写在main方法的大括号内
图1.15 记事本编写的java代码
注意:在编写Java代码时,所有的符号必须用英文半角格式,不允许出现中文字符。
编写好的Java代码文件需要编译成Java字节码文件(class文件)才能运行,Java程序的编译步骤如下:
接下来打开Dos命令行窗口,并按下面步骤来编译和运行HelloDream.java。
(1)打开Dos命令行窗口,先将路径切换到需要编译代码的位置,即在窗口依次输入“d:\javaCode\demo01”和“d:”命令,如图1.16所示。
(2)切换好磁盘路径之后,在命令行窗口输入“javac HelloDream.java”命令,对源文件进行编译,如图1.17所示。
图1.16 切换磁盘目录 图1.17 编译Java源文件
(3)在编译成功后会发现同级目录下多了一个名为“HelloDream.class”的文件,这个文件就是字节码文件,如图 1.18所示。
编译生成的字节码文件
图1.18 命令行编译后的文件目录
编程技巧:在进行编译的时候,要写文件全名加后缀名。javac编译utf-8编码的java文件时,容易出现“错误: 编码GBK的不可映射字符”。解决方法是添加encoding 参数:javac -encoding utf-8 WordCount.java,如果还不能解决,将文件保存成ANSI编码格式。
编译完成之后,就可以运行程序。在Dos命令行窗口接着输入“java HelloDream”命令,运行刚才已经编译好的java文件,运行结果如图 1.19所示。
注意:在运行的时候,输入的是文件的全名,不加后缀名。
通过前面的学习,我们知道了一个Java程序需要经过编写、编译、运行3阶段,而且细心的同学会发现,在编译的时候我们用的是javac命令,而在运行的时候我们用的是java命令,这在一定程度上给大家带来了不少麻烦。好在,JDK9之后,Java程序的编译运行进行了改动,变得更加简便,不需要再使用javac命令对java文件进行编译后运行,而是直接使用java命令对java文件进行编译运行。
接下来,我们将“d:\javaCode\demo01”目录下编译后的“HelloDrea.class”字节码文件删除掉,按照简化后方法重新编译运行HelloDream.java程序,如图1.20所示。
图1.19 运行Java程序 图1.20 Java命令编译运行
通过图1.20可以看到,我们只需要使用java命令就可以直接打印出java文件的输出结果。
java文件是高级语言代码,class文件是低级语言代码。编译过程实际上是通过Java编译器将高级语言的源代码编译为低级语言代码。那么反过来,是否可以通过低级语言代码进行反向工程,获取其源代码呢?答案是肯定得,这个过程就叫做反编译。虽然,机器语言很难反编译为源代码,但是中间代码是可以进行反编译的,比如用户可以把javac编译得到的class文件进行反编译,将其转换为java文件。通过反编译,我们可以了解别人的代码内容,学习别人的代码的实现思路,还可以通过源代码查找bug、制作外挂等。
Java中有很多反编译工具,最常用的有如下几种:
前面我们学习了Java程序的编写、编译与运行过程,那么Java程序在计算机中运行的底层原理是什么呢?它是如何实现跨平台的呢?它在运行过程中又是如何使用计算机内存的呢?接下来,我们来学习Java虚拟机与垃圾回收机制。
Java虚拟机(Java Virtual Machine ,JVM)是运行Java程序必不可少的机制。Oracle的Java虚拟机规范给出了JVM的定义:JVM是在一台真实的机器上用软件方式实现的一台假象机。虚拟机的代码存储在.class文件中,并且每个.class文件最多包含一个public class类的代码。
Java程序经过编译器(javac.exe)编译之后,会产生与平台无关的字节码文件(即扩展名.class的文件)。字节码文件本质上是一种标准化的可移植的二进制格式,它最大的好处是可跨平台运行,也就是常说的“一次编译,到处运行”。字节码文件必须交由解释器来执行,与计算机硬件、操作系统没有关系,这个解释程序就是JVM。换句话说,无论使用哪种操作平台,只要其含有JVM,就可以运行字节码文件。事实上,正是有了Java虚拟机规范,才使得Java应用程序达到与平台无关,从而实现可移植性,这也是Java语言风靡全球、迅速普及的原因之一。
回顾之前之前学习的代码编译、运行过程,我们可以很容易地理解,JVM实现跨平台代码执行的过程如图1.21所示。
图1.21 JVM执行流程图
最后需要强调的是,JVM的实现包括字节码验证、解释器、内存垃圾回收等,JVM虚拟机规范对运行时数据区域的划分及字节码的优化并不做严格的限制,它们的实现依不同的平台而有所不同。
在传统的程序开发语言(C、C++及其他语言)中允许动态分配内存,同时需要程序开发人员负责内存资源的释放,如果不释放内存,则随着程序的不断运行,不断有新的资源需要分配内存,当系统中没有内存可用时程序就会崩溃;或者,已动态分配的堆内存由于某种原因未被程序释放或无法释放,也会造成系统内存的浪费。上述这些现象都被称为“内存漏洞”。
垃圾回收(Garbage Collection,GC)就是指释放垃圾对象所占用的空间,防止内存溢出。内存处理是让所有编程人员都很头疼的地方,如果忘记或者错误地回收内存会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象并判断是否超过作用域,从而确定是不是要回收对象。
在Java语言中,引入了垃圾回收机制,程序开发者在编写程序的时候无需考虑内存管理问题。Java提供了后台系统级线程,自动记录每次内存分配的情况,并统计每个内存地址的引用次数,不定时地对内存中没有被引用或者长时间没有使用的对象进行回收,这样回收的内存资源可以再次分配其他的内存申请。
垃圾回收能自动释放内存空间,使开发者可以将更多精力投入到软件核心功能设计之上,不需要主动去考虑内存漏洞的问题,极大地减轻了程序开发者编程的负担。同时,垃圾回收是Java语言安全性策略的一个重要部份,它能够有效保护程序的完整性。当然,Java的垃圾回收也有一个潜在的缺点,就是它的开销影响程序性能,Java虚拟机必须追踪运行程序中有用的对象,最终释放没用的对象,这个过程需要花费CPU的时间。
在第1.3节编写第一个Java程序时,我们使用的是记事本,这样编写程序比较辛苦且效率不高。那么,如何来提高编程效率呢?这就需要选择一款优秀的Java程序开发工具。
在Java的学习和开发过程中,离不开一款功能强大、使用简单、高效率的开发工具。程序开发工具又叫集成开发环境(IDE),是用于提供程序开发环境的应用程序,通常包括代码编辑器、编译器、调试器和图形用户界面等,这类软件一般集代码编写、分析、编译、调试为一体,可以极大程度地提高编程效率。目前,最流行的Java集成开发环境有Eclipse、InteliJ IDEA、NetBeans、jGRASP、BlueJ等。曾经,Eclipse是Java IDE中的王者,近些年其风头逐步被InteliJ IDEA所取代。
IntelliJ IDEA简称IDEA,是业界公认最好的Java开发工具之一,特别在智能代码助手、代码自动提示、重构、JavaEE支持、各类版本工具(git、svn等)、JUnit、CVS整合、代码分析、 创新的GUI设计等方面,其功能可以说是超常的。
接下来,我们就来介绍IDEA的下载、安装与启动方法(笔者写稿时使用了IDEA的2021版,读者可以直接在官网进行下载),具体步骤如下:
(1)通过网址( https://www.jetbrains.com/idea/)进入官网,如图1.22所示。
图1.22 IDEA官网下载界面
(2)点击“Download”进行下载,弹出下载界面,如图 1.23所示。IntelliJ IDEA 提供了两个版本,即 Ultimate(旗舰版)和 Community(社区版)。社区版是免费的,但它的功能较少。旗舰版是商业版,提供了一组出色的工具和特性。
图1.23 idea2021版下载界面
(3)点击“Download”后会弹出如图 1.24所示的注册界面,让我们进行注册,不用注册,这时候已经开始下载了。下载好安装包后将其放在合适的位置,等待安装即可。
已开始下载
图1.24 等待下载与注册界面
(4)双击下载好的安装包,弹出安装界面,如图 1.25所示。
(5)点击“Next>”按钮选择安装目录,一般选择默认即可,如果C盘空间不足可以选用其他盘符,如图1.26 所示。
图1.25 安装界面 图1.26 程序安装目录界面
(6)点击“Next >”按钮后,进入安装配置界面,勾选创建桌面快捷方式,本书使用的是64位操作系统,所以勾选 “64bit Launder”(用户请根据自己操作系统位数,自行选择),如图 1.27所示。
(7)点击“Next >”按钮,跳转至开始安装界面,如图1.28所示。点击“Install”按钮即跳转至等待安装界面,如图1.29所示。程序安装完毕界面如图1.30所示,点击“Finish”按钮即可。
图1.27 安装配置界面 图1.28 开始安装界面
图1.29 安装等待界面 图1.30 安装完毕界面
安装好IDEA之后,接下来就带领大家体验使用IDEA进行程序开发的过程,步骤如下:
(1)在桌面上找到IDEA的快捷方式,双击图标打开IDEA,进入“Welcome to IntelliJ IDEA”界面(IntelliJ IDEA旗舰版是商业收费软件,非付费用户首次登陆进入不到收费界面,但是该软件为学生提供了人性化的福利,学生凭个人学号可以获得免费使用权,具体根据官方提示操作即可),如图1.31所示。IDEA界面的默认颜色为黑色,默认状态下进入下一步完全可以,如果不喜欢该风格,可自行设置。在图1.31所示界面选择左侧“Customize”,切换到如图1.32所示的界面,再在右侧“Color theme” 下拉选项卡选择“IntelliJ Light”,背景色即可变为亮色,如图 1.33所示。在图1.33所示界面选择左侧“Projects”,即可再次回到欢迎界面,如图1.34所示。
图1.31 IDEA欢迎界面 图1.32 IDEA界面颜色设置
图1.33 界面颜色变为亮色 图1.34 调整颜色后的欢迎界面
(2)在欢迎界面点击“New Project”按钮后进入“New Project”界面,如图1.35所示。选择项目类型和版本号,当前选择Java项目,Project SDK是“15 version 15”。接下来单击“Next”按钮,进入如图1.36所示的界面,本界面用来设置是否使用模板开发,这里不用勾选。
图1.35 项目类型和版本 图1.36 是否使用模板
(3)点击“Next”按钮进入下一步,按照如图 1.37所示,输入项目名称、选择项目目录。
点击选择项目存放目录
项目存放的目录
项目名称
图1.37 设置项目名称和目录
(4)单击“Finish”按钮,就可以看见我们用IDEA创建的第一个项目,如图 1.38所示。
图1.38 创建好的项目
(5)在src目录下右键选择“New”→“Java Class”,如图1.39所示。点击“Java Class”以后在弹窗内填入类名,如图1.40所示。
图1.39 创建类
创建接口
默认创建类
自定义类名,首字母大写
图1.40 创建类过程中的弹窗
(6)在类中编写图1.41所示的代码,然后右键并点击“Run 'HelloWorld.main()'”运行代码(或在菜单选择“Run”→“Run 'HelloWorld'”,或者直接单击工具栏的
图标)。
图1.41 编写并运行Java代码
程序运行结果如下:
HelloWorld!
好好学习Java!
程序运行完成后,在IDEA打印运行结果,通过在IDEA编辑器中编写代码,代码编写效率、执行效率会更高。
注意:.idea目录用来存放项目的配置信息,包括历史记录,版本控制信息等内容。
1.1 Java的三大体系分别是______、______、______。
1.2 编译Java程序需要使用______命令。
1.3 Java代码编译后的字节码文件扩展名为________。
2.1 Java程序未经过编译时的文件的后缀是( )
A..dll B..exe
C..class D..java
2.2 用Java虚拟机编译类名为Hello的应用程序的正确命令是( )
A.javac Hello.class B.Hello.class
C.java Hello.java D.java Hello
2.3 下列选项中,属于Java语言特点的一项是( )
A.使用繁琐 B.编译执行
C.节省内存空间 D.面向对象
2.4 Java属于哪一类语言( )
A.面向机器的语言 B.面向对象的语言
C.面向过程的语言 D.面向操作系统的语言
3.1 简述Java语言的特点。
3.2 简述 JDK的含义和作用。
3.3 简述什么是JVM。
4.1 编写程序,在控制台展示出“欢迎来AAA教育学习!”和“有梦想,一起实现!”两句话。
认打包生成的jar是不能够直接运行的,因为带有main方法的类信息不会添加到manifest中(打开jar文件中的META-INF/MANIFEST.MF文件,将无法看到Main-Class一行)。
用maven打包java程序,当执行 java -jar 文件时提示 no main manifest attribute。
为了生成可执行的jar文件,需要借助插件。
目录:
# 进入你想创建项目的父文件夹
cd /Volumes/RamDisk
# 查看当前文件夹
pwd
# 生成项目
docker run -itd --rm --name maven_quick_tmp \
-v "$HOME/.m2/repository":/root/.m2/repository \
-v "$PWD":/usr/src/mymaven \
-w /usr/src/mymaven \
virhuiai/maven_quick:version-aliyun \
mvn archetype:generate \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false \
-DarchetypeVersion=1.4 \
-DgroupId=com.virhuiai.www \
-DartifactId=hello-world \
-DpackageName=com.virhuiai.www \
-DarchetypeVersion=RELEASE
查看下生成的项目结构:
tree -C hello-world
其中pom.xml的部分如下:
如果要指定版本号,即将jdk版本替换为1.8:
cd hello-world/
sed -ri -e 's!<maven.compiler.source>1.7</maven.compiler.source>!<maven.compiler.source>1.8</maven.compiler.source>!g' pom.xml
sed -ri -e 's!<maven.compiler.target>1.7</maven.compiler.target>!<maven.compiler.target>1.8</maven.compiler.target>!g' pom.xml
官网地址在:
http://maven.apache.org/plugins/maven-shade-plugin/examples/executable-jar.html
按说明,在pom.xml中添加以下内容:
<build>
。。。
<pluginManagement>
。。。
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.virhuiai.www.App</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
注意这是直接位于build>plugins下的,不是pluginManagement里的,否则不会有效果()。
现在执行mvn clean install:
mvn clean install
其中有句:
Replacing /usr/src/mymaven/hello-world/target/hello-world-1.0-SNAPSHOT.jar with /usr/src/mymaven/hello-world/target/hello-world-1.0-SNAPSHOT-shaded.jar
说明已经被替换成带有Main-Class信息的可运行jar。
现在,在项目根目录中执行该jar文件:
root@9275e11b3f0f:/usr/src/mymaven/hello-world# java -jar /usr/src/mymaven/hello-world/target/hello-world-1.0-SNAPSHOT.jar
Hello World!
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.virhuiai.www.App</mainClass> <!-- //主程序入口类,可以按住control,单机定位到该类-->
</manifest>
</archive>
</configuration>
</plugin>
注意这也是直接位于build>plugins下的,不是pluginManagement里的,pluginManagement指定版本号。
、JVM中的类加载器类型
从Java虚拟机的角度讲,只有两种不同的类加载器:启动类加载器和其他类加载器。
1.启动类加载器(Boostrap ClassLoader):这个是由c++实现的,主要负责JAVA_HOME/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作。
2.其他类加载器:由java实现,可以在方法区找到其Class对象。这里又细分为几个加载器
a).扩展类加载器(Extension ClassLoader):负责用于加载JAVA_HOME/lib/ext目录中的,或者被-Djava.ext.dirs系统变量指定所指定的路径中所有类库(jar),开发者可以直接使用扩展类加载器。java.ext.dirs系统变量所指定的路径的可以通过System.getProperty("java.ext.dirs")来查看。
b).应用程序类加载器(Application ClassLoader):负责java -classpath或-Djava.class.path所指的目录下的类与jar包装入工作。开发者可以直接使用这个类加载器。在没有指定自定义类加载器的情况下,这就是程序的默认加载器。
c).自定义类加载器(User ClassLoader):在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。
这四个类加载器的层级关系,如下图所示。
二、为什么要自定义类加载器
三、自定义类加载器
3.1 ClassLoader实现自定义类加载器相关方法说明
要实现自定义类加载器需要先继承ClassLoader,ClassLoader类是一个抽象类,负责加载classes的对象。自定义ClassLoader中至少需要了解其中的三个的方法: loadClass,findClass,defineClass。
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError { return defineClass(name, b, off, len, null); }
loadClass:JVM在加载类的时候,都是通过ClassLoader的loadClass()方法来加载class的,loadClass使用双亲委派模式。如果要改变双亲委派模式,可以修改loadClass来改变class的加载方式。双亲委派模式这里就不赘述了。
findClass:ClassLoader通过findClass()方法来加载类。自定义类加载器实现这个方法来加载需要的类,比如指定路径下的文件,字节流等。
definedClass:definedClass在findClass中使用,通过调用传进去一个Class文件的字节数组,就可以方法区生成一个Class对象,也就是findClass实现了类加载的功能了。
贴上一段ClassLoader中loadClass源码,见见真面目...
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
源码说明...
/** * Loads the class with the specified <a href="#name">binary name</a>. The * default implementation of this method searches for classes in the * following order: * * <ol> * * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded. </p></li> * * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method * on the parent class loader. If the parent is <tt>null</tt> the class * loader built-in to the virtual machine is used, instead. </p></li> * * <li><p> Invoke the {@link #findClass(String)} method to find the * class. </p></li> * * </ol> * * <p> If the class was found using the above steps, and the * <tt>resolve</tt> flag is true, this method will then invoke the {@link * #resolveClass(Class)} method on the resulting <tt>Class</tt> object. * * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link * #findClass(String)}, rather than this method. </p> * * <p> Unless overridden, this method synchronizes on the result of * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method * during the entire class loading process. * * @param name * The <a href="#name">binary name</a> of the class * * @param resolve * If <tt>true</tt> then resolve the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class could not be found */
翻译过来大概是:使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String)方法检查这个类是否被加载过 使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机内置的加载器调用findClass(String)方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。 ClassLoader的子类最好覆盖findClass(String)而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)。
resolveClass:Class载入必须链接(link),链接指的是把单一的Class加入到有继承关系的类树中。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java™规范中的Execution描述进行链接。
3.2 自定义类加载器实现
按照3.1的说明,继承ClassLoader后重写了findClass方法加载指定路径上的class。先贴上自定义类加载器。
package com.chenerzhu.learning.classloader; import java.nio.file.Files; import java.nio.file.Paths; /** * @author chenerzhu * @create 2018-10-04 10:47 **/ public class MyClassLoader extends ClassLoader { private String path; public MyClassLoader(String path) { this.path = path; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] result = getClass(name); if (result == null) { throw new ClassNotFoundException(); } else { return defineClass(name, result, 0, result.length); } } catch (Exception e) { e.printStackTrace(); } return null; } private byte[] getClass(String name) { try { return Files.readAllBytes(Paths.get(path)); } catch (Exception e) { e.printStackTrace(); } return null; } }
以上就是自定义的类加载器了,实现的功能是加载指定路径的class。再看看如何使用。
package com.chenerzhu.learning.classloader; import org.junit.Test; /** * Created by chenerzhu on 2018/10/4. */ public class MyClassLoaderTest { @Test public void testClassLoader() throws Exception { MyClassLoader myClassLoader = new MyClassLoader("src/test/resources/bean/Hello.class"); Class clazz = myClassLoader.loadClass("com.chenerzhu.learning.classloader.bean.Hello"); Object obj = clazz.newInstance(); System.out.println(obj); System.out.println(obj.getClass().getClassLoader()); } }
首先通过构造方法创建MyClassLoader对象myClassLoader,指定加载src/test/resources/bean/Hello.class路径的Hello.class(当然这里只是个例子,直接指定一个class的路径了)。然后通过myClassLoader方法loadClass加载Hello的Class对象,最后实例化对象。以下是输出结果,看得出来实例化成功了,并且类加载器使用的是MyClassLoader。
在此我向大家推荐一个Java学习交流群。交流学习群号:874811168 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,一起学习,一起进步,目前受益良多。
com.chenerzhu.learning.classloader.bean.Hello@2b2948e2 com.chenerzhu.learning.classloader.MyClassLoader@335eadca
四、类Class卸载
JVM中class和Meta信息存放在PermGen space区域(JDK1.8之后存放在MateSpace中)。如果加载的class文件很多,那么可能导致元数据空间溢出。引起java.lang.OutOfMemory异常。对于有些Class我们可能只需要使用一次,就不再需要了,也可能我们修改了class文件,我们需要重新加载 newclass,那么oldclass就不再需要了。所以需要在JVM中卸载(unload)类Class。
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
很容易理解,就是要被卸载的类的ClassLoader实例已经被GC并且本身不存在任何相关的引用就可以被卸载了,也就是JVM清除了类在方法区内的二进制数据。
JVM自带的类加载器所加载的类,在虚拟机的生命周期中,会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象。因此这些Class对象始终是可触及的,不会被卸载。而用户自定义的类加载器加载的类是可以被卸载的。虽然满足以上三个条件Class可以被卸载,但是GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。
五、JVM自定义类加载器加载指定classPath下的所有class及jar
经过以上几个点的说明,现在可以实现JVM自定义类加载器加载指定classPath下的所有class及jar了。这里没有限制class和jar的位置,只要是classPath路径下的都会被加载进JVM,而一些web应用服务器加载是有限定的,比如tomcat加载的是每个应用classPath+“/classes”加载class,classPath+“/lib”加载jar。以下就是代码啦...
package com.chenerzhu.learning.classloader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Enumeration; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * @author chenerzhu * @create 2018-10-04 12:24 **/ public class ClassPathClassLoader extends ClassLoader{ private static Map<String, byte[]> classMap = new ConcurrentHashMap<>(); private String classPath; public ClassPathClassLoader() { } public ClassPathClassLoader(String classPath) { if (classPath.endsWith(File.separator)) { this.classPath = classPath; } else { this.classPath = classPath + File.separator; } preReadClassFile(); preReadJarFile(); } public static boolean addClass(String className, byte[] byteCode) { if (!classMap.containsKey(className)) { classMap.put(className, byteCode); return true; } return false; } /** * 这里仅仅卸载了myclassLoader的classMap中的class,虚拟机中的 * Class的卸载是不可控的 * 自定义类的卸载需要MyClassLoader不存在引用等条件 * @param className * @return */ public static boolean unloadClass(String className) { if (classMap.containsKey(className)) { classMap.remove(className); return true; } return false; } /** * 遵守双亲委托规则 */ @Override protected Class<?> findClass(String name) { try { byte[] result = getClass(name); if (result == null) { throw new ClassNotFoundException(); } else { return defineClass(name, result, 0, result.length); } } catch (Exception e) { e.printStackTrace(); } return null; } private byte[] getClass(String className) { if (classMap.containsKey(className)) { return classMap.get(className); } else { return null; } } private void preReadClassFile() { File[] files = new File(classPath).listFiles(); if (files != null) { for (File file : files) { scanClassFile(file); } } } private void scanClassFile(File file) { if (file.exists()) { if (file.isFile() && file.getName().endsWith(".class")) { try { byte[] byteCode = Files.readAllBytes(Paths.get(file.getAbsolutePath())); String className = file.getAbsolutePath().replace(classPath, "") .replace(File.separator, ".") .replace(".class", ""); addClass(className, byteCode); } catch (IOException e) { e.printStackTrace(); } } else if (file.isDirectory()) { for (File f : file.listFiles()) { scanClassFile(f); } } } } private void preReadJarFile() { File[] files = new File(classPath).listFiles(); if (files != null) { for (File file : files) { scanJarFile(file); } } } private void readJAR(JarFile jar) throws IOException { Enumeration<JarEntry> en = jar.entries(); while (en.hasMoreElements()) { JarEntry je = en.nextElement(); je.getName(); String name = je.getName(); if (name.endsWith(".class")) { //String className = name.replace(File.separator, ".").replace(".class", ""); String className = name.replace("\\", ".") .replace("/", ".") .replace(".class", ""); InputStream input = null; ByteArrayOutputStream baos = null; try { input = jar.getInputStream(je); baos = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = input.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } addClass(className, baos.toByteArray()); } catch (Exception e) { e.printStackTrace(); } finally { if (baos != null) { baos.close(); } if (input != null) { input.close(); } } } } } private void scanJarFile(File file) { if (file.exists()) { if (file.isFile() && file.getName().endsWith(".jar")) { try { readJAR(new JarFile(file)); } catch (IOException e) { e.printStackTrace(); 在此我向大家推荐一个Java学习交流群。交流学习群号:874811168 可免费领取java资料。 } } else if (file.isDirectory()) { for (File f : file.listFiles()) { scanJarFile(f); } } } } public void addJar(String jarPath) throws IOException { File file = new File(jarPath); if (file.exists()) { JarFile jar = new JarFile(file); readJAR(jar); } } }
如何使用的代码就不贴了,和3.2节自定义类加载器的使用方式一样。只是构造方法的参数变成classPath了,篇末有代码。当创建MyClassLoader对象时,会自动添加指定classPath下面的所有class和jar里面的class到classMap中,classMap维护className和classCode字节码的关系,只是个缓冲作用,避免每次都从文件中读取。自定义类加载器每次loadClass都会首先在JVM中找是否已经加载className的类,如果不存在就会到classMap中取,如果取不到就是加载错误了。
六、最后
至此,JVM自定义类加载器加载指定classPath下的所有class及jar已经完成了。这篇博文花了两天才写完,在写的过程中有意识地去了解了许多代码的细节,收获也很多。本来最近仅仅是想实现Quartz控制台页面任务添加支持动态class,结果不知不觉跑到类加载器的坑了,在此也趁这个机会总结一遍。当然以上内容并不能保证正确,所以希望大家看到错误能够指出,帮助我更正已有的认知,共同进步。
出处:https://www.cnblogs.com/chenerzhu/p/9741883.html
*请认真填写需求信息,我们会在24小时内与您取得联系。