Java核心技术卷II 笔记-下

文本是对《Java 核心技术卷II》做的第二篇笔记

网络

套接字超时

  • Socket.setSoTimeout()
  • SocketTimeoutException 超时异常

超时连接

可以通过先构建一个无连接的套接字,然后再使用一个超时来进行连接的方式解决

1
2
Socket s = new Socket();
s.connect(new InetSocketAddress(host, port), timeout);

InetAddress

一些访问量较大 的主机名通常会对应于多个因特网地址,以实现负载均衡。因此 InetAddress.getByName 会随机随机选择一个。

可以用 InetAddress.getAllByName 获取域名下的所有主机

1
2
3
4
5
public static void main(String[] args) throws UnknownHostException {
InetAddress[] addresses = InetAddress.getAllByName("www.baidu.com");
for (InetAddress addr : addresses)
System.out.println(addr);
}

服务器套接字 ServerSocket

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
28
29
30
31
32
33
34
35
36
37
38
39
package com.test5;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class Test1 {

public static void main(String[] args) throws Exception {
try (ServerSocket s = new ServerSocket(5555)) {
try (Socket socket = s.accept()) {
System.out.println("Client connected: " + socket.getRemoteSocketAddress());

InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();

try (Scanner scanner = new Scanner(in)) {
PrintWriter writer = new PrintWriter(new OutputStreamWriter(out), true);

while (scanner.hasNextLine()) {
String string = scanner.nextLine();
System.out.println("=> " + string);

writer.println(string);
if (string.trim().equals("quit")) {
System.out.println("Client closed: " + socket.getRemoteSocketAddress());
break;
}
}
}
}
}
}
}

半关闭

  • shutdownOutput()
  • shutdownlnput()

可中断套接字

当连接到一个套接宇时,当前线程将会被阻塞直到建立连接或产生超时为止;当通过套接字读写数据时,当前线程也会被阻塞直到操作成功或产生超时为止。

为了中断套接字操作,可以使用 java.nio 包提供的一个特性:SocketChannel 类 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws IOException {
try (SocketChannel socket = SocketChannel.open(
new InetSocketAddress(InetAddress.getByName("www.bilibili.com"), 80))) {

// try (Socket socket = new Socket(InetAddress.getByName("www.bilibili.com").getHostAddress(), 80)) {
BufferedOutputStream out = new BufferedOutputStream(Channels.newOutputStream(socket));
String req = "GET / HTTP/1.1\r\n\r\n";
out.write(req.getBytes());
out.flush();
try (BufferedInputStream in = new BufferedInputStream(Channels.newInputStream(socket))) {
System.out.println(new String(in.readAllBytes()));
}
}
}

获取 Web 数

URL 和 URI

URLURLConnection 类封装了大量复杂的实现细节,这些细节涉及如何从远程站点获取信息

在 Java 类库中,URI 类并不包含任何用于访问资源的方法,它的唯一作用就是解析,并将它分解成各种不同的组成部分。但是,URL 类可以打开一个到达资源的流。

因此,URL 类只能作用于那些 Java 类库知道该如何处理的模式,例如 http:https:ftp: 、 本地文件系统(file:)和 JAR 文件( jar:)。

使用 URLConnection 获取信息

如果想从某个 Web 资源获取更多信息,那么应该使用 URLConnection 类,通过它能够得到比基本的 URL 类更多的控制功能

  • 调用 URL 类中的 URL.openConnection 方法获得 URLConnection 对象
  • 使用以下方法来设置任意的请求属性 setXXX
  • 调用 connect 方法连接远程资源
  • 可以查询头信息
  • 访问资源数据

在默认情况下,建立的连接只产生从服务器读取信息的输入流,并不产生任何执行写操作的输出流(需要设置 setDoOutput)。

可以使用 Java 库的类来与网页交互,但是用起来并非特别方便

  • URL
    • openStream
    • openConnection
  • URLDecoder.decode
  • URLEncoder.encode
  • URLConnection/HttpURLConnection
    • setRequestProperty 设置请求头字段
    • getHeaderFields 返回headers
    • getXXX
    • getInput/OutputStream 返回从资源读取信息或向资源写人信息的流。属于 HTTP body 部分
    • getErrorStream()
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
28
29
30
31
32
33
34
35
package com.test5;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;

public class Test2 {
public static void main(String[] args) throws IOException {
URL url = new URL("http://www.example.com");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

// 自动重定向
urlConnection.setInstanceFollowRedirects(true);
urlConnection.connect();

System.out.println("Header: ");
Map<String, List<String>> headers = urlConnection.getHeaderFields();
headers.forEach((k, v) -> {
System.out.print(k + ": ");
v.forEach(System.out::print);
System.out.println();
});

System.out.println("Body: ");
BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String line;
while (null != (line = reader.readLine())) {
System.out.println(line);
}
}
}

代理服务器 Proxy

  • Proxy
  • ProxySelector 自动选择代理服务器
    • select 根据业务需要返回代理服务器列表
    • connectFailed 连接代理服务器失败
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
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.test6;

import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.util.ArrayList;
import java.util.List;

public class ProxyTest {

public static class MyProxySelector extends ProxySelector {
@Override
public List<Proxy> select(URI uri) {
System.out.println("==> " + uri);
List<Proxy> list = new ArrayList<>();
list.add(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8888)));
list.add(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", 1088)));
return list;
}

@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
System.out.println("无法连接到代理服务器!");
}
}

public static void main(String[] args) throws Exception {
// 设置代理选择器
ProxySelector.setDefault(new MyProxySelector());

// Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8888));
URL url = new URL("https://www.google.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);

try (InputStream in = conn.getInputStream()) {
System.out.println(new String(in.readAllBytes()));
}
}
}

数据库 SQL

注册驱动器类

包含 META-INF/services/java.sql.Driver 文件的 JAR 文件可以 自动注册驱动器类,解压缩驱动程序 JAR 文件就可以检查其是否包含该文件

如果驱动程序 JAR 文件不支持自动注册,那就需要找出数据库提供商使用的 JDBC 驱动器类的名字,通过使用 DriverManager 注册驱动器。有以下两种方式:

  • 在 Java 程序中 加载驱动器类,比如 Class.forName("com.mysql.cj.jdbc.Driver")
  • 设置 jdbc.drivers 属性
    • 命令行: java -Djdbc.drivers=com.mysql.cj.jdbc.Driver Main
    • Java程序中: System.setProperty("jdbc.drivers", "com.mysql.cj.jdbc.Driver")

连接到数据库

驱动管理器遍历所有 注册过的驱动程序,以便找到一个能够使用 数据库 URL 中指定的子协议的驱动程序。

  • DriverManager.getConnection 连接到数据库服务器

  • DriverManager.setLogWriter跟踪信息 发送给 PrintWriter

