文本是对《Java 核心技术卷II》做的第二篇笔记
网络
套接字超时
Socket.setSoTimeout()
SocketTimeoutException
超时异常
超时连接
可以通过先构建一个无连接的套接字,然后再使用一个超时来进行连接的方式解决
Socket s = new Socket(); s.connect(new InetSocketAddress(host, port), timeout);
InetAddress
一些访问量较大 的主机名通常会对应于多个因特网地址,以实现负载均衡。因此 InetAddress.getByName
会随机随机选择一个。
可以用 InetAddress.getAllByName
获取域名下的所有主机
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
类 。
public static void main (String[] args) throws IOException { try (SocketChannel socket = SocketChannel.open( new InetSocketAddress(InetAddress.getByName("www.bilibili.com" ), 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
URL
和 URLConnection
类封装了大量复杂的实现细节,这些细节涉及如何从远程站点获取信息
在 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()); 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 中指定的子协议的驱动程序。
使用 JDBC 语句
Connection.createStatemen
Statement.executeUpdate
返回受 SQL 语句影响的行数
Statement.getResultSet()
返回 前一条查询语句的结果集 。如果前一条语句未产生结果集,则返回 null 值
executeUpdate
方法既可以执行诸如 INSERT
、UPDATE
和 DELETE
之类的操作,也可以执行诸如 CREATE TABLE
和 DROP 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 { 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 { 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=?" )) { 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(); crs.setUrl(SQLTest.db_url); crs.setUsername(SQLTest.user); crs.setPassword(SQLTest.pwd); crs.setPageSize(1 ); 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
ResultSet res = stmt.executeQuery(); RowSetFactory factory = RowSetProvider.newFactory(); CachedRowSet crs = factory.createCachedRowSet(); crs.populate(res); conn.close();
元数据
我们可以获得三类元数据 :
关于数据库的元数据,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
是两个时刻之间的时间量。
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 )); } }
输出
PT1 .500160471 S2021 -08 -18 2121 -11 -26 P -100 Y-3 M-8 D2021 -02 -28
日期调整器 TemporalAdjusters
本地时间 LocalTime
LocalTime 表示当日时刻
LocalDateTime 表示日期和时间
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
格式化 LocalDate -> String
DateTimeFormatter.ofPattern("").format(LocalDate.now())
LocalDate.now().format(DateTimeFormatter.ofPattern(""))
解析 + 格式化 String -> LocalDate
LocalDate.parse("", DateTimeFormatter.ofPattern(""))
LocalDate.parse("")
DateTimeFormatter 类提供了 三种用于打印日期/ 时间值的格式器
使用旧的Date
对象时,我们用SimpleDateFormat
进行格式化显示。使用新的LocalDateTime
或ZonedLocalDateTime
时,我们要进行格式化显示,就要使用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); }
输出
2021-08 -18 19:30:29 周三 2021-08 -18 19:30:29 2021/8/18 下午7:30
与遗留代码的互操作
国际化
数字恪式
Java 类库提供了一个格式器(formatter)对象的集合,可以对 java.text
包中的数字值进行 格式化和解析
创建 Locale 对象
使用一个“工厂方法”得到一个格式器对象
使用这个格式器对象来完成格式化和解析工作
getNumberInstance,对数字格式化
getCurrencyInstance,对货币量格式化
getPercentinstance,对百分比格式化
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 )); } }
使用占位符 {}
进行字符串的格式化
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 表示成功,否则将错误信息输出到错误流中
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()); 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 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")
在注解中引入 循环依赖 是一种错误
@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
中注解的,该文件只包含以注解先导的包语句,比如:
@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
将同种类型的注解多次应用于某一项是合法的,可重复注解的实现者需要提供 一个容器注解 ,它可以 将这些重复注解存储到一个数组中
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(); }
一般较为常用的是以下几个
@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" )); 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); } } 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); 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); } } }
package com.test5;public class Test8 { public static void main (String[] args) { new ClassLoaderTest().runClass("com.test5.ClassLoaderTest" , "0" ); } }
输出
this is a test() --> hello --> world
URLClassLoader 类
一 旦得到了 URLClassLoader
对象之后,就可以调用该对象的 loadClass()
方法来加载指定类。
注意,加载时需要指定协议头 protocol
,比如 file://
URLClassLoader
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 { 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)}; 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())); int mode = Cipher.ENCRYPT_MODE; Key key = secretKey; Cipher cipher = Cipher.getInstance("AES" ); 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 库提供了 一组使用便捷的流类,用于对流数据进行自动加密或解密 。密码流类能够透明地调用 update
和 doFinal
方法
CipherInputStream
read
读取输入流中的数据,该数据会被自动解密和加密
CipherOutputStream
write
将数据写入输出流,该数据会被自动加密和解密
flush
刷新密码缓冲 区,如果需要的话,执行填充操作
公共密钥密码 RSA
所有已知的公共密钥算法的操作速度都比对称密钥算法,使用公共密钥算法对大量的信息进行加密是不切实际的。
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); } } 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); } } public static void main (String[] args) throws Exception { genKey("pubKey" , "priKey" ); } }
本地方法 native
Java 程序中调用 C/C++ 函数
关键字native
提醒编译器该方法将在外部定义。
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
/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 #include <jni.h> #ifndef _Included_com_test6_HelloNative #define _Included_com_test6_HelloNative #ifdef __cplusplus extern "C" {#endif JNIEXPORT void JNICALL Java_com_test6_HelloNative_greeting (JNIEnv *, jclass) ;#ifdef __cplusplus }#endif #endif
之后就可以在源文件实现该函数了。
#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 代码编译到一个动态装载库中,具体方法依赖于编译器。
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
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
方法的调用,为了确保虚拟机在第一次使用该类之前就会装载这个库,需要使用静态初始化代码块
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
设置系统属性
% 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);
数值参数与返回值
package com.test6.printf1;public class Printf1 { public static native int print (int width, int precision, double x) ; static { System.loadLibrary("Printf1" ); } }
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 #include <jni.h> #ifndef _Included_com_test6_printf1_Printf1 #define _Included_com_test6_printf1_Printf1 #ifdef __cplusplus extern "C" {#endif JNIEXPORT jint JNICALL Java_com_test6_printf1_Printf1_print (JNIEnv *, jclass, jint, jint, jdouble) ;#ifdef __cplusplus }#endif #endif
#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; }
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++版本有一个内联成员,负责帮你查找函数指针。
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" ; 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(); 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 ); env->CallStaticVoidMethod (class_Welcome, id_main, args); jvm->DestroyJavaVM (); return 0 ; }
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=.
然后编译 cpp 文件
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
包含了共享类库的目录
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH :/usr/lib/jvm/default/lib/server/
最后运行 invocationTest
./invocationTest hello world