从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

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);