使用 JDBC 语句

  • Connection.createStatemen
  • Statement.executeUpdate 返回受 SQL 语句影响的行数
  • Statement.getResultSet() 返回 前一条查询语句的结果集。如果前一条语句未产生结果集,则返回 null 值

executeUpdate 方法既可以执行诸如 INSERTUPDATEDELETE 之类的操作,也可以执行诸如 CREATE TABLEDROP TABLE 之类的数据定义语句。但是 执行 SELECT 查询时必须使用 executeQuery 方法(返回 ResultSet 结果集对象)。execute 语句可以执行 任意的 SQL 语句,此方法通常只用于由用户提供的 交互式查询

如果 Statement 对象上有一个打开的结果集,那么调用 close 方法将自动关闭该结果集。同样地,调用 Connection 类的 close 方法将关闭该连接上的所有语句

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
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.test5;

import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.*;

public class SQLTest {
private final static String jdbc_driver = "com.mysql.cj.jdbc.Driver";
private final static String db_url = "jdbc:mysql://localhost:3306/db_test";
private final static String user = "root";
private final static String pwd = "fuckingyou";

public static void main(String[] args) throws Exception {
// Class.forName(jdbc_driver);
System.setProperty("jdbc.drivers", jdbc_driver);

PrintWriter writer = new PrintWriter(
new OutputStreamWriter(
Files.newOutputStream(Paths.get("basic/res/out.text"))));

DriverManager.setLogWriter(writer);
try (Connection conn = DriverManager.getConnection(db_url, user, pwd)) {
try (Statement stmt = conn.createStatement()) {
ResultSet res = stmt.executeQuery("select *from tb_test");
// 表的字段信息
ResultSetMetaData metaData = res.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
System.out.print(metaData.getColumnName(i) + "\t");
}
System.out.println();

while (res.next()) {
System.out.println(res.getString(1) + "\t" + res.getString(2));
}
}
}
}
}

SQL异常

预备语旬

在预备查询语句中,每个宿主变量都用 ? 来表示。使用 PreparedStatement 查询的时候不需要再次提供sql语句了。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.test5;

import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.*;

public class SQLTest {
private final static String jdbc_driver = "com.mysql.cj.jdbc.Driver";
private final static String db_url = "jdbc:mysql://localhost:3306/db_test";
private final static String user = "root";
private final static String pwd = "fuckingyou";

public static void main(String[] args) throws Exception {
// Class.forName(jdbc_driver);
System.setProperty("jdbc.drivers", jdbc_driver);

PrintWriter writer = new PrintWriter(
new OutputStreamWriter(
Files.newOutputStream(Paths.get("basic/res/out.text"))));

DriverManager.setLogWriter(writer);
try (Connection conn = DriverManager.getConnection(db_url, user, pwd)) {
try (PreparedStatement stmt = conn.prepareStatement("select *from tb_test where username=?")) {
// try (Statement stmt = conn.createStatement()) {
// ResultSet res = stmt.executeQuery("select *from tb_test");

stmt.setString(1, "root");
ResultSet res = stmt.executeQuery();

// 表的字段信息
ResultSetMetaData metaData = res.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
System.out.print(metaData.getColumnName(i) + "\t");
}
System.out.println();

while (res.next()) {
System.out.println(res.getString(1) + "\t" + res.getString(2));
}
}
}
}
}

多结果集

在执行存储过程,或者在使用允许在单个查询中提交多个 SELECT 语句的数据库时, 一个查询有可能会返回多个结果集

  • 使用 execute 方法来执行 SQL 语句
  • 获取第一个结果集或更新计数
  • 重复调用 getMoreResults 方法以移动到下一个结果集
  • 当不存在更多 的结果集或更新计数时,完成操作

行集

RowSet 接口扩展自 ResultSet 接口,却无需始终保持与数据库的连接。

行集还适用于将查询结果移动到复杂应用的其他层,或者是诸如手机之类的其他设备中。

以下都是扩展了 RowSet 接口:

  • CachedRowSet 允许在断开连接的状态下执行相关操作
  • WebRowSet 该行集可以保存为 XML 文件
  • FilteredRowSet/JoinRowSet 等同于 SQL 中的 SELECT 和 JOIN 操作
  • JdbcRowSet
被缓存的行集 CachedRowSet

一个被缓存的行集中包含了 一个结果集中所有的数据,断开数据库连接后仍然可以使用行集。在执行每个用户命令时,我们只需打开数据库连接、执行查询操作、将查询结果放入被缓存的行集,然后关闭数据库连接即可。

  • RowSetProvider.newFactory()
  • RowSetFactory.createCachedRowSet()

让 CachedRowSet 对象自动建立一个数据库连接

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
28
29
30
31
32
33
package com.test5;

import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.RowSetFactory;
import javax.sql.rowset.RowSetProvider;

public class Test3 {
public static void main(String[] args) throws Exception {
// 可缓存行集
RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSet();
// 连接 url
crs.setUrl(SQLTest.db_url);
crs.setUsername(SQLTest.user);
crs.setPassword(SQLTest.pwd);

// 设置每页有多少行数据
crs.setPageSize(1);
// crs.setCommand("select *from tb_test where username=?;");
// crs.setString(1, "root");

crs.setCommand("select *from tb_test");
crs.execute();

do {
while (crs.next()) {
System.out.println(crs.getString(1) + "\t" + crs.getString(2));
}
System.out.println("==> Next Page!");
// 是否还有下一页
} while (crs.nextPage());
}
}

或者直接从 ResultSet 构造 CachedRowSet

1
2
3
4
5
6
ResultSet res = stmt.executeQuery();

RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSet();
crs.populate(res); // 缓存结果集
conn.close(); // 然后就可以关闭连接了,这样就可以直接操控 CachedRowSet

元数据

我们可以获得三类元数据 :

  • 关于数据库的元数据,Connection DatabaseMetaData
  • 关于结果集的元数据,Result ResultSetMetaData
  • 关于预备语句参数的元数据,Statement ParameterMetaData

事务

我们可以将一组语句构建成一个事务(transaction)。当所有语句都顺利执行之后,事务可以被提交(commit)。否则,如果其中某个语句遇到错误,那么事务将被回滚,就好像没有任何语句被执行过一样 。

默认情况下,数据库连接处于自动提交模式,即每个 SQL 语句一旦被执行便被提交给数据库。因此在使用事务时, 需要关闭这个默认值

  • Connection.setAutoCommit(false)
  • Connection.commit/rollback 提交事务或者回滚

保存点

在使用某些驱动程序时,使用保存点(save point)可以更细粒度地 控制回滚操作。一个保存点意味着稍后只需回滚到这个点,而非事务的开头。

  • Savepoint sp = Connection.setSavepoint() 设置保存点
  • Connection.releaseSavepoint(sp) 当不再需要保存点时,必须释放它

