Java 高级编程、I/O File、JMX、硬件OSHI 日志/Logger NetBeans CDI JSON
重要
判 null:
// 必不会为 null,仅为了抑制 NetBeans 警告 Dereferencing possible null pointer.
Objects.requireNonNull(obj); obj.callMethod();
// 运行时异常捕获 catch (IOException | RuntimeException ex) { }
转基类避免运行时异常:
// HttpsURLConnection 可能报运行时 ClassCastException,故先转为基类用。
var conn = (HttpURLConnection) url.openConnection();
JMX/MBean
JMX - Java Management Extensions
MBean - Jakarta Managed Beans @ManagedBean 是 CDI Bean 的过时“阉割版”,限于 @Dependent 范围且不支持 @Observes。
MXBean - 客户端侧非强依赖,可转换为 CompositeData 按 Key 访问。
JConsole - 读写已注册了 JMX 的 JVM 程序,位于 bin/jconsole.exe。
K/V、缓存、CDN
Ehcache - K/V 缓存库
implementation("org.ehcache.modules:ehcache-api:3.12.0")
implementation("org.ehcache.modules:ehcache-core:3.12.0")
implementation("org.ehcache.modules:ehcache-impl:3.12.0") {
//exclude(group = "org.terracotta")
}
// SPI 发现该库就会自动桥接 Ehcache 至 JCache API,即 Caching.getCachingProvider().getCacheManager(); // Eh107CacheManager[urn:X-ehcache:jsr107-default-config]。
implementation("org.ehcache.modules:ehcache-107:3.12.0") // 默认已启用监测并自动注册至 JMX,可代码关闭:
_CacheConfigurationBuilder.withService(new Jsr107CacheConfiguration(ConfigurationElementState.ENABLED, ConfigurationElementState.ENABLED));
implementation("org.ehcache.modules:ehcache-management:3.12.0") // 需手动注册监控 MBean 至 JMX:
CacheManagerBuilder.using(new DefaultManagementRegistryService(new DefaultManagementRegistryConfiguration().setCacheManagerAlias("myCM")))
ManagementFactory.getPlatformMBeanServer().registerMBean(_ManagementRegistryService, new ObjectName("org.ehcache:type=CacheManager,name=myCM"));
CDN:
RFC 9111 将 CDN 称为共享缓存(Shared Cache),浏览器则为私有缓存(Private Cache),s-maxage 只会被前者识别。
Cache-Control 头优先级比 Last-Modified 高。
Cache-Control: no-cache 不会走 200 (from disk cache),总是会交互下后端。
部分云存储对象(OCI 对象存储)按读写次数消耗,尽量避免 Cache-Control: no-cache,额度按月重置,故设个 30 多天(max-age=3000000)。
文件名不敏感时,可通过改名规避回源,然后缓存约 11 天的整数值 Cache-Control: public, max-age=1000000。
经过 CDN 后响应头会多一个 Age,告知浏览器,从第一棒 CDN 拉取算,后续 CDN 接力累加的相对秒数,规避了固定时间的校准。
Java httpclient5-cache:
缓存状态 VALIDATED 指复用了 Body,但 Header 交互了下,必须存在 Cache-Control 才能命中强缓存 CACHE_HIT。
Cyberduck S3-like 对象存储客户端支持在文件的元数据里,添加 Cache-Control 头,默认 30 天。
httpclient5-cache 允许请求加个 Authorization 头,会在内部剥离掉,避免影响缓存。
OCI 对象存储 -> Create private endpoint(需升级版账号!) -> Name: cdn-source DNS Prefix: cdn-source [可选] Add access target(默认*通配所有存储桶) -> Create... 就拿到私网端点 URL 了。
非升级版账号只能创建个 VCN 的 Service Gateway,来降级公共端点的访问范围了。
IO File
支持 ~/ 家目录展开: cn.hutool.core.io.FileUtil.file("~/.gradle/");
方案:
背压式双缓冲队列 - 将“不稳定的网络流”转化为“稳定和缓存较小的内存块”,同时避免了全量载入内存,比单缓冲的 BufferedInputStream 多一份。
尾参指初始容量,会扩充至 ReadLimit - new BufferedInputStream(stream, 64 * 1024).mark(_ReadLimit);
JSON
新:
JEP 540: Simple JSON API - Java 官方 JSON 处理内置实现。
Json.parse(body).get("properties").get(0).get("temperature").asInt();
JSON-B: 序列化 Java Class 或 Record 对象,依赖包 tools.jackson.core:jackson-databind;Jackson 3 已融合 java.time.* 序列化包 jackson-datatype-jsr310。
JSON-P: 即 Jakarta JSON Processing,逐字段构造 JSON 对象,依赖包 org.eclipse.parsson:jakarta.json。
CDI
方案:
Jetty + Weld、GraalVM + Jetty + Avaje-Inject(Weld无法AOT)、Quarkus + Arc。
Android 则选用官方 com.google.dagger:hilt-android。
Weld SE:
// gradle就地执行Java程序时,还没到jar打包beans.xml阶段,故将其拷贝至classes路径,使Weld能感知到它。
val copyRes by tasks.registering(Copy::class) {
from(layout.projectDirectory.file("src/main/resources/META-INF"))
into(layout.buildDirectory.dir("classes/java/main/META-INF"))
} // 亲测 bean-discovery-mode="all" 才能解决 WELD-ENV-002009。
tasks.processResources { dependsOn(copyRes) }
tasks.jar { // 避免因重复文件(beans.xml)而构建失败。
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
依赖库:
implementation("org.jboss.weld.se:weld-se-core:6.0.3.Final")
implementation("io.smallrye:jandex:3.4.0") // for weld
var wc = new Weld().initialize();
System.out.println("Weld - " + wc.isRunning());
//wc.select(MyClass.class).get().method();
wc.shutdown();
// 测试CDI Events监听:
调用 StartMain.main(null); // 或 java -cp app.jar:weld-se-shaded.jar org.jboss.weld.environment.se.StartMain
@Singleton
public class InitCDI {
public void initCDI(@Observes ContainerInitialized event, @Parameters List parameters) {
System.out.println("InitCDI " + parameters);
}
}
未标注CDI范围的普通类可使用静态方法注入:
CDI.current().select(InterfaceCDI.class, new AnnotationLiteral() { }).get();
//CDI.current().select(RequestContext.class, UnboundLiteral.INSTANCE).get().activate()
Weld Servlet:
原理 -
实现了 ServletContainerInitializer 的 Listener 会在 Jetty Server 启动时触发,并:
org.jboss.weld.environment.servlet.Listener.contextInitialized(ServletContextEvent sce)
WeldServletLifecycle.initialize(ServletContext context)
createDeployment(...) 负责扫描 CDI 注解并收集为 Beans 列表;strategy.performDiscovery().size() != 0。
context.getAttribute(Listener.CONTAINER_ATTRIBUTE_NAME) instanceof ContainerInstanceFactory
CONTAINER_ATTRIBUTE_NAME 属性无值则会 new WeldBootstrap().startContainer(...);
可直接传入 sch.addEventListener(Listener.using(weld)),其内部进行了 setAttribute(Listener.CONTAINER_ATTRIBUTE_NAME, weld实例或工厂)
WebAppBeanArchiveScanner.java scan() 底部可输出可用 beans.xml 路径和版本:
results.stream().forEach(x -> { IO.println(x.getBeansXml().getVersion() + " | " + x.getBeansXml().getUrl()); });
GraalVM 原生构建时,路径会加个 Weld 不识别的协议前缀 resource:/META-INF/beans.xml,故需要 registerHandler(BeanArchiveHandler handler) 兼容下;由于 Jandex 也不支持,故可禁之 -Dorg.jboss.weld.environment.deployment.discovery.jandex=false -Dorg.jboss.weld.discovery.disableJandexDiscovery=true。
NetBeans
IDE 注入的环境变量: if (System.getenv("NETBEANS_USERDIR") != null) { }
通用
静态方法里获取所在类:
MethodHandles.lookup().lookupClass();
带行号所在方法 - StackWalker.getInstance().walk(Stream::findFirst).get() // pkg.App.main(App.java:67)
或 new Throwable().getStackTrace()[0];
守护线程:
var es = Executors.newSingleThreadScheduledExecutor(runnable -> {
var t = new Thread(runnable, "auto-shutdown-thread");
t.setDaemon(true);
return t;
});
es.schedule(() -> {
System.out.println("setDaemon 守护线程不阻止 JVM 正常退出!");
}, seconds, TimeUnit.SECONDS);
日志:
static final LazyConstant<System.Logger> LOGGER = LazyConstant.of(() -> {
var logger = System.getLogger("global"); // Logger.getGlobal()
var loggerImpl = LogManager.getLogManager().getLogger("global");
loggerImpl.setLevel(Level.ALL);
// [可选]控制台输出
var ch = new ConsoleHandler();
ch.setLevel(Level.ALL);
loggerImpl.addHandler(ch);
try {
var fh = new FileHandler("global.log");
fh.setLevel(Level.ALL);
fh.setFormatter(new SimpleFormatter()); // 默认 XML。
loggerImpl.addHandler(fh);
} catch (IOException e) {
e.printStackTrace(System.err);
}
return logger;
}); //用处 LOGGER.get().log(System.Logger.Level.ALL, "msg..."); LOGGER.get().log(Logger.Level.ALL, MethodHandles.lookup().toString(), ex);
if (logger.isLoggable(Logger.Level.INFO)) {
logger.log(Logger.Level.INFO, "Msg: {0}", Arrays.toString(args));
}
logger.log(Logger.Level.INFO, () -> "Msg: " + Arrays.toString(args)); // 日志级别不匹配则不执行 Lambda,特别是复杂日志内容。
JDK/JRE
JRE 自定义镜像:
cd "C:\Program Files\Java\jdk-24\bin"
./jlink --help 只含 java.base 则生成后约 30-47MiB。
[可选] 查看依赖的modules: jdeps HelloWorld.class
不含应用: ./jlink --output $HOME/Desktop/my-jre/ --add-modules java.base
./jlink --output $HOME/Desktop/my-jre/ --add-modules java.base,jdk.localedata --include-locales en,zh-Hans-CN --strip-debug --compress=zip-9
说明 - 不指定 --module-path 则默认取 JDK 目录 jmods/;可指定 jar(不指定模块名则取文件名) 和通过 jar 或 *.class 生成的 jmod 文件。
其中 --strip-debug --compress=zip-9 参数均为可选,但建议加上,压缩似乎只针对 my-jre/lib/modules 文件。
包含应用: ./jlink --output $HOME/Desktop/my-jre/ --module-path ../jmods/:path/to/your-modular.jar:your-modular.jmod --add-modules java.base
说明 - 运行命令 java --module your.modular/HelloWorld.class
其他参数:如果想重用参数,可通过配置文件指定 ./jlink @my.conf
--add-modules java.base,javafx.base,javafx.graphics,javafx.controls --launcher start=java.base/java
指定本地化语言(默认en_US) ./jlink --add-modules java.base,jdk.localedata --include-locales en,ja,zh-Hans-CN
清理输出目录: rm -r $HOME/Desktop/my-jre/
[可选] 创建jmod文件: jmod create --class-path mods/com.example.mymodule --output mymods/com.example.mymodule.jmod --module-version 1.0.0 --main-class com.example.mymodule.MyClass --resource-path resources
其他
OSHI 库获取"设备 ID"(重装会变) - RegistryUtil.getStringValue(WinReg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\SQMClient", "MachineID");
主板 SMBIOS(DMTF格式/硬件固化): new SystemInfo().getHardware().getComputerSystem().getHardwareUUID();