Java 编程技术综合性知识库 - File、IO、反射、序列化、安全、单例、JSON、FFmpeg/JMF、WASM(WebAssembly)
综述
Java API 在线文档
主要
sun.misc.Unsafe 于 JDK 23 删除,作用已被 VarHandle API 和 外部函数和内存 API 取代。 SerializedLambda 用于 Lambda 序列化,可以提取 User::getName 方法引用的 Name 字样。 record入参过多优化: public record Extra (string Address,string Email,int Age); public record Person(string FirstName, string LastName, Extra e); 死记: Math.toIntExact(123L) 不会像 Long.valueOf(123L).intValue() 返回溢出值,若超出范围则直接抛异常。
常用库
解析字符串中的URL - https://github.com/URL-Detector/URL-Detector Markdown to HTML - implementation("org.commonmark:commonmark:0.24.0") 音视频处理库 FFmpeg JavaCV - https://github.com/bytedeco 注意 - 指针转实际字符串别误用toString():avformat.av_disposition_to_string(avformat.AV_DISPOSITION_DEFAULT).getString() 用法: 对象 - AVFrame即单帧;BytePointer即数据(Bytes)指针。 读摄像头 - new OpenCVFrameGrabber(CAMERA_INDEX); 显示视频 - CanvasFrame 读 - new FFmpegFrameGrabber(filepath); // frame.image;声音属性.samples 写 - new FFmpegFrameRecorder(file); r.record(frame);//声音recordSamples 滤镜 - var f = new FFmpegFrameFilter(filterString,768,320); f.push(frame); frame=f.pullImage(); r.record(frame); 该库默认未启用srt字幕滤镜--enable-libass,执行subtitles=filename=x.srt报No such filter: 'subtitles'
单例模式
单例模式: public class Singleton { // 懒加载+线程安全 private static class Holder { // 静态内部类成员首次调用时,才会被JVM装载 public static Singleton instance = new Singleton(); } public static Singleton instance() { return Holder.instance; } private Singleton() { //super(p); // extends Father } // [可选] 仅能通过instance()获取实例 private static String p; // super(p); public static Singleton instance(String s) { p = s; // 多次调用仍会使用首次传参 return Holder.instance; } public static void main(String[] args) { System.out.println(Singleton.instance()); } }
反射
反射: JDK9起setAccessible(true)会触发非法访问WARNING,可通过--illegal-access=permit和--add-opens module.name/pkg.name=ALL-UNNAMED 解决,最终过渡至 --illegal-access=deny。 临时生成并反射使用的可通过Lookup::defineHiddenClass来定义。 获取修饰符 - Modifier.isPublic(m.getModifiers()) MethodHandle: 若报非法访问且无解可降级至传统的Method 公共成员 - MethodHandles.lookup(); MethodHandles.lookup().findStatic(X.class,"m", MethodType.methodType(String.class)); //参1为返回类型,参2起为入参类型 私有成员访问使用:module-info.java内需要opens pkg.name MethodHandles.privateLookupIn(方法所在类, MethodHandles.lookup()); 注意 - “方法所在类”指该方法必须真实存在所在类中,继承的方法不算。 实例方法 - lookup.findVirtual(...); 静态方法 - lookup.findStatic(...); 实例若已绑定mh.bindTo(obj); 则mh.invoke(...)首个参数不用填写该实例,等同静态方法的填参; 切记入参必须强转真实类型mh.invoke((int)byteValue)。 示例:MethodHandles.lookup().findVirtual(ar.getClass(), "x", MethodType.methodType(void.class)).invoke(ar); // 数组类型用byte[].class 返回值+入参:MethodType.methodType(void.class,String.class); 泛型T用Object.class代替。 变量示例:l.findVarHandle(Entity.class, "f", String.class).set(obj,"value"); VarHandle比反射方式少了设置可访问性步骤 field.setAccessible(true); 方法访问性设置: Method m = cf.getDeclaredMethod("method", String.class, int.class); m.setAccessible(true); var cfMethod = MethodHandles.lookup().unreflect(m); Android缺失的方法处理: var v = Class.forName("java.lang.Runtime$Version"); var rv = MethodHandles.lookup().findStatic(Runtime.class, "version", MethodType.methodType(v)); System.out.println(rv.invoke().toString()); 反射static final field: applicationDefaultJvmArgs = ['--add-opens','java.base/java.lang.reflect=ALL-UNNAMED']
File I/O
InputStream、FilterInputStream类没有实现reset方法,故无法实现流的复用,可通过BufferedInputStream和ByteArrayInputStream来实现复用。 FileInputStream直接读取;BufferedInputStream是分批读取,通常套在FileInputStream外改善性能;ByteArrayInputStream则是全部读取。
音视频
综合: 比特率=采样率 * 位 * 通道数 RTP强制支持PCMU(μ-law)音频格式 而非PCMA(a-law)。 FMJ规整URL斜杠:new MediaLocator(URLUtils.createUrlStr(new File("samplemedia/gulp2.wav"))) 音频接口:Windows保底为MME (Microsoft Multimedia Extensions),追求质量上DirectSound或独占模式的WASAPI (Windows Audio Session API)。 libjitsi: implementation("org.jitsi:libjitsi:1.1-34-gb93ce2ee") // 解压出 win32-x86-64 目录;已包含了fmj库 //implementation("org.jitsi:jitsi-lgpl-dependencies:1.2-23-g7b49874:win32-x86-64") tasks.named("run") { jvmArgs = listOf("-Djava.library.path=D:\\temp\\libjitsi-1.2-23-g7b49874\\win32-x86-64\\") args = listOf("--local-port-base=11111","--remote-host=localhost","--remote-port-base=22222") //args = listOf("--local-port-base=22222","--remote-host=localhost","--remote-port-base=11111") //args +=['--local-port-base=11111', '--remote-host=localhost', '--remote-port-base=22222'] // 接收入参;2分钟自动退出 //args +=['--local-port-base=22222', '--remote-host=localhost', '--remote-port-base=11111'] // 发送入参;1分钟自动退出 } 说明: 注意 - LibJitsi.stop() 一旦被调用,LibJitsi.getMediaService() 就会卡住,故只在程序退出时调一次。 PortAudioStream用于音频输入、PortAudioRenderer用于音频输出? 示例 - https://github.com/jitsi/libjitsi/blob/master/src/main/java/org/jitsi/examples/AVTransmit2.java 通用API: LibJitsi.start(); LibJitsi.getConfigurationService().setProperty(DISABLE_VIDEO_SUPPORT_PNAME, true); //LibJitsi.getConfigurationService().setProperty("net.java.sip.communicator.impl.neomedia.echocancel.filterLengthInMillis", 10000); MediaService ms = LibJitsi.getMediaService(); // NeomediaServiceUtils.getMediaServiceImpl(); var cdi = ((MediaDeviceImpl) ms.getDefaultDevice(MediaType.AUDIO, MediaUseCase.CALL)).getCaptureDeviceInfo(); if (cdi != null) { // 因getDefaultDevice匹配不到设备会返回一个非null实例,故只能用getCaptureDeviceInfo()来判null。 var stream = ms.createMediaStream((MediaDevice) cdi); System.out.println("stream - " + stream); } MediaServiceImpl msi = (MediaServiceImpl)ms; if (msi != null) { var dc = msi.getDeviceConfiguration(); if (dc != null) { var r = dc.getEchoCancelFilterLengthInMillis(); System.out.println("r - " + r); } } WASAPISystem感知不到小部分USB声卡“ReSpeaker 4 Mic Array (UAC1.0)”,获取到的MediaDevice是个“inactive”实例,可改用能感知USB声卡的PortAudioSystem: //默认 AudioSystem 实现在 DeviceSystem.java 中 OSUtils.IS_WINDOWS ? ".WASAPISystem" : null 和 OSUtils.IS_ANDROID ? null : ".PortAudioSystem"。 LibJitsi.start(); // 初始化、启动库;并暂时禁用依赖FFmpeg的视频支持。 LibJitsi.getConfigurationService().setProperty(DISABLE_VIDEO_SUPPORT_PNAME, true); LibJitsi.getMediaService(); // 调用后getAudioSystem才不返回null。 var as = AudioSystem.getAudioSystem(LOCATOR_PROTOCOL_PORTAUDIO); // 换成除 IS_ANDROID 操作系统外均支持的 PortAudioSystem。 var devices = as.getDevices(AudioSystem.DataFlow.CAPTURE); System.out.println(devices); if (!devices.isEmpty()) { var cdi = (CaptureDevice) Manager.createDataSource(devices.getFirst().getLocator()).getCaptureDeviceInfo(); if (cdi != null) { var stream = ms.createMediaStream(new MediaDeviceImpl(cdi, MediaType.AUDIO)); System.out.println("stream - " + stream); } } JMF/FMJ: 说明 - JMF已被JDK删除,FMJ官方成员也说停止维护和不推荐使用,故可换用 Jitsi libjitsi 库。 Bug: Manager.createDataSink(ds, m).open() 中 rtpManager.initialize(new SessionAddress()) 入参未判空被调导致NPE: 故应改写 DataSink 实现类 net.sf.fmj.media.datasink.rtp.Handler.open() 源码,传入 new SessionAddress(InetAddress.getLocalHost(), parsedRTPUrl.elements[0].port); 完整用法 - var ds = new Handler(); ds.setSource(processor.getDataOutput()); ds.setOutputLocator(m); ds.open(); ds.start(); rtp最简实例: 仅需 implementation("org.jitsi:fmj:1.0.2-jitsi") VLC media player 媒体 -> 打开网络串流 填入组播(全子网)网址 rtp://224.0.0.1:22222/audio/16 RTP发送源码 https://github.com/jitsi/fmj/blob/master/src.examples.rtp/SimpleVoiceTransmiter.java https://github.com/jitsi/fmj/blob/master/src.test/net/sf/fmj/rtp/rtpaudio.java String url = "rtp://224.0.0.1:22222/audio/16"; // 224.0.0.1 为组播IP,多个流则通过RTP头的SSRC或CNAME来区分;audio为媒体类型,16为可选的TTL值;无TTL写法 rtp://224.0.0.1:22222/audio。 解决 Unable to transcode format - Audacity -> 打开文件(示例音频) -> 导出音频 -> 导出到计算机 -> 单声道、采样率 8000 Hz、编码 U-Law -> 导出。 解决 com.sun.media.processor.unknown.Handler ConcurrentModificationException - 将 if blockingRealize() 块儿换为 processor.realize();,取消注释下方的 while (processor.getState() != Processor.Realized) 块儿。
综合
HTML转义 - guava 库com.google.common.html.HtmlEscapers.htmlEscaper().escape(s)
EQ 就是 EQUAL等于 NE 就是 NOT EQUAL不等于 GT 就是 GREATER THAN大于 LT 就是 LESS THAN小于 GE 就是 GREATER THAN OR EQUAL 大于等于 LE 就是 LESS THAN OR EQUAL 小于等于
其他
WASM(WebAssembly): 语法: 每个元素均用括号区隔,树根元素为module。 type或import、export元素可单列也可内联: (func $f1 (import "mod" "f1") (type $ft1)) $标识编译为wasm后会变成索引值,用来重复引用其他元素。 Java调WASM实例(纯JVM即可) - https://github.com/graalvm/graal-languages-demos/tree/main/graalwasm/graalwasm-starter 纯JVM无法编译为Native程序 - Unknown name in option specification: macro:truffle-svm 或 [engine] WARNING: The polyglot engine uses a fallback runtime that does not support runtime compilation to native code. implementation("org.graalvm.polyglot:polyglot:24.1.1") implementation("org.graalvm.polyglot:wasm:24.1.1") implementation("org.graalvm.polyglot:js:24.1.1") 示例代码: try (var context = Context.newBuilder()//.allowAllAccess(true) // 解决JS调宿主Java报错:Java is not defined .allowHostAccess(HostAccess.ALL).allowHostClassLookup(_ -> true) //.option("wasm.Builtins", "wasi_snapshot_preview1") // 非实验选项 //.allowExperimentalOptions(true).option("wasm.Threads", "true") .build()) { // or org.graalvm.polyglot.Context.create() // note - wasm only supports binary based sources context.eval("js", "console.log('js log')"); var jsCallJava = "var JavaString = Java.type('java.lang.String');" + "JavaString.valueOf(Math.PI); // new JavaString('s');"; // Java的String与JS中String重名,故new的类名应加个前后缀。 var ce = context.eval(Source.newBuilder("js", jsCallJava, null).build()); // 非兼容类则用.isHostObject()和.asHostObject(); System.out.println(ce.isString() ? ce.asString() : null); context.getBindings("js").getMemberKeys().forEach(System.out::println); // wasm用name进行getMember(name)寻址,不设则取文件名,若是ByteSequence则用“js:module-字节哈希”。 var r = context.eval(Source.newBuilder("wasm", wasmFile).name("topMod").build()); System.out.println(r.hashCode()); // 等同下行代码返回值: var topMod = context.getBindings("wasm").getMember("topMod"); // 底层实现类则可不设topMod名 - WebAssembly.instanceExport(wasmInstance, "functionName"); var fr = topMod.getMember("stringFunctionExample").execute("s"); System.out.println(fr.as(String.class)); context.getBindings("wasm").getMemberKeys().forEach(System.out::println); // 取定级模块名。 } WASM命令工具 - https://github.com/oracle/graal/blob/master/wasm/README.md/#graalwasm-standalone-distribution Java其他WebAssembly库 - https://teavm.org/ 、 https://github.com/i-net-software/JWebAssembly 序列化: // Jackson使用OffsetDateTime必须引入jackson-datatype-jsr310库,不setDateFormat则为纯数字。 var om = new ObjectMapper().registerModules(new JavaTimeModule()) .setDateFormat(new StdDateFormat().withColonInTimeZone(true)); var jsonString = om.writeValueAsString(obj); 序列化防护: try (var ois = new ObjectInputStream(is)) { ois.setObjectInputFilter(FilterClass::dateTimeFilter); return (LocalDateTime) ois.readObject(); // 类名筛选白名单或黑名单? } catch (ClassNotFoundException ex) { } 用之前应对 jakarta.json.JsonObject 判null: if (!jsonObject.isNull("q")) { var q = jsonObject.getString("q"); } UTF-16LE乱码转UTF-8中文: 说明 - Windows字符用UTF-16小尾序表示;Unicode定义字符位置,UTF(Unicode Transformation Formats)定义字节表达。 字节byte由八个位(bit/二进制0或1)组成,缩写为16进制符(可读性缘故缩至1-2位/范围是a-f加0-9)。 UTF-16LE即至少2个或偶数字节表达1个字符,可表达英文、中文等全球文字;Unicode码已超出单个UTF-16的表达范围了,但都是些非经常使用的新增符号; 纯英文字符可用无字节序的1个字节表达,而中文则适合用可变长(variable-length)的UTF-8或UTF-16LE; 若不考虑传输长度,可采用固定字节数目UTF-32。 var buffer = ByteBuffer.allocate(4); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.putChar('中'); buffer.putChar('文'); System.out.println(Arrays.toString(buffer.array())); var bytes = new String(buffer.array(), 0, buffer.array().length, "UTF-16LE").getBytes(Charset.forName("UTF-8")); System.out.println(Arrays.toString(bytes)); Files.write(Path.of("d:/a.txt"), bytes); JavaScript对应函数 - new TextDecoder('utf-16le').decode(uint16Array);