批量更新

Connection.getMetaData().supportsBatchUpdates() 查看是否支持批量更新。

处于同一批中的语句可以是 INSERT、UPDATE 和 DELETE 等操作,也可以是数据库定义语句,如 CREATE TABLE 和 DROP TABLE

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
28
static void batchUpdate(int size) throws Exception {
Map<String, String> info = new HashMap<>();
for (int i = 0; i < size; i++) {
info.put(generateString(6), generateString(10));
}

try (Connection conn = getConnection()) {
try (PreparedStatement pstmt = conn.prepareStatement("INSERT INTO tb_test VALUES(?,?);")) {
info.forEach((k, v) -> {
try {
pstmt.setString(1, k);
pstmt.setString(2, v);
pstmt.addBatch(); // 添加批处理
} catch (SQLException e) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
});
// 执行批处理语句
pstmt.executeBatch();
// 提交事务
conn.commit();
}
}
}

曰期和时间 API

时间线 Instant

Instant 表示时间线上的某个点。被称为“新纪元”的时间线原点被设置为穿过伦敦格林威治皇家天文台的本初子午线所处时区的 1970 年 1 月 1 日的午夜。

Duration 是两个时刻之间的时间量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.test5;

import java.time.Duration;
import java.time.Instant;

public class Test4 {
public static void main(String[] args) throws Exception {
Instant s = Instant.now();
Thread.sleep(2000);
Instant e = Instant.now();

System.out.println(Duration.between(s, e).toMillis());
}
}

本地日期 LocalDate

为了构建 LocalDate 对象,可以使用 now 或 of 静态方法。

两个 Instant 之间的时长是 Duration,而用于本地日期 LocalDate 的等价物是 Period,它表示的是流逝 的 年、月或日的数量。两者都实现了 TemporalAmount 接口。

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
package com.test5;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.Period;

public class Test4 {
public static void main(String[] args) throws Exception {
Instant s = Instant.now();
Thread.sleep(1500);
Instant e = Instant.now();
// 时间差
System.out.println(Duration.between(s, e));

LocalDate now = LocalDate.now();
LocalDate localDate = LocalDate.now().plusDays(100).plus(Period.ofYears(100));

System.out.println(now);
System.out.println(localDate);
// 时间差
System.out.println(localDate.until(now));

System.out.println(LocalDate.of(2021, 3, 31).minusMonths(1));
}
}

输出

1
2
3
4
5
PT1.500160471S
2021-08-18
2121-11-26
P-100Y-3M-8D
2021-02-28

日期调整器 TemporalAdjusters

本地时间 LocalTime

  • LocalTime 表示当日时刻
  • LocalDateTime 表示日期和时间
1
2
3
4
5
6
7
public static void main(String[] args) {
LocalTime time = LocalTime.now();
System.out.println(time.plus(2, ChronoUnit.HOURS));

LocalDateTime datetime = LocalDateTime.now();
System.out.println(datetime.plus(2, ChronoUnit.YEARS));
}

时区时间 ZonedDateTime

格式化和解析 DateTimeFormatter

  • 格式化 LocalDate -> String
    • DateTimeFormatter.ofPattern("").format(LocalDate.now())
    • LocalDate.now().format(DateTimeFormatter.ofPattern(""))
  • 解析 + 格式化 String -> LocalDate
    • LocalDate.parse("", DateTimeFormatter.ofPattern(""))
    • LocalDate.parse("")

DateTimeFormatter 类提供了 三种用于打印日期/ 时间值的格式器

使用旧的Date对象时,我们用SimpleDateFormat进行格式化显示。使用新的LocalDateTimeZonedLocalDateTime时,我们要进行格式化显示,就要使用DateTimeFormatter

SimpleDateFormat不同的是,DateTimeFormatter不但是不变对象,它还是线程安全的。因为SimpleDateFormat不是线程安全的,使用的时候,只能在方法内部创建新的局部变量。而DateTimeFormatter可以只创建一个实例,到处引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
// 不支持时间格式化
String text = DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDate.now());
System.out.println(text);
// 不支持日期格式化
text = DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalTime.now());
System.out.println(text);
// 支持日期和时间格式化
text = DateTimeFormatter.ofPattern("EE yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(LocalDateTime.now());
System.out.println(text);

text = new SimpleDateFormat().format(new Date());
System.out.println(text);

LocalDate date = LocalDate.parse("2021-02-13", DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
System.out.println(date);
}

输出

1
2
3
4
2021-08-18
19:30:29
周三 2021-08-18 19:30:29
2021/8/18 下午7:30

与遗留代码的互操作

国际化

数字恪式

Java 类库提供了一个格式器(formatter)对象的集合,可以对 java.text 包中的数字值进行 格式化和解析

  • 创建 Locale 对象
  • 使用一个“工厂方法”得到一个格式器对象
  • 使用这个格式器对象来完成格式化和解析工作

NumberFormat

  • getNumberInstance,对数字格式化
  • getCurrencyInstance,对货币量格式化
  • getPercentinstance,对百分比格式化
1
2
3
4
5
6
7
8
9
10
11
12
package com.test5;

import java.text.NumberFormat;
import java.util.Locale;

public class Test5 {
public static void main(String[] args) {
Locale loc = Locale.CHINA;
NumberFormat fmt = NumberFormat.getCurrencyInstance(loc);
System.out.println(fmt.format(123456.774));
}
}

消息恪式化 MessageFormat

使用占位符 {} 进行字符串的格式化

  • MessageFormat.format()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.test5;

import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Locale;

public class Test5 {
public static void main(String[] args) {
String message = MessageFormat.format(
"My name is {0}, and I love programming: {1}, {2}. I have {3,number,currency}",
"Mike", "C++", "Java", 10.20001);

System.out.println(message);
}
}

脚本、编译与注解处理

编译器 API

  • JavaCompiler.compile 实际上就是调用 javac 编译Java程序生成 .class 字节码文件
  • 返回 0 表示成功,否则将错误信息输出到错误流中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.test5;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

public class Test5 {
public static void main(String[] args) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
System.out.println(compiler.name()); // javac
int res = compiler.run(null, null, null, "-sourcepath", "src", "basic/src/com/test5/SQLTest.java");
System.out.println(res);
}
}

使用编译工具

使用 CompilationTask 对象来对编译过程进行更多的控制。

总之,如果只是想以常规的方式调用编译器,那就只需要调用 JavaCompiler 任务的 run 方法,去读写磁盘文件,可以捕获输出和错误消息,但是需要你自己去解析它们。如果想对文件处理和错误报告进行更多的控制,可以使用 CompilationTask 类。

使用注解

