当今Java图形化界面的开发工具有两大霸主:一个是Eclipse(免费)/MyEclipse(收费),另一个是JetBrains公司的IntelliJ IDEA(简称IDEA,收费)。两者都是Java语言的跨平台图形化集成开发环境,可以在Windows、Mac OS和Linux上提供一致的体验,两者的用户群体都不少。一般预算足够的人用IntelliJ IDEA比较多,IntelliJ IDEA在业界被公认为是最好的Java开发工具,尤其在智能代码助手、代码自动提示、重构、JavaEE支持、各类版本工具(Git、SVN等)、JUnit、CVS整合、代码分析、创新的GUI设计等方面的功能可以说是超常的。这两个工具笔者都使用过,感觉IntelliJ IDEA更胜一筹。举一个例子,比如在IntelliJ IDEA开发中,只知道一个类名,但不知道是哪个程序包里的,即不知道import后如何编写,此时把鼠标放在这个类名上就会出现智能提示,如图3-27所示。
图3-27
PKCS7是要使用的类名,但不知道是哪个程序包里的,此时可以单击蓝色的Import class,就会自动帮助添加好包,我们在源文件开头可以看到新增了import sun.security.pkcs.PKCS7;这一句,这就是IDEA帮助添加的,它识别出了PKCS7,非常智能。别小看这个功能,只知道类名但不知道包名这种情况经常会发生,IDEA可以最大限度地帮我们节省时间。这里笔者推荐IntelliJ IDEA,它提供了30天的免费使用期限,读者可以去官网下载,地址是https://www.jetbrains.com/idea/download/。笔者使用的是IntelliJ IDEA 2021.1.3版本,在Windows 7下使用(用Windows 10与此类似)。
在图形化的Linux下使用这两款开发工具非常简单,都是傻瓜式操作。在企业一线开发中,很多Linux系统都是不带图形界面的,而且Linux主机都是锁在机柜里的,开发人员只能在自己办公桌上用计算机进行远程的Linux开发。因此,我们要学会在计算机上进行远程Linux开发,而且要使用集成开发环境。值得庆幸的是,IDEA已经完全考虑到了这一点,并且提供了周到的支持,让我们远程开发起来非常舒心。
笔者在Windows 7上安装了IntelliJ IDEA 2021.1.3,然后用VMware安装了CentOS 7.6来模拟远程Linux,这样只需要一台计算机就够了。笔者的Windows 7的IP是192.168.11.2,虚拟机CentOS 7.6的IP是192.168.11.129,两者已经能互相ping通。下载安装IntelliJ IDEA的过程这里就不赘述了,相信读者都是Windows安装高手。另外,要在Windows 7下使用IDEA,必须先在Windows 7下安装好JDK,我们在上一章已经介绍过其安装方法,这里不再赘述。下面直接进入实战。
1)在Windows下打开IntelliJ IDEA 2021.1.3,新建一个Java工程,如图3-28所示。
图3-28
单击Next按钮,然后指定工程名和路径,如图3-29所示。
图3-29
用户也可以根据自己的习惯设置工程名和路径。单击Finish按钮进入IDEA主界面。如果磁盘上没有E:\ex\myjava路径,则会提示我们是否要自动建立,选择“是”,系统会帮助我们建立E:\ex\myjava路径,非常贴心。
2)添加一个源文件。在IDEA的project视图下右击src,然后在快捷菜单中选择New→Java Class,再输入类名helloworld,随后编辑窗口就被打开了,在其中输入如下代码:
public class helloworld { public static void main(String[] args) { System.out.println("Hello world!"); } }
3)进行运行与调试配置。单击菜单Run→Edit Configurations...,此时出现Run/Debug Configurations对话框,单击Add New Configuration,出现一个菜单,选择Application菜单项,也就是为应用程序添加一个运行配置(包括编译所在的主机、工作文件夹等信息),如图3-30所示。
图3-30
在该对话框的Name框中输入自定义的配置名,比如myconfig,随后在Run on右边的下拉列表框中选择SSH,将出现New Target:SSH对话框,如图3-31所示。
图3-31
在Host框中输入192.168.11.129,在Username框中输入root,其中192.168.11.129是虚拟机Linux的IP地址,root是虚拟机Linux的账号,通过SSH连接,我们可以把本地编辑的Java源码文件安全传输到虚拟机Linux上去编译和运行。单击Next按钮,稍等片刻,如果连接成功,则会出现root账号与密码的对话框,如图3-32所示。
图3-32
输入root账号的密码123456,单击Next按钮,稍等片刻,如果认证成功,则自动解析Linux上的Java安装成功,如图3-33所示。
图3-33
单击Next按钮,出现工程目录在Linux上的配置对话框,这里保持默认设置,如图3-34所示。
图3-34
默认的/root/test/路径就是在Linux上的工程目录的路径。单击Finish按钮,再次回到Run/Debug Configurations对话框,可以发现对话框的下方出现了一句错误提示信息Error:No main class specified,如图3-35所示。
图3-35
这是经常令初学者头疼的问题,也是IDEA有点“弱智”的地方。在对话框中找到Main class编辑框,在该编辑框的右边末端单击一个矩形图标,随后出现Choose Main Class对话框,如图3-36所示。
图3-36
选中helloworld,并单击OK按钮,回到Run/Debug Configurations对话框,这是就会发现错误提示信息没有了,如图3-37所示。
图3-37
IDEA会自动识别主类并填好,单击OK按钮,配置结束。此时回到IDEA主界面上,单击右上方的绿色箭头就可以开始编译和运行了,而且是在虚拟机Linux上编译和运行,如图3-38所示。
图3-38
稍等片刻,编译并运行成功,可以看到在IDEA下方显示出运行的结果,如图3-39所示。
图3-39
可以看到输出了“Hello world!”,说明运行成功。至此,第一个IDEA开发的远程Linux Java程序成功。
开发服务器程序免不了要与第三方库打交道,尤其是为了保障服务器程序的安全,肯定要在服务端程序中加入安全机制,比如加密、签名等措施。为了增加这些安全功能,通常会加入提供安全机制的算法库,所以我们要学会如何在工程中添加第三方库,并且使用库中的函数。
在Java领域,BouncyCastle可谓是大名鼎鼎的安全算法库,尤其是实现了JDK不曾支持的国密算法,比如SM2等。现在要设计安全的服务端程序,自主可控是基本要求,而自主可控的基本要求就是使用国家密码管理局认可的国密算法。下面我们在工程中使用BouncyCastle库,当然本书不专门介绍安全编程,只是让读者学会如何导入和使用第三方安全算法库。这是每个服务端程序开发者必须要学会的。
我们可以到BouncyCastle官网下载JAR文件,根据自己的JDK版本进行选择,笔者的JDK是1.8,所以找到这个链接进行下载,如图3-40所示。
图3-40
单击bcprov-jdk15to18-169.jar,稍等片刻下载完成,下载下来的文件是bcprov-jdk15to18-169.jar。下面进入实战,导入bcprov-jdk15to18-169.jar并使用它。
1)打开IDEA,新建一个Java应用程序工程,工程名是test。路径笔者设为e:\ex\test。
2)在工程中新建一个文件夹,用来存放第三方库。在Project视图中,右击test,然后在快捷菜单上选择New→Directory,输入文件夹名为lib,此时Project视图中就多了一个lib文件夹。如果到磁盘上的工程目录(笔者的是e:\ex\test)去看,可以看到多了一个空的lib文件夹,我们把下载下来的bcprov-jdk15to18-169.jar文件复制到lib下。
3)导入库。在Project视图中,用鼠标右击lib,在快捷菜单上选择Add as Library,注意,如果有时不出现该菜单项,那么可以确认bcprov-jdk15to18-169.jar文件已经在lib文件夹中了,然后在出现的Create Library对话框的Name框中输入lib,接着单击OK按钮,如图3-41所示。
图3-41
接着添加该项目的Library,也就是指定该lib文件夹作为项目的一个Library。然后检查是否添加成功,单击菜单File→Project Structure,在Project Structure对话框左侧的Project Settings下选中Libraries,可以看到中间有一个lib了,如图3-42所示。
图3-42
下面添加Dependence。在Project Settings下选中Modules,然后在右边的Dependencies下勾选lib,如图3-43所示。
图3-43
最后单击OK按钮。至此,第三方库添加成功。下面开始添加源码。
4)在IDES中的Project视图下右击src,然后选择菜单New→Java Class,输入类名test,然后在test.java中输入如下代码:
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; import sun.misc.BASE64Encoder; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.*; import java.security.spec.ECGenParameterSpec; public class test { public static void main(String[] args) { sm3(); printProvider(); sm2(); } static int sm2() { System.out.println("----------生成密钥对start----------------"); //引入BC库 Security.addProvider(new BouncyCastleProvider()); //获取SM2椭圆曲线的参数 final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1"); try { //获取一个椭圆曲线类型的密钥对生成器 final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider()); //使用SM2参数初始化生成器 kpg.initialize(sm2Spec); //使用SM2算法使用规范初始化密钥生成器 kpg.initialize(sm2Spec, new SecureRandom()); //获取密钥对 KeyPair keyPair = kpg.generateKeyPair(); PublicKey pk = keyPair.getPublic(); PrivateKey privk = keyPair.getPrivate(); System.out.println("公钥串:"+new BASE64Encoder().encode(keyPair.getPublic().getEncoded())); System.out.println("私钥串:"+new BASE64Encoder().encode(keyPair.getPrivate().getEncoded())); System.out.println("公钥对象:"+pk); System.out.println("私钥对象:"+privk); } catch (NoSuchAlgorithmException e) { } catch (InvalidAlgorithmParameterException e) { } System.out.println("----------生成密钥对end----------------"); return 0; } static int sm3() { try { //注册BouncyCastle Security.addProvider(new BouncyCastleProvider()); //按名称正常调用 MessageDigest md = MessageDigest.getInstance("Sm3"); md.update("abc".getBytes("UTF-8")); byte[] result = md.digest(); System.out.println(new BigInteger(1, result).toString(16)); System.out.println("Hello world!"); } catch(NoSuchAlgorithmException e) { } catch(UnsupportedEncodingException e) { } return 0; } //打印支持的算法 private static void printProvider() { Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider(); for (Provider.Service service : provider.getServices()) { System.out.println(service.getType() + ": " + service.getAlgorithm()); } } /** * byte[] 转换为十六进制字符串 */ private final static char[] HEX_CHAR = "0123456789ABCDEF".toCharArray(); public static String hex16(byte[] b) { StringBuilder sb = new StringBuilder(); for (byte value : b) { sb.append(HEX_CHAR[value >> 4 & 0xf]) .append(HEX_CHAR[value & 0xf]); } return sb.toString(); } }
上面的程序主要实现了3个功能,对“abc”计算SM3摘要,生成SM2密钥对,打印BouncyCastle所支持的算法。至于SM2与SM3的原理这里就不介绍了,读者可以参考密码相关的图书。建议网络编程开发人员了解和掌握这些常用的国密安全算法。记住,服务器编程不单是编写一个网络程序,更要考虑应用过程的安全性。笔者一直认为,一个优秀的网络程序员肯定也是一个安全应用开发的高手。举一个简单的例子,如果要开发网络游戏的服务端程序,那样一定要考虑如何防止逆向和外挂,如何有效地认证真实的客户,不加密肯定不行。
5)保存工程并运行,运行结果如下:
66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0 Hello world! MessageDigest: GOST3411 Mac: HMACGOST3411 KeyGenerator: HMACGOST3411 MessageDigest: GOST3411-2012-256 Mac: HMACGOST3411-2012-256 KeyGenerator: HMACGOST3411-2012-256 ...
在一线开发Java服务器程序的过程中,经常为了加入安全性机制,要跟主机上插着的各种安全硬件打交道,比如加密卡、USBKEY等,这些硬件通常都提供了Linux下的C语言版本的共享库(.so),然后我们的Java服务器程序需要和这些C语言版本的SO库联合编程,也就是在Java程序中要调用C语言库。这也是Java网络程序员必须要掌握的一项技能,而且工作中肯定会碰到。
在Java中使用C语言库的传统做法是使用JNI编程。现在有了更好的替代者,即JNA(Java Native Access)。下面我们来看一下IntelliJ IDEA平台的JNA编程。JNA是一个开源的Java框架,是Sun公司推出的一种调用本地方法的技术,是建立在经典的JNI基础之上的一个框架。之所以说它是JNI的替代者,是因为JNA大大简化了调用本地方法的过程,使用很方便,基本上不需要脱离Java环境就可以完成。使用JNI的朋友可以考虑升级了,但JNI编程依旧要掌握,因为很多需要维护的老系统依旧使用的是JNI。
JNA只需要我们编写Java代码,而不用编写JNI或本地代码(适配用的.dll/.so),只需要在Java中编写一个接口和一些代码,作为.dll/.so的代理,就可以在Java程序中调用DLL/SO。JNA的功能相当于Windows的Platform/Invoke和Python的ctypes。
首先要下载jna.jar包,到JNA官网下载新版本的jna.jar,链接为https://github.com/twall/jna。
下载的文件是jna-master.zip,解压后搜索出文件jna.jar,把它单独复制出来,先找一个临时目录保存好,以后要导入工程中。jna.jar准备好了,下面可以实战。首先开发一个Linux SO库出来。
1)准备一个Linux的SO库(共享库)。在Windows下编辑一个C语言源文件,代码如下:
void PrintBuf(unsigned char *buf, int buflen) { int i; printf("\n"); printf("len = %d\n", buflen); for (i = 0; i < buflen; i++) { if (i % 32 != 31) printf("%02x", buf[i]); else printf("%02x\n", buf[i]); } printf("\n"); return; } int dosth(unsigned char *in, int inlen, unsigned char *out, int * poutlen) { printf("in:"); PrintBuf(in, inlen); memset(out, '1', 32); *poutlen = 32; puts("-------bye------"); return 0; }
上述代码把传进来的参数in打印出来,然后用“1”填充out,并更新*poutlen。把该源文件上传到Linux,然后用命令编译:
[root@localhost ex]# gcc test.c -shared -fPIC -o libmy.so [root@localhost ex]#
在同一个目录下将生成文件libmy.so,我们把这个libmy.so文件复制到/usr/lib64系统路径下,这样Java调用时就能找到该库文件了。
2)开发Java调用程序。在Windows下打开IDEA,新建一个Java工程并新建一个类,类名是test,然后在test.java中输入如下代码:
import java.io.IOException; import com.sun.jna.Library; import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.Memory; public class test { CLibrary lib; public interface CLibrary extends Library { int dosth(byte[]in,int inlen,byte []out,Pointer poutlen); } test() //构造方法,加载库 { lib = (CLibrary) Native.load("my", CLibrary.class); } public static void main(String[] args) throws IOException { test t1 = new test(); t1.test_myfunc(); } public void test_myfunc() //测试功能函数 { byte[] in= new byte[3]; String strIn="abc"; System.arraycopy(strIn.getBytes(), 0, in, 0 , 3); StringBuffer signValue = new StringBuffer(""); myfunc(in,signValue); System.out.println(signValue); } //功能函数,indata是输入数据,signValue是返回参数,存放最终结果 public int myfunc(byte[]indata,StringBuffer signValue) { byte[] out=new byte[512]; int size=Native.getNativeSize(Integer.class); Pointer poutlen = new Memory(size); int r = lib.dosth(indata,indata.length,out,poutlen); int outlen = poutlen.getInt(0); //得到最终结果的长度 System.out.println("outlen="+outlen); System.out.println("r:"+Integer.toHexString(r)); for(int i=0;i<outlen;i++) System.out.print(Integer.toHexString(out[i]&0xff)+","); System.out.println(""); String str = new String(out,0,outlen); //把字节数组中的有效数据转为字符串 signValue.delete(0,signValue.capacity()).append(str); return r; } }
为了让Java认识SO库中的函数dosth,我们在代码中以Java的形式声明了dosth,4个参数分别是in(输入参数字节数组)、inlen(字节数组的长度)、out(输出参数,也是字节数组,存放函数的最终结果)以及poutlen(类似于指针,存放最终结果的长度)。以上4个参数的类型基本覆盖了实战环境中的常见情况。
3)在工程目录下新建一个lib文件夹,把jna.jar文件放到lib文件夹下。在IDEA中单击菜单File→Project Structure...,打开Project Structure对话框,在该对话框中选择左边的Modules,在右边单击加号,选择JARs or Directories...,随后选择lib文件夹下的jna.jar文件,此时jna.ja出现在列表框中,然后勾选该复选框,如图3-44所示。
图3-44
最后单击OK按钮。
4)保存工程并运行,运行结果如下:
in: len = 3 616263 -------bye------ outlen=32 r:0 31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31, 31,31,31,31,31,31,31,31, 11111111111111111111111111111111
其中,“-------bye------”及其之前的内容都是dosth内部打印的,这是为了让我们知道,IDEA最终也能输出库函数中的打印,这样方便查找库中代码的问题。另外,31是字符“1”的ASCII码的十六进制形式,r旁边的0表示库函数的返回值。
至此,Java调用C语言共享库成功了。在以后的工作中,我们开发Java服务器程序时调用硬件厂家的共享库就不怕了。一个网络程序员必须是多面手。