在 Java 中,注解是当作一个修饰符来使用的,它被置于被注解项之前,中间没有分号。注解可以存在于任何可以放置一个像 public 或者 static 这样的修饰符的地方。还可以注解包、参数变量、类型参数和类型用法

注解接口

通过 @interface 来定义一个注解接口。

所有的注解接口都隐式地扩展自 java.lang.annotation.Annotation 接口。这个接口是一个常规接口,不是一个注解接口。为注解添加属性 : 接口中的方法都是抽象方法 , 其中 public abstract 可以省略

注解元素的类型:

  • 基本类型 (int、short、long、byte、char、double、float 或者 boolean)
  • String
  • Class
  • enum类型
  • 注解类型
  • 由上面所述类型组成的数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.test5;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
// 提供一个默认值
int count() default 100;
boolean exist() default false;
// public abstract 可以省略
public abstract String message() default "hello world";
enum Color {RED, BLUE, GREEN}
Class<?> testCase() default String.class;
Class<Integer> testInteger() default Integer.class;
Color color() default Color.RED;
String[] array();
}
注解规范

每个注解都具有下面这种格式,顺序无关紧要,如果某个元素的值并未指定,那么就使用声明的默认值

@AnnotationName(elementName1=value1, elementName2=value2, ...)

  • 标记注解:如果没有指定元素,要么是因为注解中 没有任何元素,要么是因为 所有元素都使用默认值,那么就不需要使用圆括号了 @Test
  • 单值注解:如果一个元素具有 特殊的名字 XXX value(); ,并且没有指定其他元素,那么就可以忽略掉这个元素名以及赋值等号 @Test("xx")
  • 因为注解是由编译器计算而来的,因此,所有元素值必须是 编译期常量
  • 一个项可以有多个 不同的注解,如果注解的作者将其声明为 可重复的,那么你就可以多次重复使用 同一个注解
  • 一个注解元素永远不能设置为 null ,甚至不允许其默认值为 null
  • 如果元素值是一个数组,那么要将它的值用括号括起来 @Test(arr={"hello", "world"})
    • 如果该元素具有单值,那么可以忽略这些括号 @Test(arr="hello")
  • 在注解中引入 循环依赖 是一种错误
1
2
@MyTest(message = "this is a test", array = "linux")
private static void test1() { }
反射实现自定义注解赋值
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
28
29
30
31
32
33
34
35
36
37
package com.test5;

import java.lang.reflect.Field;

public class Test7 {

@TestCase("This is a message")
private String text;

public void test() throws Exception {
// 字段变量,通过 反射获取类的字段
Field field = Test7.class.getDeclaredField("text");
// 设置可访问
field.setAccessible(true);
if (field.isAnnotationPresent(TestCase.class)) {
System.out.println("存在注解 @TestCase");
}

TestCase tc = field.getAnnotation(TestCase.class);
// 获取注解里面的属性值
String value = tc.value();

System.out.println("value: " + value);
// 设置字段变量值
field.set(this, value);
System.out.println("text: " + this.text);
}

public static void main(String[] args) throws Exception {
new Test7().test();
}
}

// 输出
存在注解 @TestCase
value: This is a message
text: This is a message
注解各类声明

注解可以出现在许多地方:

  • 类(包括枚举类enum)
  • 接口(包括注解接口)
  • 方法
  • 构造器
  • 实例域(包括enum常量)
  • 局部变量
  • 参数变量
  • 类型参数

泛化类或方法中的类型参数可以:public class Cache<@Immutable V> { ... }

包是在文件 package-info.java 中注解的,该文件只包含以注解先导的包语句,比如:

1
2
3
4
5
6
/**
* 包级文档注释
* This package was deprecated!!!
*/
@Deprecated
package com.test5;
注解类型用法

@NonNull 注解是 Checker Framework 的一部分

类型用法注解可以出现在下面的位置

  • 与泛化类型引元一起使用,List<@NonNull String>
  • 数组中的任何位置
    • @NonNull String[][] arr; arr[i][j] 不为 null
    • String @NonNull [][] arr; arr 不为 null
    • String[] @NonNull [] arr; arr[i] 不为 null
  • 与超类和实现接口一起使用,class Warning extends @Localized Message
  • 与构造器调用一起使用,new @Localized String(...)
  • 与强制转型和 instanceof 检查一起使用,text instanceof @Localized String
  • 与异常规约一起使用,public String read() throws @Localized IOException
  • 与通配符和类型边界一起使用,List<@Localized ? extends Message>
  • 与方法和构造器引用一起使用,@Localized Message::getText
标准注解
  • 用于编译的注解

    • @Deprecated 注解可以被添加到任何不再鼓励使用的项上
    • @SuppressWarnings 注解会告知编译器 阻止特定类型的警告信息
    • @Override 这种注解只能应用到方法上 。 编译器会检查具有这种注解的方法是否真正覆盖了一个来自于超类 的方法
    • @Generated 注解的目的是供代码生成工具来使用
  • 用于管理资源的注解

    • @PostConstruct@PreDestroy 注解用于控制对象生命周期的环境中。标记了这些注解的方法应该 在对象被构建之后,或者在对象被移除之前,紧接着调用
    • @Resource 注解用于资源注入
  • 元注解

    • @Target 元注解可以应用于一个注解,以限制该注解可以应用到哪些项上。可以指定任意数量的元素类型,用括号括起来。限制的注解可以应用于任何项上

    • @Retention 元注解用于指定一条注解应该 保留多长时间,默认值是 RetentionPolicy.CLASS

    • @Documented 元注解为像 Javadoc 这样的归档工具提供了一些提示

    • @Inherited 元注解只能应用于对的注解。如果一个类具有继承注解,那么它的 所有子类都自动具有同样的注解

    • @Repeatable 将同种类型的注解多次应用于某一项是合法的,可重复注解的实现者需要提供 一个容器注解,它可以 将这些重复注解存储到一个数组中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.test5;

import java.lang.annotation.*;

@Repeatable(MyTests.class)
public @interface MyTest {
int count() default 100;
String message() default "hello world";
enum Color {RED, BLUE, GREEN}
Color color() default Color.RED;
}

@interface MyTests {
MyTest[] value();
}

一般较为常用的是以下几个

1
2
3
@Target
@Retention
@Documented
源码级注解处理

注解的另一种用法是自动处理源代码以产生更多的源代码、配置文件、脚本或其他任何我们想要生成的东西

注解处理器只能产生新的源文件,它无法修改已有的源文件

注解处理器通常通过扩展 AbstractProcessor 类而实现 Processor 接口。

安全

类加载器

JVM 初始化 一 个类包含如下几个步骤

  • 假如这个类还没有被加载和连接,则程序先 加载并连接该类
  • 假如该类的直接父类还没有被初始化,则先 初始化其直接父类
  • 假如类中有初始化语句,则系统 依次执行这些初始化语句
类加载过程

每个 Java 程序至少拥有三个类加载器

  • 引导类加载器
  • 扩展类加载器
  • 系统类加载器
类加载器的层次结构

类加载器有一种父/子关系。除了引导类加载器外,每个类加载器都有一个父类加载器

某些程序具有插件架构,其中代码的某些部分是作为可选的插件打包的 。 如果插件被打包为 JAR 文件,那就可以直接用 URLClassLoader 类的实例去加载这些类。

JVM 的根类加载器 并不是 Java 实现的,而且由于程序通常无须访问根类加载器,因此访问扩展类加载器的父类加载器时返回 null

系统类加载器是 AppClassLoader 的实例,扩展类加载器 PlatformClassLoader的实例。实际上,这两个类都是 URLClassLoader 类的 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.test6;

public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
// 系统类加载器
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemLoader);
// 获取系统类加载器的加载路径
var x = systemLoader.getResources("");
while (x.hasMoreElements()) {
System.out.println(x.nextElement());
}
// 扩展加载器
ClassLoader extensionLoader = systemLoader.getParent();
System.out.println(extensionLoader);
System.out.println(System.getProperty("java.ext.dirs"));

// 根类加载器并没有继承 ClassLoader 抽象类,
// 所以扩展类加载器的 getParent 方法返回 null
System.out.println(extensionLoader.getParent());
}
}

Class.forName 使用的是 系统类加载器

每个线程都有一个对类加载器的引用,称为 上下文类加载器主线程的上下文类加载器是系统类加载器。当新线程创建时,它的上下文类加载器会被设置成为创建该线程的上下文类加载器。

编写你自己的类加载器

如果要编写自己的类加载器,只需要继承 Classloader 类,然后重写 findClass 方法。

Classloader 超类的 loadClass 方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用 findClass 方法。

  • ClassLoader
    • findClass方法:类加载器应该覆盖该方法,以查找类的字节码,并通过调用 defineClass 方法将字节码传给虚拟机,使用 . 作为包名分隔符
    • defineClass方法:将一个新的类添加到虚拟机中,其字节码在给定的数据范围中
  • URLClassLoader
    • 构建一个类加载器,它可以从给定的 URL 处加载类。如果 URL 以 / 结尾,那么它表示的一个目录。否则,它表示的是一个 JAR 文件
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
28
29
30
31
32
33
34
35
package com.test5;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class MyClassLoader extends ClassLoader {
private int key;

public MyClassLoader(int k) {
this.key = k;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classBytes = loadClassByBytes(name);
Class<?> cls = defineClass(name, classBytes, 0, classBytes.length);
if (cls == null) throw new ClassNotFoundException(name);
return cls;
} catch (IOException ex) {
throw new ClassNotFoundException(name);
}
}
// 读取 .class 字节码
private byte[] loadClassByBytes(String name) throws IOException {
String cname = name.replace('.', '/') + ".class";
System.out.println(cname);
byte[] bytes = Files.readAllBytes(Paths.get(cname));
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (bytes[i] - key);
}
return bytes;
}
}
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
28
29
30
package com.test5;

import java.lang.reflect.Method;

public class ClassLoaderTest {

public void runClass(String name, String key) {
try {
ClassLoader loader = new MyClassLoader(Integer.parseInt(key));
Class<?> cls = loader.loadClass(name);
// main方法
Method m = cls.getMethod("main", String[].class);
m.invoke(null, (Object) new String[]{"hello", "world"});
} catch (Throwable e) {
System.out.println(e.getMessage());
}
}

public static void test() {
System.out.println("this is a test()");
}

public static void main(String[] args) {
test();
for (String str : args) {
System.out.println("--> " + str);
}
}
}

1
2
3
4
5
6
7
package com.test5;

public class Test8 {
public static void main(String[] args) {
new ClassLoaderTest().runClass("com.test5.ClassLoaderTest", "0");
}
}

输出

1
2
3
this is a test()
--> hello
--> world

URLClassLoader 类

一 旦得到了 URLClassLoader 对象之后,就可以调用该对象的 loadClass() 方法来加载指定类。

注意,加载时需要指定协议头 protocol,比如 file://

  • URLClassLoader
    • loadClass()
  • newInstance() 创建了一个类的默认实例
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
28
29
30
31
32
33
34
35
36
37
38
39
package com.test6;

import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Properties;

public class URLClassLoaderTest {
// 注意需要 file:// 协议头
private final static String FILE = "file://basic/res/libs/mysql-connector-java-8.0.26.jar";
private final static String db_url = "jdbc:mysql://localhost:3306/db_test";

private static Connection conn;

public static void main(String[] args) throws Exception {
URL[] urls = {new URL(FILE)};
// 加载器类
// 从文件中 加载MySQL驱动
URLClassLoader loader = new URLClassLoader(urls);
// 加载指定的类名
Driver driver = (Driver) loader.loadClass("com.mysql.cj.jdbc.Driver").getConstructor().newInstance();

Properties properties = new Properties();
properties.setProperty("user", "root");
properties.setProperty("password", "fuckingyou");
// 连接到数据库
conn = driver.connect(db_url, properties);
Statement stmt = conn.createStatement();
ResultSet set = stmt.executeQuery("SELECT *FROM tb_test;");
while (set.next()) {
System.out.println(set.getString(1) + "\t" + set.getString(2));
}
set.close();
conn.close();
}
}

字节码校验

当类加载器将新加载的 Java 平台类的字节码传递给虚拟机时,这些字节码首先要接受校验器 (verifier) 的校验。

  • 变量要在使用之前进行初始化
  • 方法调用与对象引用类型之间要匹配
  • 访问私有数据和方法的规则没有被违反
  • 对本地变量的访问都落在运行时堆樵内
  • 运行时堆拢没有溢出

安全管理器与访问权限

java.security.Permission

  • boolean implies(Permission other): 检查该权限是否隐含了 other 权限

用户认证

JAAS 框架

Java 认证和授权服务:“认证”部分主要负责确定程序使用者的身份,而“授权”将各个用户映射到相应的权限

数字签名 MessageDigest

支持 MD5, SHA-1, SHA-256, SHA-384 和 SHA-512

MessageDigest 抽象基类是用于创建封装了指纹算法的对象的 “工厂”,它的静态方法 getInstance 返回继承了 MessageDigest 类的某个类的对象。

在获取 MessageDigest 对象之后,可以通过 反复调用 update 方法,将信息中的所有字节提供给该对象

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
28
package com.test5;

import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Arrays;

public class DigestTest {
public static void main(String[] args) throws Exception {
MessageDigest digest = MessageDigest.getInstance("MD5");

byte[] data = Files.readAllBytes(Path.of("basic/res/test.xml"));
digest.update(data);
byte[] hash = digest.digest();

System.out.println(digest);
System.out.println(Arrays.toString(hash));

StringBuilder d = new StringBuilder();
for (byte b : hash) {
int h = (b & 0xFF);
if (h < 16) d.append("0");
// 十六进制
d.append(Integer.toString(h, 16).toUpperCase());
}
System.out.println(d);
}
}

加密 Cipher

对称密码
  • Cipher.getInstance()
  • KeyGenerator.getinstance()

一旦获得了 一个密码对象,就可以通过 设置模式和密钥 来对它初始化

SecureRandom 类产生的随机数远比由 Random 类产生的那些数字安全得多

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.test5;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.io.*;
import java.security.Key;
import java.security.SecureRandom;
import java.util.Arrays;

public class AESTest {
public static void main(String[] args) throws Exception {
// 密钥生成
KeyGenerator keygen = KeyGenerator.getInstance("AES");
// 随机密钥
SecureRandom random = new SecureRandom();
// 对密钥生成器进行初始化
keygen.init(random);
// 生成一个新的密钥
SecretKey secretKey = keygen.generateKey();
System.out.println(Arrays.toString(secretKey.getEncoded()));

// 加密 ENCRYPT_MODE
// 解密 DECRYPT_MODE
int mode = Cipher.ENCRYPT_MODE;
Key key = secretKey;

Cipher cipher = Cipher.getInstance("AES");
// 对加密算法对象进行初始化 mode, key
cipher.init(mode, key);

// 返回密码块的大小
int blockSize = cipher.getBlockSize();
int outputSize = cipher.getOutputSize(blockSize);
byte[] inBytes = new byte[blockSize];
byte[] outBytes = new byte[outputSize];
System.out.println("blockSize: " + blockSize);
System.out.println("outputSize: " + outputSize);

InputStream in = new FileInputStream("basic/res/test.xml");
OutputStream out = new FileOutputStream("basic/res/test.xml.enc");

int inLen = 0;
boolean more = true;
while (more) {
// 读取文件内容
inLen = in.read(inBytes);
System.out.println("read in size: " + inLen);
if (inLen == blockSize) {
// 对输入数据块进行转换
int outLen = cipher.update(inBytes, 0, blockSize, outBytes);

System.out.println("-> in size: " + inLen + ", out size: " + outLen);

// 写出文件
out.write(outBytes, 0, outLen);
} else more = false;
}
// 最后填充处理
// 转换输入的最后一个数据块,并刷新该加密算法对象的缓冲
if (inLen > 0)
outBytes = cipher.doFinal(inBytes, 0, inLen);
else
outBytes = cipher.doFinal();
out.write(outBytes);
}
}

输出

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
28
29
30
[14, -56, -1, -79, -120, 11, -26, 76, 83, 80, -27, -6, 91, 96, 8, 65]
blockSize: 16
outputSize: 32
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 16
-> in size: 16, out size: 16
read in size: 7
密码流

JCE 库提供了 一组使用便捷的流类,用于对流数据进行自动加密或解密。密码流类能够透明地调用 updatedoFinal 方法

  • CipherInputStream
    • read 读取输入流中的数据,该数据会被自动解密和加密
  • CipherOutputStream
    • write 将数据写入输出流,该数据会被自动加密和解密
    • flush 刷新密码缓冲 区,如果需要的话,执行填充操作

公共密钥密码 RSA

所有已知的公共密钥算法的操作速度都比对称密钥算法,使用公共密钥算法对大量的信息进行加密是不切实际的。

  • KeyPairGenerator
  • KeyPair
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.test5;

import javax.crypto.Cipher;
import java.io.*;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;

public class RSATest {
private final static int KEY_SIZE = 512;
private final static String DIR = "basic/res/";

public static void genKey(String pubKeyName, String priKeyName) throws Exception {
KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
SecureRandom random = new SecureRandom();
keygen.initialize(KEY_SIZE, random);

KeyPair keyPair = keygen.generateKeyPair();
Key pubKey = keyPair.getPublic();
Key priKey = keyPair.getPrivate();

try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(DIR + pubKeyName))) {
out.writeObject(pubKey);
}
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(DIR + priKeyName))) {
out.writeObject(priKey);
}
}

// 公钥加密
public static void encrypt(String src, String dest, String pubKeyName) throws Exception {
try (ObjectInputStream pubKeyIn = new ObjectInputStream(new FileInputStream(DIR + pubKeyName));
DataOutputStream out = new DataOutputStream(new FileOutputStream(DIR + dest));
InputStream in = new FileInputStream(DIR + src)) {
Key pubKey = (Key) pubKeyIn.readObject();
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.WRAP_MODE, pubKey);

byte[] wrappedKey = cipher.wrap(pubKey);
out.writeInt(wrappedKey.length);
out.write(wrappedKey);

cipher=Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE,pubKey);
// Util.crype(in, out, cipher);
}
}

// 私钥解密
public static void decrypt(String src, String dest, String priKeyName) throws Exception {
try (ObjectInputStream priKeyIn = new ObjectInputStream(new FileInputStream(DIR + priKeyName));
DataInputStream in = new DataInputStream(new FileInputStream(DIR + dest));
OutputStream out = new FileOutputStream(DIR + src)) {
int length = in.readInt();
byte[] wrappedKey = new byte[length];
in.read(wrappedKey, 0, length);

Key priKey = (Key) priKeyIn.readObject();
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.UNWRAP_MODE, priKey);

Key key = cipher.unwrap(wrappedKey, "AES", Cipher.SECRET_KEY);

cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);

// Util.crype(in, out, cipher);
}
}

public static void main(String[] args) throws Exception {
genKey("pubKey", "priKey");
}
}

本地方法 native

Java 程序中调用 C/C++ 函数

关键字native 提醒编译器该方法将在外部定义。

1
2
3
4
5
package com.test6;

public class HelloNative {
public static native void greeting();
}

为了实现本地代码,需要编写一个相应的 C 函数,你必须完全按照 Java 虚拟机预期的那样来命名这个函数:

  • 使用完整的 Java 方法名,包括包名
  • 下划线 替换掉所有的 句号 .,并加上 Java_ 前缀
  • 如果类名含有非ASCII 字母或数字,用 _0xxxx 来替代它们,xxxx 是该字符的 Unicode 值的 4 个十六进制数序列
  • 如果重载了本地方法,必须在名称后附加两个下划线 __,后面再加上 己编码的参数类型

比如上面的为: Java_com_test6_HelloNative_greeting

可以使用 javah 来生成函数名(Java8可以使用,高版本不行!)或者 javac -h

1
2
3
4
5
6
7
# pwd: /home/joxrays/Code_Projects/java_projects/my_learn_java/basic/src
# 编译
/usr/lib/jvm/java-8-openjdk/bin/javac com/test6/HelloNative.java
# 生成函数头文件
/usr/lib/jvm/java-8-openjdk/bin/javah com.test6.HelloNative

javac -h headers src/com/test6/printf1/Printf1.java

比如下面的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_test6_HelloNative */

#ifndef _Included_com_test6_HelloNative
#define _Included_com_test6_HelloNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_test6_HelloNative
* Method: greeting
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_test6_HelloNative_greeting
(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

之后就可以在源文件实现该函数了。

1
2
3
4
5
6
7
#include "com_test6_HelloNative.h"
#include <stdio.h>

extern "C"
JNIEXPORT void JNICALL Java_com_test6_HelloNative_greeting(JNIEnv *, jclass) {
printf("Hello World!\n");
}

然后将本地 C 代码编译到一个动态装载库中,具体方法依赖于编译器。

1
2
# 生成 .so 动态链接库
gcc -fPIC -I /usr/lib/jvm/java-8-openjdk/include -I /usr/lib/jvm/java-8-openjdk/include/linux -shared -o libHelloNative.so com_test6_HelloNative.c

如果是macOS,注意扩展名为 dylib

1
gcc -fPIC -I /opt/homebrew/Cellar/openjdk/17.0.1/libexec/openjdk.jdk/Contents/Home/include -I /opt/homebrew/Cellar/openjdk/17.0.1/libexec/openjdk.jdk/Contents/Home/include/darwin -shared -o libPrintf1.dylib csrc/com_test6_printf1_Printf1.cpp

最后,我们要在程序中添加一个对 System.loadLibrary 方法的调用,为了确保虚拟机在第一次使用该类之前就会装载这个库,需要使用静态初始化代码块

1
2
3
4
5
6
7
8
9
10
11
12
package com.test6;

public class HelloNativeTest {
static {
// 载入链接库
System.loadLibrary("HelloNative");
}

public static void main(String[] args) {
HelloNative.greeting();
}
}

编译后运行,如果运行在 Linux 下,必须把当前目录添加到库路径中

  • export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
  • java -Djava.library.path=. com.test6.HelloNativeTest 设置系统属性
1
2
3
4
5
6
% pwd
/home/joxrays/Code_Projects/java_projects/my_learn_java/out/production/basic
% ls -l libHelloNative.so
-rwxr-xr-x 1 joxrays joxrays 15448 8月 24 23:59 libHelloNative.so
% java -Djava.library.path=. com.test6.HelloNativeTest
Hello World!

如果使用IDEA,那么可以使用:

  • pwd /Users/xraysjoseph/CodeProjects/Code/java_projects/my_learn_java/out/production/basic
  • java -Djava.library.path=com/test6/printf1/ com.test6.printf1.Test

总之方法就是:

  • 在 Java 类中声明一个 本地native方法
  • 运行 javah 以获得包含该方法的 C 声明的头文件
  • 用 C 实现该本地方法
  • 将代码置于 共享类库
  • 在 Java 程序中 加载该类库

一些本地代码的共享库必须先运行初始化代码,你可以把初始化代码放到 JNI_OnLoad 方法中 。 类似地,如果你提供该方法,当虚拟机关闭时,将会调用JNI_OnUnload 方法 。 它们的原型是:

  • jint JNI_OnLoad(JavaVM* vm, void* rese 「ved);
  • void JNI_OnUnload(JavaVM* vm, void* reserved);
数值参数与返回值
1
2
3
4
5
6
7
8
9
10
package com.test6.printf1;

public class Printf1 {
// 本地方法
public static native int print(int width, int precision, double x);

static {
System.loadLibrary("Printf1");
}
}
1
2
3
4
5
6
7
8
package com.test6.printf1;

public class Test {
public static void main(String[] args) {
int x = Printf1.print(8, 4, 3.14);
System.out.println(x);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_test6_printf1_Printf1 */

#ifndef _Included_com_test6_printf1_Printf1
#define _Included_com_test6_printf1_Printf1
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_test6_printf1_Printf1
* Method: print
* Signature: (IID)I
*/
JNIEXPORT jint JNICALL Java_com_test6_printf1_Printf1_print
(JNIEnv *, jclass, jint, jint, jdouble);

#ifdef __cplusplus
}
#endif
#endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include"../cheaders/com_test6_printf1_Printf1.h"
#include<iostream>
#include<stdio.h>
using namespace std;

extern "C" JNIEXPORT jint JNICALL Java_com_test6_printf1_Printf1_print
(JNIEnv *, jclass, jint width, jint precision, jdouble x) {
char fmt[30];
jint ret;
sprintf(fmt,"%%%d.%df",width,precision);
ret = printf(fmt,x);
fflush(stdout);
return ret;
}
1
2
3
4
5
6
javac -h headers src/com/test6/printf1/Printf1.java
g++ -fPIC -I /usr/lib/jvm/default/include/ -I /usr/lib/jvm/default/include/linux -shared -o libPrintf1.so csrc/com_test6_printf1_Printf1.cpp

mv libPrintf1.so ../out/production/basic/com/test6/printf1/
cd ../out/production/basic/
java -Djava.library.path=com/test6/printf1/ com.test6.printf1.Test

将生成的 libPrintf1.so 放到 com/test6/printf1/ 目录下,然后 -Djava.library.path=com/test6/printf1/ 就可以加载该目录下的库文件了。

字符串参数

Java 编程语言中的字符串是 UTF-16 编码点的序列,而 C 的字符串则是以 null 结尾的字节序列。 Java 本地接口有两组操作 字符串的函数,一组把 Java 字符串转换成“改良的 UTF-8 ”字节序列,另一组将它们转换成 UTF-16 数值的数组。

带有字符串参数的本地方法实际上都要接受一个 jstring 类型的值,而带有字符串参数返回值的本地方法必须返回一个 jstring 类型的值

NewStringUTF 函数会从包含 ASCII 字符的字符数组,或者是更一般的“改良的 UTF-8 ”编码的字节序列中,创建一个新的 jstring 对象。

NewStringUTF 函数可以用来构造一个新的 jstring ,而读取现有 jstring 对象的内容,需要使用 GetStringUTFChars 函数。该函数返回指向描述字符串的“改良 UTF-8 ”字符的 const jbyte * 指针。

下面是 C++ 对JNI的访问,比较简单。因为 JNIEnv 类的 C++版本有一个内联成员,负责帮你查找函数指针。

1
2
3
4
5
6
7
8
9
10
extern "C" JNIEXPORT jstring JNICALL Java_com_test6_printf1_Printf1_getString
(JNIEnv *env, jclass cls, jstring str) {
jstring jstr=str;
const char *s = "Hello World\n";
// 如果是 C 代码,那么就是
// jstr = (*env)->NewStringUTF(enc,s);
// C++ 代码的话,忽略了该调用的参数列表里的 JNIEnv 指针
jstr = env->NewStringUTF(s);
return jstr;
}

具体接口可以查看 /usr/lib/jvm/default/include/jni.h

访问域

在对象上进行操作的本地方法。静态方法得到的是类的引用 jclass,而非静态方法得到的是对隐式的 this 参数对象的引用 jobject

  • 修改或设置字段值
    • env->GetXXXField(jobject, fieldID)/env->SetXXXField(jobject, fieldID, x) 设置或获取对象字段值
    • env->GetStaticXXXField(jclass ,fieldID) / env->SetStaticXXXField(jclass ,fieldID, x) 设置或获取静态域
  • 获取 jclass
    • jclass = env->GetObjectClass(jobject) 返回任意对象的类
    • jclass = env->FindClass("java/lang/String") 以字符串形式来指定类名并返回该对象的类,用于静态域
  • 获取 jfieldID
    • jfieldID = env->GetFieldID(jclass, "age", "I") 获取字段ID
    • jfieldID = env->GetStaticFieldID(jclass, "", "") 获取静态字段ID
  • 类引用只在本地方法返回之前有效
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
28
29
30
31
32
33
34
35
package com.test6.employee;

public class Employee {
private String name;
private int age;

public native void test(int x);

public Employee(String name, int age) {
this.name = name;
this.age = age;
}

public void print() {
System.out.println(name + "\t" + age);
}

static {
System.loadLibrary("Employee");
}
}


package com.test6.employee;

public class Test {
public static void main(String[] args) {
Employee e = new Employee("Mike", 32);
e.print();
// 修改 age = 1000
e.test(1000);

e.print();
}
}
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
28
29
#include <jni.h>
#ifndef _Included_com_test6_employee_Employee
#define _Included_com_test6_employee_Employee
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_com_test6_employee_Employee_test(JNIEnv *, jobject, jint);
#ifdef __cplusplus
}
#endif
#endif


#include "../cheaders/com_test6_employee_Employee.h"
#include <stdio.h>
#include <string.h>
extern "C" JNIEXPORT void JNICALL Java_com_test6_employee_Employee_test(
JNIEnv *env, jobject this_obj, jint x){

// 获取类
jclass cls_employee = env->GetObjectClass(this_obj);
// 读取字段值
jfieldID id_age = env->GetFieldID(cls_employee, "age", "I");
jint age = env->GetIntField(this_obj, id_age);

age = x;
// 设置字段值
env->SetIntField(this_obj, id_age, age);
}
编码签名
编码 类型
B byte
C char
D double
F float
I int
J long
Lclassname; 类的类型。在 L 表达式结尾处的分号是类型表达式的终止符
S short
V void
Z bool
[Ljava/lang/String String[] 数组
[[F float[][]数组
(Ljava/lang/String;D)Ljava/lang/String; 该方法接收一个String和double,返回Strign
(Ljava/lang/String;DLjava/util/Date;)V 构造器 Employee(java.lang.String, double, java.Util.Date)

可以使用 javap -s 命令来从类文件中产生方法签名,即 输出内部类型签名

调用 Java 方法

从本地代码调用 Java 方法,一般是在动态链接库中调用 Java的对象方法。这时,具体的实现代码是在java中。

实例方法
  • env->GetObjectClass(out)

  • env->GetMethodID(jclass, "print", "(Ljava/lang/String;)V")

  • env->CallXXXMethod(implicit_parameter, methodID, explicit_parameters)

静态方法
  • env->GetStaticMethodID
  • env->FindClass
  • env->CallStaticXXXMethod
构造器

本地方法可以通过调用构造器来创建新的 Java 对象

  • env->NewObject(jclass, methodID, construction_parameters)
  • env->GetMethodID(jclass, "<init>", "(...)V") 指定构造器方法
访问数组元素

jarray 类型表示一个通用数组

  • env->GetArrayLength(jarray)
  • env->GetObjectArrayElement/SetObjectArrayElement
  • env->GetXXXArrayElements() 返回 一个指向数组起始元素的 C 指针,不需要时调用 env->ReleaseXxxArrayElements()
错误处理

要使用 Throw 函数,需要调用 NewObject 来创建一个 Throwable 子类的对象,或者直接 env->ThrowNew()

Throw 和 ThrowNew 都仅仅只是发布异常,它们不会中断本地方法的控制流。只有当该方法返回时,Java 虚拟机才会抛出异常。

如果用 C实现本地方法,那么就 无法用你的 C代码抛出 Java 异常

便用调用 API

调用 API(invocationAPI) 能够把 Java 虚拟机嵌入到 C 或者 C++程序中。

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
28
29
30
31
32
33
34
35
36
37
38
39
#include "jni.h"
#include <stdio.h>
#include <string.h>

int main() {
JavaVMOption options[2];
JavaVMInitArgs vm_args;
JavaVM *jvm;
JNIEnv *env;

long status;
jclass class_Welcome;
jclass class_String;
jobjectArray args;
jmethodID id_main;

options[0].optionString = "-Djava.class.path=.";
memset(&vm_args, 0, sizeof(vm_args));
vm_args.version = JNI_VERSION_10;
vm_args.nOptions = 1;
vm_args.options = options;

status = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
if (status == JNI_ERR) {
fprintf(stderr, "Error creating VM\n");
return 1;
}
class_Welcome = env->FindClass("Welcome");
id_main =
env->GetStaticMethodID(class_Welcome, "main", "([Ljava/lang/String;)V");

class_String = env->FindClass("java/lang/String");
args = env->NewObjectArray(0, class_String, NULL);

// 调用main方法
env->CallStaticVoidMethod(class_Welcome, id_main, args);
jvm->DestroyJavaVM();
return 0;
}
1
2
3
4
5
public class Welcome {
public static void main(String[] args) {
System.out.println("hello world");
}
}

首先需要编译 Welcome.java 得到 Welcome.class

确保Java源文件和C源文件在同一个目录下,因此C代码中指定了Java类路径为 -Djava.class.path=.

1
javac Welcome.java

然后编译 cpp 文件

1
g++ -I /usr/lib/jvm/default/include/ -I /usr/lib/jvm/default/include/linux -L /usr/lib/jvm/default/lib/server/ -ljvm invocationTets.cpp -o invocationTest

需要注意的是:

  • 头文件库:
    • /usr/lib/jvm/default/include/
    • /usr/lib/jvm/default/include/linux
  • 共享链接库:
    • /usr/lib/jvm/default/lib/server/libjvm.so

最后还要需要确保 LD_LIBRARY_PATH 包含了共享类库的目录

1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/lib/jvm/default/lib/server/

最后运行 invocationTest

1
2
3
./invocationTest
# 输出
hello world

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!