<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Java &#8211; 张三太爷</title>
	<atom:link href="https://www.somedoc.net/category/java/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.somedoc.net</link>
	<description>看前面，黑洞洞</description>
	<lastBuildDate>Mon, 21 Apr 2025 06:11:09 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8</generator>

<image>
	<url>https://www.somedoc.net/wp-content/uploads/2016/12/cropped-dandycheung-1-32x32.jpg</url>
	<title>Java &#8211; 张三太爷</title>
	<link>https://www.somedoc.net</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>通过 JNA 技术实现 COM 接口时虚函数表指针的构建</title>
		<link>https://www.somedoc.net/2025/04/21/%e9%80%9a%e8%bf%87-jna-%e6%8a%80%e6%9c%af%e5%ae%9e%e7%8e%b0-com-%e6%8e%a5%e5%8f%a3%e6%97%b6%e8%99%9a%e5%87%bd%e6%95%b0%e8%a1%a8%e6%8c%87%e9%92%88%e7%9a%84%e6%9e%84%e5%bb%ba/</link>
					<comments>https://www.somedoc.net/2025/04/21/%e9%80%9a%e8%bf%87-jna-%e6%8a%80%e6%9c%af%e5%ae%9e%e7%8e%b0-com-%e6%8e%a5%e5%8f%a3%e6%97%b6%e8%99%9a%e5%87%bd%e6%95%b0%e8%a1%a8%e6%8c%87%e9%92%88%e7%9a%84%e6%9e%84%e5%bb%ba/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Mon, 21 Apr 2025 06:11:09 +0000</pubDate>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=6369</guid>

					<description><![CDATA[这是 DeepSeek 回答的，没测试，但是看上去可行。 Q <a href="https://www.somedoc.net/2025/04/21/%e9%80%9a%e8%bf%87-jna-%e6%8a%80%e6%9c%af%e5%ae%9e%e7%8e%b0-com-%e6%8e%a5%e5%8f%a3%e6%97%b6%e8%99%9a%e5%87%bd%e6%95%b0%e8%a1%a8%e6%8c%87%e9%92%88%e7%9a%84%e6%9e%84%e5%bb%ba/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>这是 DeepSeek 回答的，没测试，但是看上去可行。</p>
<p>Q：以实现 <code>IUnknown</code> 接口为例。目前的做法是，定义一个 Groovy 版本的 <code>IUnknown</code>，如，</p><pre class="crayon-plain-tag">interface IUnknown {
    int QueryInterface(Pointer riid, PointerByReference ppvObject)
    int AddRef()
    int Release()
}</pre><p>然后，</p><pre class="crayon-plain-tag">class UnknownImpl extends Structure implements IUnknown {
    public Pointer vtable
    private int refCount = 1
    UnknownImpl() {
        vtable = createVTable()
        write()
    }

    Pointer createVTable() {
        def callbacks = new Callback[] {
            new Callback() { int callback(Pointer thisPtr, Pointer riid, PointerByReference ppvObject) { QueryInterface(riid, ppvObject) } },
            new Callback() { int callback(Pointer thisPtr) { AddRef() } },
            new Callback() { int callback(Pointer thisPtr) { Release() } },
        }
        Pointer vtable = new Memory(Native.POINTER_SIZE * callbacks.length)
        for (int i = 0; i &lt; callbacks.length; i++) {
            def callbackPtr = CallbackReference.getFunctionPointer(callbacks[i])
            if (callbackPtr == null) {
                throw new RuntimeException("Callback $i 初始化失败")
            }
            vtable.setPointer(i * Native.POINTER_SIZE, callbackPtr)
        }
        return vtable
    }

    int QueryInterface(Pointer riid, PointerByReference ppvObject) {
       // ...
    }
    // ...
}</pre><p>可以看到 <code>QueryInterface</code> 的实现，就是在 <code>callback</code> 方法里去掉 <code>thisPtr</code> 然后调用到了 <code>UnknownImpl</code> 的对应同名方法上，如果方法多了的话，以及每个方法可能有很多参数，写起来繁琐。期望引入 CallbackProxy，在 new Callback 的时候写起来轻松些。</p>
<p>A：你的需求非常明确：<strong>希望通过 <code>CallbackProxy</code> 自动剥离 <code>thisPtr</code> 参数，避免为每个 COM 方法手动编写样板代码</strong>。以下是针对 <code>IUnknown</code> 接口的优化实现，利用 <code>CallbackProxy</code> 减少重复代码。</p>
<h3><strong>1. 定义通用的 <code>ComCallbackProxy</code> 类</strong></h3>
<p></p><pre class="crayon-plain-tag">import com.sun.jna.*
import com.sun.jna.ptr.PointerByReference

class ComCallbackProxy implements CallbackProxy {
    private final Method method // 目标方法
    private final Object impl   // 实现类实例（如 UnknownImpl）

    ComCallbackProxy(Object impl, Method method) {
        super(getParameterTypes(method), impl)
        this.impl = impl
        this.method = method
    }

    // 自动跳过第一个参数（thisPtr）
    @Override
    Object callback(Object[] args) {
        Object[] methodArgs = args.length &gt; 1 ? Arrays.copyOfRange(args, 1, args.length) : new Object[0]
        return method.invoke(impl, methodArgs)
    }

    // 获取方法参数类型（排除 thisPtr）
    private static Class&lt;?&gt;[] getParameterTypes(Method m) {
        Class&lt;?&gt;[] original = m.parameterTypes
        return original.length &gt; 0 ? Arrays.copyOfRange(original, 0, original.length) : original
    }
}</pre><p><strong>2. 改造 <code>UnknownImpl</code> 使用代理</strong></p><pre class="crayon-plain-tag">class UnknownImpl extends Structure implements IUnknown {
    public Pointer vtable
    private int refCount = 1

    UnknownImpl() {
        vtable = createVTable()
        write()
    }

    Pointer createVTable() {
        // 1. 获取 IUnknown 接口的所有方法
        Method[] methods = IUnknown.declaredMethods.findAll { !it.synthetic }

        // 2. 为每个方法创建代理 Callback
        Pointer vtable = new Memory(Native.POINTER_SIZE * methods.size())
        methods.eachWithIndex { method, index -&gt;
            def proxy = new ComCallbackProxy(this, method)
            def callbackPtr = CallbackReference.getFunctionPointer(proxy)
            vtable.setPointer(index * Native.POINTER_SIZE, callbackPtr)
        }
        return vtable
    }

    // 实现方法（无需关心 thisPtr）
    int QueryInterface(Pointer riid, PointerByReference ppvObject) {
        println("QueryInterface called with riid: $riid")
        ppvObject.value = this.pointer
        AddRef()
        return 0 // S_OK
    }

    int AddRef() { ++refCount }
    int Release() { --refCount }
}</pre><p></p>
<h3><strong>3. 使用示例</strong></h3>
<p></p><pre class="crayon-plain-tag">// 定义 COM 接口
interface IUnknown {
    int QueryInterface(Pointer riid, PointerByReference ppvObject)
    int AddRef()
    int Release()
}

// 创建实例
def unknown = new UnknownImpl()

// 模拟 COM 调用（实际由本地代码触发）
def ppv = new PointerByReference()
unknown.vtable.getPointer(0) // 获取 QueryInterface 函数指针
// 本地代码会调用：QueryInterface(thisPtr, riid, ppv)</pre><p></p>
<h3><strong>关键改进点</strong></h3>
<table>
<thead>
<tr>
<th>传统方式问题</th>
<th>使用 <code>ComCallbackProxy</code> 后的优化</th>
</tr>
</thead>
<tbody>
<tr>
<td>每个方法需手动写 <code>Callback</code></td>
<td>自动生成，无需重复代码</td>
</tr>
<tr>
<td>需显式忽略 <code>thisPtr</code></td>
<td>代理自动跳过第一个参数</td>
</tr>
<tr>
<td>参数多时易出错</td>
<td>通过反射自动匹配参数</td>
</tr>
</tbody>
</table>
<hr />
<h3><strong>工作原理</strong></h3>
<ol start="1">
<li><strong>反射获取方法</strong><br />
通过 <code>IUnknown.declaredMethods</code> 获取所有接口方法，动态生成代理。</li>
<li><strong>参数自动裁剪</strong><br />
<code>ComCallbackProxy.callback()</code> 自动跳过 <code>thisPtr</code>，将剩余参数传递给实现类。</li>
<li><strong>统一函数指针生成</strong><br />
所有方法通过相同的代理机制转换为函数指针，保证 vtable 布局正确。</li>
</ol>
<hr />
<h3><strong>扩展性</strong></h3>
<ul>
<li><strong>支持复杂参数</strong><br />
若方法包含结构体参数，只需在接口中正确定义类型（如 <code>MyStruct.ByReference</code>）。</li>
<li><strong>混合实现</strong><br />
对特殊方法（如需要手动处理 <code>thisPtr</code>），仍可单独实现：<br />
<pre class="crayon-plain-tag">Pointer createVTable() {
    def methods = IUnknown.declaredMethods.findAll { !it.synthetic }
    Pointer vtable = new Memory(Native.POINTER_SIZE * (methods.size() + 1))

    // 默认用代理
    methods.eachWithIndex { method, index -&gt;
        def proxy = new ComCallbackProxy(this, method)
        vtable.setPointer(index * Native.POINTER_SIZE, CallbackReference.getFunctionPointer(proxy))
    }

    // 手动处理特殊方法
    vtable.setPointer(methods.size() * Native.POINTER_SIZE, manualMethodPointer)
    return vtable
}</pre>
</li>
</ul>
<h3><strong>注意事项</strong></h3>
<ol start="1">
<li><strong>性能开销</strong><br />
反射调用比直接调用略慢，但对大多数 COM 场景影响极小。</li>
<li><strong>线程安全</strong><br />
若 COM 回调可能跨线程，需在实现类中添加同步逻辑。</li>
<li><strong>JNA 版本兼容性</strong><br />
确保使用最新版 JNA（如 5.x+），其对 <code>CallbackProxy</code> 的支持更完善。</li>
</ol>
<p>这种方式将 vtable 构造的样板代码减少到最低，同时保持类型安全和可维护性。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2025/04/21/%e9%80%9a%e8%bf%87-jna-%e6%8a%80%e6%9c%af%e5%ae%9e%e7%8e%b0-com-%e6%8e%a5%e5%8f%a3%e6%97%b6%e8%99%9a%e5%87%bd%e6%95%b0%e8%a1%a8%e6%8c%87%e9%92%88%e7%9a%84%e6%9e%84%e5%bb%ba/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>最小限度移植 Config 库到安卓平台的思路</title>
		<link>https://www.somedoc.net/2023/12/02/%e6%9c%80%e5%b0%8f%e9%99%90%e5%ba%a6%e7%a7%bb%e6%a4%8d-config-%e5%ba%93%e5%88%b0%e5%ae%89%e5%8d%93%e5%b9%b3%e5%8f%b0%e7%9a%84%e6%80%9d%e8%b7%af/</link>
					<comments>https://www.somedoc.net/2023/12/02/%e6%9c%80%e5%b0%8f%e9%99%90%e5%ba%a6%e7%a7%bb%e6%a4%8d-config-%e5%ba%93%e5%88%b0%e5%ae%89%e5%8d%93%e5%b9%b3%e5%8f%b0%e7%9a%84%e6%80%9d%e8%b7%af/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Fri, 01 Dec 2023 16:42:58 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[技术]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=5797</guid>

					<description><![CDATA[尚未着手，仅作备忘。 该库使用标准 Java 写就，在 An <a href="https://www.somedoc.net/2023/12/02/%e6%9c%80%e5%b0%8f%e9%99%90%e5%ba%a6%e7%a7%bb%e6%a4%8d-config-%e5%ba%93%e5%88%b0%e5%ae%89%e5%8d%93%e5%b9%b3%e5%8f%b0%e7%9a%84%e6%80%9d%e8%b7%af/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>尚未着手，仅作备忘。</p>
<p>该库使用标准 Java 写就，在 Android 上编译通过的修改较小，之前已经介绍了。但是它的一个主要方法——对配置文件的加载，由于用到了的 ClassLoader 的 getResources 方法，在 Android 上略有不适。经过初步考察，目前将着眼点放在 Parseable 的内嵌类 ParseableResources 的 rawParseValue 方法上。在 Android 平台下为该方法单独实现一个版本，将对 ClassLoader.getResources 的使用迁移到 AssetManager 上来似乎是个不错的选择，这样的话，reader 仍然可以保持使用现在的流访问。</p>
<p>抽时间试试。</p>
<p>&nbsp;</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2023/12/02/%e6%9c%80%e5%b0%8f%e9%99%90%e5%ba%a6%e7%a7%bb%e6%a4%8d-config-%e5%ba%93%e5%88%b0%e5%ae%89%e5%8d%93%e5%b9%b3%e5%8f%b0%e7%9a%84%e6%80%9d%e8%b7%af/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>在 Android 上使用 java.beans.*</title>
		<link>https://www.somedoc.net/2023/11/27/%e5%9c%a8-android-%e4%b8%8a%e4%bd%bf%e7%94%a8-java-beans/</link>
					<comments>https://www.somedoc.net/2023/11/27/%e5%9c%a8-android-%e4%b8%8a%e4%bd%bf%e7%94%a8-java-beans/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Mon, 27 Nov 2023 15:29:26 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=5786</guid>

					<description><![CDATA[在 Github 里看到一个 Java 写就的配置文件访问库 <a href="https://www.somedoc.net/2023/11/27/%e5%9c%a8-android-%e4%b8%8a%e4%bd%bf%e7%94%a8-java-beans/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>在 Github 里看到一个 Java 写就的配置文件访问库，直觉之下比较符合老夫的审美，决定下载下来试用一下。原作者没有考虑 Android 平台，老夫自己动手试用，相当于自主退毛。</p>
<p>把源代码复制到 Android 项目中，编译，报 java.beans 包下的若干类无法导入。在 CSDN 上看到一篇<a href="https://blog.csdn.net/qq_32682301/article/details/100153192">不太久远的文章</a>，里面提到三种方法（核心都是用 openbeans 来替代），俺选择了第三种。</p>
<p>第一步是下载。就是要下载到 openbeans 的 jar 包。该文中未给出链接，此处列出<a href="https://mvnrepository.com/artifact/com.googlecode/openbeans/1.0">访问页面</a>以及<a href="https://maven.repository.redhat.com/ga/com/googlecode/openbeans/1.0/openbeans-1.0.jar">直接下载链接</a>。</p>
<p>第二步是安置。下载后放置到 Android 项目中的 libs 目录下，而且，尽管大多数的构建脚本中都存在，但确实有时候会缺的一行配置 <code>implementation fileTree(dir: 'libs', includes: ['*.jar'])</code> 要自己确认它确实存在。</p>
<p>第三步是修订。要将之前源码里导入语句中的 <code>java.beans</code> 包名替换为 <code>com.googlecode.openbeans</code>。现在就可以尝试编译了，反正俺是一遍就通过了。当然库的功能还要测试，以及修正，毕竟像默认配置的存放路径之类的，就必须要考虑 Android 平台的个性了。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2023/11/27/%e5%9c%a8-android-%e4%b8%8a%e4%bd%bf%e7%94%a8-java-beans/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>打造开发 Android 2.2 应用的便携环境（一）</title>
		<link>https://www.somedoc.net/2023/09/03/%e6%89%93%e9%80%a0%e5%bc%80%e5%8f%91-android-2-2-%e5%ba%94%e7%94%a8%e7%9a%84%e4%be%bf%e6%90%ba%e7%8e%af%e5%a2%83%ef%bc%88%e4%b8%80%ef%bc%89/</link>
					<comments>https://www.somedoc.net/2023/09/03/%e6%89%93%e9%80%a0%e5%bc%80%e5%8f%91-android-2-2-%e5%ba%94%e7%94%a8%e7%9a%84%e4%be%bf%e6%90%ba%e7%8e%af%e5%a2%83%ef%bc%88%e4%b8%80%ef%bc%89/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Sun, 03 Sep 2023 13:34:13 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[软件]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=5643</guid>

					<description><![CDATA[本文有一些背景前文可以参考：《锦书 SD968 上来了个年轻 <a href="https://www.somedoc.net/2023/09/03/%e6%89%93%e9%80%a0%e5%bc%80%e5%8f%91-android-2-2-%e5%ba%94%e7%94%a8%e7%9a%84%e4%be%bf%e6%90%ba%e7%8e%af%e5%a2%83%ef%bc%88%e4%b8%80%ef%bc%89/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>本文有一些背景前文可以参考：《<a href="https://www.somedoc.net/?p=5270">锦书 SD968 上来了个年轻人</a>》、《<a href="https://www.somedoc.net/?p=5190">2022 年了，我决定搭一个 Android 2.2 的开发环境</a>》。</p>
<p>基于以上，目标显然就直接锁定在 Android Studio 1.0 上，而非复古至 Eclipse 等地步。</p>
<p>1. 将安装包 <code>android-studio-bundle-135.1629389.exe</code> 中的子目录 <code>$_27_</code> 先单独释出，命名为 <code>AS1.0</code>。此时直接运行 <code>bin</code> 下的 <code>studio64.exe</code> 会提示：</p>
<blockquote><p>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;<br />
Error launching Android Studio<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;<br />
No JVM installation found. Please install a 64-bit JDK.<br />
If you already have a JDK installed, define a JAVA_HOME variable in Computer &gt; System Properties &gt; System Settings &gt; Environment Variables.<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;<br />
确定<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;</p></blockquote>
<p>2. 将安装包 <code>jdk-7u80-windows-x64.exe</code> 中的所有内容释出至 <code>AS1.0</code> 下的 <code>jdk1.7.0_80</code> 目录下，并到系统的环境变量中将 <code>JAVA_HOME</code> 指向该目录。运行 <code>studio64.exe</code> 会闪退（无任何信息输出）。</p>
<p>第一步，参考可以正常运行的设备，把 <code>HKLM\Software\Android Studio</code> 下的所有注册表键值补齐，无效。</p>
<p>在命令行下运行 <code>java.exe</code>，发现不识别，感觉只是指定解压缩后的 JDK 内容给 AS 恐怕不够，毕竟还没在系统范围内宣告 java 的存在。于是，把 JDK 的 <code>bin</code> 目录添加到了系统的 Path 环境变量里，再一运行，输出：</p>
<blockquote><p>Error occurred during initialization of VM<br />
java/lang/NoClassDefFoundError: java/lang/Object</p></blockquote>
<p>显然，Java 的运行时信息不够完整。</p>
<p>第二步，参考可以正常运行的设备，把 <code>HKLM\Software\JavaSoft\Java Development Kit</code> 下的所有注册表键值补齐，无效。</p>
<p>第三步，参考可以正常运行的设备，把 <code>HKLM\Software\JavaSoft\Java Runtime Environment</code> 下的所有注册表键值补齐，无效。</p>
<p>使用以上错误信息在网络上搜索，才了解到 JDK 安装包内某些文件是需要二次解压的。</p>
<p>第四步，找到所有的 <code>.pack</code> 文件，使用 <code>bin</code> 下的 <code>unpack200.exe</code> 将之一一就地解压为同名的 <code>.jar</code> 文件后，<code>java.exe</code> 工作即告正常。</p>
<p>至此，<code>studio64.exe</code> 可以正常启动运行。</p>
<p>作为一个轻度洁癖，倒回去检查了上述前三步的注册表键值是否有用；事实证明，它们均非必需（删除即可）。</p>
<p>接下去，要解决与 SDK 以及 Gradle 配合协作的部分了。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2023/09/03/%e6%89%93%e9%80%a0%e5%bc%80%e5%8f%91-android-2-2-%e5%ba%94%e7%94%a8%e7%9a%84%e4%be%bf%e6%90%ba%e7%8e%af%e5%a2%83%ef%bc%88%e4%b8%80%ef%bc%89/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>近期成就</title>
		<link>https://www.somedoc.net/2023/08/10/%e8%bf%91%e6%9c%9f%e6%88%90%e5%b0%b1/</link>
					<comments>https://www.somedoc.net/2023/08/10/%e8%bf%91%e6%9c%9f%e6%88%90%e5%b0%b1/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Thu, 10 Aug 2023 05:15:47 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[生活]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=5579</guid>

					<description><![CDATA[一晃眼竟然快有一个月没有写了，有点怠惰了。其中有点小小的外部 <a href="https://www.somedoc.net/2023/08/10/%e8%bf%91%e6%9c%9f%e6%88%90%e5%b0%b1/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>一晃眼竟然快有一个月没有写了，有点怠惰了。其中有点小小的外部因素，因为兜哥今年是一个坎，需要盯着他看能不能过去，目前看来还不错，比预料的稍微好那么一点。</p>
<p>主力工作机 Dell Latitude 5401 的输入问题在经历各种折磨后终于解决。排查到最后，问题的焦点集中到了触控板上，而触控板和整个 C 面是一体化的（工艺其实是用胶粘合的，但是剥离以及更换后的复原操作难度应该有点大），所以在和 5400 互换确定问题后突然意识到，5400 是在保的。于是让官方售后来解决了这一棘手难题。</p>
<p>一个好久没有更新的产品 XPC，由于其中集成的一个 SDK 版本过时，应用商店要求必须更新，因此顺便把其它的所有依赖也更新到了最新版，连编译工具链也升了上来，中间解决了一堆的问题，目前还有一个由于引入 Kotlin 的某些组件而导致使用了不同版本的 JDK 的库产生了冲突（Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules jetified-kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10) and jetified-kotlin-stdlib-jdk8-1.7.10 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10)），令我哭笑不得的是，Kotlin 也是由第三方组件间接引入的……</p>
<p>兜哥有个发小在天津，好几年没见了，本来计划过几天再去天津一块儿见面玩，顺便把另一个兄弟在津门购置的新房暖一下的，结果两位小朋友都面临马上就要补课的大事，所以只好提前杀过去让两小叙旧。天津这些年的发展似乎不是很好，走在好多地方感觉都象是二十年前的城镇，烟火气倒是挺足，可惜脏乱差的面貌给人的感觉还是不能算好的。</p>
<p>编译冲突的那个问题，赴津前查到一篇文档，https://www.concretepage.com/questions/746，看着不太靠谱，而且似乎是内容农场划拉回来的。回京后，上午再次搜索，则在 Stackoverflow 上找到了这一篇 <a href="https://stackoverflow.com/questions/75263047/duplicate-class-in-kotlin-android">https://stackoverflow.com/questions/75263047/duplicate-class-in-kotlin-android</a>，通读下来，其中排名第三的 <a href="https://stackoverflow.com/a/75719642/8127335">Eric Aya 的回答</a>一看就对眼，直觉就是解决问题的法门。照方抓药，药到病除，喜不自禁。</p>
<p>就在心下以为万事俱备的当口，事情立刻就又发生了变化。确实，debug 编译是没有问题，但是一切换到 release 下就会发生若干问题，一部分是警告，说有些类的伴生对象字节码找不到，另一部分是报告某种数组越界的错误，但并不会有任何更清晰的信息（即使在命令行中使用 <code>--warning-mode all -Pandroid.debug.obsoleteApi=true</code> 这样的选项开关也无济于事）。但种种迹象表明，跟 AppLovin 的构建插件有关。IDE 一般会有依赖的组件版本过期的视觉提示，但是 <code>com.applovin.quality:AppLovinQualityServiceGradlePlugin:3.9.11</code> 这一行并无异常。手动到网上搜索到一个比较大的版本号，是 <code>4.3.7</code>，改动之后问题没有好转。急切间还发现似乎 AppLovin 的官网是在铁幕的另一边，联想到检索到 <code>4.3.7</code> 这个版本的信息时，隐约看到时间据现在并不久远，开启尝试大法，用了个 <code>4.10</code>，报告找不到，灵机一动改成 <code>4.10.0</code>，竟然成功。是不是其实还存在更新的最小版本号变化了的版本，那就不得而知了。断然放弃自己给自己找罪受的征程，打包交付要紧。</p>
<p>另外，XSC 几天前竟然默默地迎来了第一位内购用户，也是个可喜可贺的事情。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2023/08/10/%e8%bf%91%e6%9c%9f%e6%88%90%e5%b0%b1/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Eclipse 下的 Java 工程迁移至 Maven</title>
		<link>https://www.somedoc.net/2022/06/13/eclipse-%e4%b8%8b%e7%9a%84-java-%e5%b7%a5%e7%a8%8b%e8%bf%81%e7%a7%bb%e8%87%b3-maven/</link>
					<comments>https://www.somedoc.net/2022/06/13/eclipse-%e4%b8%8b%e7%9a%84-java-%e5%b7%a5%e7%a8%8b%e8%bf%81%e7%a7%bb%e8%87%b3-maven/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Mon, 13 Jun 2022 08:54:14 +0000</pubDate>
				<category><![CDATA[Java]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=5035</guid>

					<description><![CDATA[需要一个小小的算法，在 GitHub 上找到一个相关的原型工 <a href="https://www.somedoc.net/2022/06/13/eclipse-%e4%b8%8b%e7%9a%84-java-%e5%b7%a5%e7%a8%8b%e8%bf%81%e7%a7%bb%e8%87%b3-maven/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>需要一个小小的算法，在 GitHub 上找到一个相关的原型工程，拉下来后发现要把它跑起来有点困难。代码距今已经达八年之久，工程文件是 <code>.project</code>，找了一下信息后基本可以确定，如果你有 Eclipse+JDT，应该就是可以直接打开的，然而我没有。我只有一个百纳环境来处理这些非日常工程，那就是 VSCode，可是它里面集成的插件只认 maven 构建所需的 <code>pom.xml</code>。</p>
<p>于是开始寻找如何从 <code>.project</code> 迁移到 <code>pom.xml</code>。找到的方案大多令人啼笑皆非，说 Eclipse 下有个插件可以做到这件事，可问题是我如果有个 Eclipse 那就不需要做这件事了。最后决定查阅这两种工程之间的具体差别，手工完成 <code>pom.xml</code>。</p>
<p>神奇的是，maven 的工程好像也还需要 <code>.project</code> 文件的存在，只是要补充一些内容进去。原始文件内容如下：</p><pre class="crayon-plain-tag">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;projectDescription&gt;
	&lt;name&gt;ContourTracer&lt;/name&gt;
	&lt;comment&gt;&lt;/comment&gt;
	&lt;projects&gt;
	&lt;/projects&gt;
	&lt;buildSpec&gt;
		&lt;buildCommand&gt;
			&lt;name&gt;org.eclipse.jdt.core.javabuilder&lt;/name&gt;
			&lt;arguments&gt;
			&lt;/arguments&gt;
		&lt;/buildCommand&gt;
	&lt;/buildSpec&gt;
	&lt;natures&gt;
		&lt;nature&gt;org.eclipse.jdt.core.javanature&lt;/nature&gt;
	&lt;/natures&gt;
&lt;/projectDescription&gt;</pre><p>添入 maven 所需内容后如下：</p><pre class="crayon-plain-tag">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;projectDescription&gt;
	&lt;name&gt;ContourTracer&lt;/name&gt;
	&lt;comment&gt;&lt;/comment&gt;
	&lt;projects&gt;
	&lt;/projects&gt;
	&lt;buildSpec&gt;
		&lt;buildCommand&gt;
			&lt;name&gt;org.eclipse.jdt.core.javabuilder&lt;/name&gt;
			&lt;arguments&gt;
			&lt;/arguments&gt;
		&lt;/buildCommand&gt;
		&lt;buildCommand&gt;
			&lt;name&gt;org.eclipse.m2e.core.maven2Builder&lt;/name&gt;
			&lt;arguments&gt;
			&lt;/arguments&gt;
		&lt;/buildCommand&gt;
	&lt;/buildSpec&gt;
	&lt;natures&gt;
		&lt;nature&gt;org.eclipse.jdt.core.javanature&lt;/nature&gt;
		&lt;nature&gt;org.eclipse.m2e.core.maven2Nature&lt;/nature&gt;
	&lt;/natures&gt;
&lt;/projectDescription&gt;</pre><p>第一步结束。接下来要手工打造 <code>pom.xml</code> 了，先构造一个框架，如下：</p><pre class="crayon-plain-tag">&lt;project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;

    &lt;groupId&gt;com.helloworld&lt;/groupId&gt;
    &lt;artifactId&gt;contourtracer&lt;/artifactId&gt;
    &lt;version&gt;0.0.1&lt;/version&gt;
    &lt;packaging&gt;jar&lt;/packaging&gt;
    &lt;name&gt;ContourTracer&lt;/name&gt;
    &lt;url&gt;http://www.contourtracer.com&lt;/url&gt;

    &lt;properties&gt;
        &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;/properties&gt;
&lt;/project&gt;</pre><p>其中，从 <code>groupId</code> 到 <code>url</code> 根据工程自身的信息填写就好了。接下来要在 <code>properties</code> 下增加两个节点，<code>dependencies</code> 和 <code>build</code>。在实际的操作中，前者里的明细条目可能是需要把后者基本完善到能够执行构建过程后才能一一补充完整的。最终的 <code>pom.xml</code> 内容如下：</p><pre class="crayon-plain-tag">&lt;project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;

    &lt;groupId&gt;com.helloworld&lt;/groupId&gt;
    &lt;artifactId&gt;contourtracer&lt;/artifactId&gt;
    &lt;version&gt;0.0.1&lt;/version&gt;
    &lt;packaging&gt;jar&lt;/packaging&gt;

    &lt;name&gt;ContourTracer&lt;/name&gt;
    &lt;url&gt;http://www.contourtracer.com&lt;/url&gt;

    &lt;properties&gt;
        &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;/properties&gt;

    &lt;dependencies&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.xmlgraphics&lt;/groupId&gt;
            &lt;artifactId&gt;batik-swing&lt;/artifactId&gt;
            &lt;version&gt;1.14&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.apache.xmlgraphics&lt;/groupId&gt;
            &lt;artifactId&gt;batik-dom&lt;/artifactId&gt;
            &lt;version&gt;1.14&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;

    &lt;build&gt;
        &lt;sourceDirectory&gt;src/&lt;/sourceDirectory&gt;
        &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
                &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
                &lt;version&gt;3.10.1&lt;/version&gt;
                &lt;configuration&gt;
                    &lt;source&gt;1.8&lt;/source&gt;
                    &lt;target&gt;1.8&lt;/target&gt;
                &lt;/configuration&gt;
            &lt;/plugin&gt;
        	&lt;plugin&gt;
                &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
                &lt;artifactId&gt;maven-jar-plugin&lt;/artifactId&gt;
                &lt;version&gt;3.2.2&lt;/version&gt;
                &lt;configuration&gt;
                    &lt;archive&gt;
                        &lt;manifest&gt;
                            &lt;addClasspath&gt;true&lt;/addClasspath&gt;
                            &lt;classpathPrefix&gt;lib/&lt;/classpathPrefix&gt;
                            &lt;mainClass&gt;tracer.main.MainWindow&lt;/mainClass&gt;
                        &lt;/manifest&gt;
                    &lt;/archive&gt;
                &lt;/configuration&gt;
            &lt;/plugin&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
                &lt;artifactId&gt;maven-dependency-plugin&lt;/artifactId&gt;
                &lt;version&gt;3.0.1&lt;/version&gt;
                &lt;executions&gt;
                    &lt;execution&gt;
                        &lt;id&gt;copy-dependencies&lt;/id&gt;
                        &lt;phase&gt;package&lt;/phase&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;copy-dependencies&lt;/goal&gt;
                        &lt;/goals&gt;
                        &lt;configuration&gt;
                            &lt;outputDirectory&gt;${project.build.directory}/lib&lt;/outputDirectory&gt;
                        &lt;/configuration&gt;
                    &lt;/execution&gt;
                &lt;/executions&gt;
		&lt;/plugin&gt;
        &lt;/plugins&gt;
    &lt;/build&gt;
&lt;/project&gt;</pre><p><code>build</code> 节点下的 <code>plugins</code> 下的三个 <code>plugin</code>，一个是编译器，一个是打包器，一个是依赖包处理器。最重要的是，我一开始漏掉了 <code>sourceDirectory</code>，导致打出来的 <code>jar</code> 文件里没有任何代码。你一次次地尝试编译，根据出错信息就可以把 <code>dependencies</code> 节点下的各个 <code>dependency</code> 列举完了。我的实际经历还要更悲催一些，因为代码太老，有些依赖的包名都发生了变化，不过最终还是搞定了。</p>
<p>打包并不止一种方式，上面的方式是把依赖包复制到主包所在路径下，还有一种把所有依赖都打进最终单一包体内的方法。以下<a href="https://blog.csdn.net/xiao__gui/article/details/47341385">所附文章</a>内容中说的很详细。</p>
<hr />
<h1 id="articleContentId" class="title-article">Maven 生成可以直接运行的 jar 包的多种方式</h1>
<p>Maven 可以使用 <code>mvn package</code> 指令对项目进行打包，如果使用 <code>java -jar xxx.jar</code> 执行运行 <code>jar</code> 文件，会出现“<code>no main manifest attribute, in xxx.jar</code>”（没有设置 <code>Main-Class</code>）、<code>ClassNotFoundException</code>（找不到依赖包）等错误。</p>
<p>要想 <code>jar</code> 包能直接通过 <code>java -jar xxx.jar</code> 运行，需要满足：</p>
<p>1、在 <code>jar</code> 包中的 <code>META-INF/MANIFEST.MF</code> 中指定 <code>Main-Class</code>，这样才能确定程序的入口在哪里；</p>
<p>2、要能加载到依赖包。</p>
<p>使用 Maven 有以下几种方法可以生成能直接运行的 <code>jar</code> 包，可以根据需要选择一种合适的方法。</p>
<h2><strong>方法一：使用 maven-jar-plugin 和 maven-dependency-plugin 插件打包</strong></h2>
<p>在 <code>pom.xml</code> 中配置：</p><pre class="crayon-plain-tag">&lt;build&gt;
	&lt;plugins&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
			&lt;artifactId&gt;maven-jar-plugin&lt;/artifactId&gt;
			&lt;version&gt;2.6&lt;/version&gt;
			&lt;configuration&gt;
				&lt;archive&gt;
					&lt;manifest&gt;
						&lt;addClasspath&gt;true&lt;/addClasspath&gt;
						&lt;classpathPrefix&gt;lib/&lt;/classpathPrefix&gt;
						&lt;mainClass&gt;com.xxg.Main&lt;/mainClass&gt;
					&lt;/manifest&gt;
				&lt;/archive&gt;
			&lt;/configuration&gt;
		&lt;/plugin&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
			&lt;artifactId&gt;maven-dependency-plugin&lt;/artifactId&gt;
			&lt;version&gt;2.10&lt;/version&gt;
			&lt;executions&gt;
				&lt;execution&gt;
					&lt;id&gt;copy-dependencies&lt;/id&gt;
					&lt;phase&gt;package&lt;/phase&gt;
					&lt;goals&gt;
						&lt;goal&gt;copy-dependencies&lt;/goal&gt;
					&lt;/goals&gt;
					&lt;configuration&gt;
						&lt;outputDirectory&gt;${project.build.directory}/lib&lt;/outputDirectory&gt;
					&lt;/configuration&gt;
				&lt;/execution&gt;
			&lt;/executions&gt;
		&lt;/plugin&gt;
	&lt;/plugins&gt;
&lt;/build&gt;</pre><p>maven-jar-plugin 用于生成 <code>META-INF/MANIFEST.MF</code> 文件的部分内容，<code>&lt;mainClass&gt;com.xxg.Main&lt;/mainClass&gt;</code> 指定 <code>MANIFEST.MF</code> 中的 <code>Main-Class</code>，<code>&lt;addClasspath&gt;true&lt;/addClasspath&gt;</code> 会在 <code>MANIFEST.MF</code> 加上 <code>Class-Path</code> 项并配置依赖包，<code>&lt;classpathPrefix&gt;lib/&lt;/classpathPrefix&gt;</code> 指定依赖包所在目录。</p>
<p>例如下面是一个通过 maven-jar-plugin 插件生成的 <code>MANIFEST.MF</code>文件片段：</p><pre class="crayon-plain-tag">Class-Path: lib/commons-logging-1.2.jar lib/commons-io-2.4.jar
Main-Class: com.xxg.Main</pre><p>只是生成 <code>MANIFEST.MF</code> 文件还不够，maven-dependency-plugin 插件用于将依赖包拷贝到 <code>&lt;outputDirectory&gt;${project.build.directory}/lib&lt;/outputDirectory&gt;</code> 指定的位置，即 <code>lib</code> 目录下。</p>
<p>配置完成后，通过 <code>mvn package</code> 指令打包，会在 <code>target</code> 目录下生成 <code>jar</code> 包，并将依赖包拷贝到 <code>target/lib</code> 目录下，目录结构如下：</p><pre class="crayon-plain-tag">`-+- target
  +- lib
  | +- commons-io-2.4.jar
  | +- commons-logging-1.2.jar
  +- test.jar</pre><p>指定了 <code>Main-Class</code>，有了依赖包，那么就可以直接通过 <code>java -jar xxx.jar</code> 运行 <code>jar</code> 包了。</p>
<p>这种方式生成 <code>jar</code> 包有个缺点，就是生成的 <code>jar</code> 包太多不便于管理，下面两种方式只生成一个 <code>jar</code> 文件，包含项目本身的代码、资源以及所有的依赖包。</p>
<h2>方法二：使用 maven-assembly-plugin 插件打包</h2>
<p>在 <code>pom.xml</code> 中配置：</p><pre class="crayon-plain-tag">&lt;build&gt;
	&lt;plugins&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
			&lt;artifactId&gt;maven-assembly-plugin&lt;/artifactId&gt;
			&lt;version&gt;2.5.5&lt;/version&gt;
			&lt;configuration&gt;
				&lt;archive&gt;
					&lt;manifest&gt;
						&lt;mainClass&gt;com.xxg.Main&lt;/mainClass&gt;
					&lt;/manifest&gt;
				&lt;/archive&gt;
				&lt;descriptorRefs&gt;
					&lt;descriptorRef&gt;jar-with-dependencies&lt;/descriptorRef&gt;
				&lt;/descriptorRefs&gt;
			&lt;/configuration&gt;
		&lt;/plugin&gt;
	&lt;/plugins&gt;
&lt;/build&gt;</pre><p>打包方式：</p><pre class="crayon-plain-tag">mvn package assembly:single</pre><p>打包后会在 <code>target</code> 目录下生成一个 <code>xxx-jar-with-dependencies.jar</code> 文件，这个文件不但包含了自己项目中的代码和资源，还包含了所有依赖包的内容。所以可以直接通过 <code>java -jar</code> 来运行。</p>
<p>此外还可以直接通过 <code>mvn package</code> 来打包，无需 <code>assembly:single</code>，不过需要加上一些配置：</p><pre class="crayon-plain-tag">&lt;build&gt;
	&lt;plugins&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
			&lt;artifactId&gt;maven-assembly-plugin&lt;/artifactId&gt;
			&lt;version&gt;2.5.5&lt;/version&gt;
			&lt;configuration&gt;
				&lt;archive&gt;
					&lt;manifest&gt;
						&lt;mainClass&gt;com.xxg.Main&lt;/mainClass&gt;
					&lt;/manifest&gt;
				&lt;/archive&gt;
				&lt;descriptorRefs&gt;
					&lt;descriptorRef&gt;jar-with-dependencies&lt;/descriptorRef&gt;
				&lt;/descriptorRefs&gt;
			&lt;/configuration&gt;
			&lt;executions&gt;
				&lt;execution&gt;
					&lt;id&gt;make-assembly&lt;/id&gt;
					&lt;phase&gt;package&lt;/phase&gt;
					&lt;goals&gt;
						&lt;goal&gt;single&lt;/goal&gt;
					&lt;/goals&gt;
				&lt;/execution&gt;
			&lt;/executions&gt;
		&lt;/plugin&gt;
	&lt;/plugins&gt;
&lt;/build&gt;</pre><p>其中 <code>&lt;phase&gt;package&lt;/phase&gt;</code>、<code>&lt;goal&gt;single&lt;/goal&gt;</code> 即表示在执行 package 打包时，执行 <code>assembly:single</code>，所以可以直接使用 <code>mvn package</code> 打包。</p>
<p>不过，如果项目中用到 Spring Framework，用这种方式打出来的包运行时会出错，使用下面的方法三可以处理。</p>
<h2>方法三：使用 maven-shade-plugin 插件打包</h2>
<p>在 <code>pom.xml</code> 中配置：</p><pre class="crayon-plain-tag">&lt;build&gt;
	&lt;plugins&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
			&lt;artifactId&gt;maven-shade-plugin&lt;/artifactId&gt;
			&lt;version&gt;2.4.1&lt;/version&gt;
			&lt;executions&gt;
				&lt;execution&gt;
					&lt;phase&gt;package&lt;/phase&gt;
					&lt;goals&gt;
						&lt;goal&gt;shade&lt;/goal&gt;
					&lt;/goals&gt;
					&lt;configuration&gt;
						&lt;transformers&gt;
							&lt;transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"&gt;
								&lt;mainClass&gt;com.xxg.Main&lt;/mainClass&gt;
							&lt;/transformer&gt;
						&lt;/transformers&gt;
					&lt;/configuration&gt;
				&lt;/execution&gt;
			&lt;/executions&gt;
		&lt;/plugin&gt;
	&lt;/plugins&gt;
&lt;/build&gt;</pre><p>配置完成后，执行 <code>mvn package</code> 即可打包。在 <code>target</code> 目录下会生成两个 <code>jar</code> 包，注意不是 <code>original-xxx.jar</code> 文件，而是另外一个。和 maven-assembly-plugin 一样，生成的 <code>jar</code> 文件包含了所有依赖，所以可以直接运行。</p>
<p>如果项目中用到了 Spring Framework，将依赖打到一个 <code>jar</code> 包中，运行时会出现读取 XML schema 文件出错。原因是 Spring Framework 的多个 <code>jar</code> 包中包含相同的文件 <code>spring.handlers</code> 和 <code>spring.schemas</code>，如果生成一个 <code>jar</code> 包会互相覆盖。为了避免互相影响，可以使用 AppendingTransformer 来对文件内容追加合并：</p><pre class="crayon-plain-tag">&lt;build&gt;
	&lt;plugins&gt;
		&lt;plugin&gt;
			&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
			&lt;artifactId&gt;maven-shade-plugin&lt;/artifactId&gt;
			&lt;version&gt;2.4.1&lt;/version&gt;
			&lt;executions&gt;
				&lt;execution&gt;
					&lt;phase&gt;package&lt;/phase&gt;
					&lt;goals&gt;
						&lt;goal&gt;shade&lt;/goal&gt;
					&lt;/goals&gt;
					&lt;configuration&gt;
						&lt;transformers&gt;
							&lt;transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"&gt;
								&lt;mainClass&gt;com.xxg.Main&lt;/mainClass&gt;
							&lt;/transformer&gt;
							&lt;transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"&gt;
								&lt;resource&gt;META-INF/spring.handlers&lt;/resource&gt;
							&lt;/transformer&gt;
							&lt;transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"&gt;
								&lt;resource&gt;META-INF/spring.schemas&lt;/resource&gt;
							&lt;/transformer&gt;
						&lt;/transformers&gt;
					&lt;/configuration&gt;
				&lt;/execution&gt;
			&lt;/executions&gt;
		&lt;/plugin&gt;
	&lt;/plugins&gt;
&lt;/build&gt;</pre><p></p>
<hr />
<p>另外还有一篇文章详述 <code>pom.xml</code> 中的各个节点的含义，实在太长，就不全文引入了：<a href="https://www.cnblogs.com/hafiz/p/5360195.html">https://www.cnblogs.com/hafiz/p/5360195.html</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2022/06/13/eclipse-%e4%b8%8b%e7%9a%84-java-%e5%b7%a5%e7%a8%8b%e8%bf%81%e7%a7%bb%e8%87%b3-maven/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AsyncTask API 被弃用，继任者何？</title>
		<link>https://www.somedoc.net/2022/03/21/asynctask-api-%e8%a2%ab%e5%bc%83%e7%94%a8%ef%bc%8c%e7%bb%a7%e4%bb%bb%e8%80%85%e4%bd%95%ef%bc%9f/</link>
					<comments>https://www.somedoc.net/2022/03/21/asynctask-api-%e8%a2%ab%e5%bc%83%e7%94%a8%ef%bc%8c%e7%bb%a7%e4%bb%bb%e8%80%85%e4%bd%95%ef%bc%9f/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Mon, 21 Mar 2022 12:14:51 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[技术]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=4994</guid>

					<description><![CDATA[标题是一个常见问题，老夫维护的一份代码里，AsyncTask <a href="https://www.somedoc.net/2022/03/21/asynctask-api-%e8%a2%ab%e5%bc%83%e7%94%a8%ef%bc%8c%e7%bb%a7%e4%bb%bb%e8%80%85%e4%bd%95%ef%bc%9f/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>标题是一个常见问题，老夫维护的一份代码里，AsyncTask 也是在好几个地方被使用了的，所以也想知道 Android 达人们的选择答案。</p>
<p>在网上看了些资料，讨论比较集中的仍然是在 Stack Overflow 上，链接在此：<a href="https://stackoverflow.com/questions/58767733/">The AsyncTask API is deprecated in Android 11. What are the alternatives?</a>。</p>
<p>记得还看过一篇为 AsyncTask 正名的文章，但急切间找不到 URL 了，找到之后回来补上。</p>
<p>更新：有点意思，文章找到了，标题竟然跟本文的中文标题非常契合，叫《<a href="https://www.techyourchance.com/asynctask-deprecated/">Android AsyncTask Deprecated, Now What?</a>》（November 15, 2019 by Vasiliy），顺便就翻译如下吧。</p>
<p>================================================================================</p>
<p>在过去的十年中，AysncTask 一直是在 Android 应用程序中编写并发代码的一种非常流行的方法。然而，AsyncTask 的时代结束了，因为从 Android 11 开始，AsyncTask 被废弃了。</p>
<p>在这篇文章中，我将回顾促使 AsyncTask 被废弃的官方声明，解释为什么它没有道理，然后分享这个框架的真正问题列表，这些问题真正合理化了它的退役。此外，我还会分享我对 Android 中并发 API 的未来的看法，并建议如果你的代码库中遍布 AsyncTasks，你应该怎么做。</p>
<p><strong>废弃 AsyncTask 的官方原因</strong></p>
<p>对 AsyncTask 的正式废弃，以及这一决定的动机，是在<a href="https://android-review.googlesource.com/c/platform/frameworks/base/+/1156409/6/core/java/android/os/AsyncTask.java">这次提交</a>中。Javadoc 中新增加的一段指出：</p>
<blockquote><p>AsyncTask 的目的是使 UI 线程的使用恰当且方便。然而，最常见的使用情况是集成到 UI 中，这将导致 Context 泄露、错过回调或配置变化时的崩溃。它在不同版本的平台上的行为也有所不一致，会吞噬来自 <code>doInBackground</code> 的异常，而且与直接使用 Executor 相比没有提供太多的功效。</p></blockquote>
<p>虽然这是谷歌的官方声明，但其中有几个不准确的地方值得指出。</p>
<p>首先，AsyncTask 从来都不是为了 &#8220;使 UI 线程的使用更加恰当和方便&#8221;。它的目的是将长期运行的操作从 UI 线程减负到后台线程，然后将这些操作的结果送回给 UI 线程。我知道，我这是吹毛求疵。然而，在我看来，当谷歌废弃了这么多开发者使用的 API 时，应该在废弃信息上投入更多的精力，以防止进一步的混乱。</p>
<p>其所说的这段话中更有趣的部分是，“这将导致 Context 泄露，错过回调，或在配置改变时崩溃”。也就是说，谷歌从根本上指出，对 AsyncTask 最常见的使用方式会自动导致非常严重的问题。然而，有许多高质量的应用程序使用了 AsyncTask 并且工作得完美无缺。甚至 AOSP 内部的一些类也使用 AsyncTask。为什么他们没有遇到这些问题呢？</p>
<p>为了回答这个问题，让我们讨论一下 AsyncTask 和内存泄露之间的关系。</p>
<p><strong>AsyncTask 和内存泄漏</strong></p>
<p>以下这个 AsyncTask 永远地泄露了所包围的 Fragment（或 Activity）对象：</p><pre class="crayon-plain-tag">private final AtomicInteger counter = new AtomicInteger(0);
@Override
public void onStart() {
    super.onStart();
    new AsyncTask&lt;Void, Void, Void&gt;() {
        @Override
        protected Void doInBackground(Void... voids) {
            while (true) {
                Log.d("AsyncTask", "count: " + counter.get());
                counter.incrementAndGet();
            }
        }
    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}</pre><p>看起来这个例子证明了谷歌的观点：AsyncTask 确实会导致内存泄漏。我们也许应该使用一些其他的方法来编写并发的代码!</p>
<p>好吧，让我们试一试吧。这是同一个例子，用 RxJava 重写的，</p><pre class="crayon-plain-tag">@Override
public void onStart() {
    super.onStart();
    Observable.fromCallable(() -&gt; {
        while (true) {
            Log.d("RxJava", "count: " + counter.get);
            counter.incrementAndGet();
        }
    }).subscribeOn(Schedulers.computation()).subscribe();
}</pre><p>它以完全相同的方式泄漏了包围的 Fragment（或 Activity）对象。</p>
<p>也许新的 Kotlin Coroutine 会有帮助？这是我使用 Coroutine 实现相同功能的方式：</p><pre class="crayon-plain-tag">override fun onStart() {
    super.onStart()
    CoroutineScope(Dispatchers.Main).launch {
        withContext(Dispatchers.Default) {
            while (true) {
                Log.d("Coroutines", "count: ${counter.get()}")
                counter.incrementAndGet()
            }
        }
    }
}</pre><p>不幸的是，它导致了完全相同的内存泄漏。</p>
<p>看起来这个功能泄漏了封闭的 Fragment（或 Activity），不管选择什么样的多线程框架。事实上，即使我使用一个裸 Thread 类，也会导致泄漏，</p><pre class="crayon-plain-tag">@Override
public void onStart() {
    super.onStart();
    new Thread(() -&gt; {
        while (true) {
            Log.d("Thread", "count: " + counter.get());
            counter.incrementAndGet();
        }
    }).start();
}</pre><p>所以，这并不是 AsyncTask 的问题，而是我写的逻辑问题。为了证明这一点，让我们修改一下使用 AsyncTask 来修复内存泄漏的例子。</p><pre class="crayon-plain-tag">private AsyncTask mAsyncTask;

@Override
public void onStart() {
    super.onStart();
    mAsyncTask = new AsyncTask&lt;Void, Void, Void&gt;() {
        @Override
        protected Void doInBackground(Void... voids) {
            while (!isCancelled()) {
                Log.d("AsyncTask", "count: " + counter.get());
                counter.incrementAndGet();
            }
            return null;
        }
    }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

@Override
public void onStop() {
    super.onStop();
    mAsyncTask.cancel(true);
}</pre><p>在这种情况下，我使用了可怕的 AsyncTask，但没有泄漏。这是个魔术!</p>
<p>嗯，当然这不是魔术。它只是反映了这样一个事实：你可以用 AsyncTask 编写安全、正确的多线程代码，就像你可以用其他多线程框架一样。实际上，AsyncTask 和内存泄露之间并没有直接的联系。因此，普遍认为 AsyncTask 会自动导致内存泄露，以及 AOSP 中新的弃用信息，都是根本不正确的。</p>
<p>[编辑：本文的原始版本使用了本地计数器变量，而并非使它作为一个封闭的 Activity 或 Fragment 的成员。正如一些读者正确指出的，相对于内部类，如果没有必要，lambda 不会捕获对父对象的引用。因此，如果计数器变量是局部的，那么上述使用 lambda 的例子实际上不会泄漏封闭的 Activity 或 Fragment。因此，我对这些例子做了一些修改。然而请注意，由于 AsyncTask 早在我们可以使用 lambda 之前就已经在 Android 中使用了，所以 lambda 的行为不同并不那么重要。此外，我不建议依靠 lambda 的这一属性作为避免内存泄漏的手段，因为如果你这样做，你总是只需要修改一小段代码就可以了]。</p>
<p>你现在可能想知道：如果这种认为 AsyncTask 会导致内存泄漏的想法是不正确的，为什么它在 Android 开发者中如此普遍？</p>
<p>好吧，在 Android Studio 中有一个内置的 Lint 规则，它警告你并建议使你的 AsyncTask 成为静态的以避免内存泄漏。这个警告和建议也是不正确的，但是在项目中使用了 AsyncTask 的开发者得到了这个警告，而且因为它来自于谷歌，所以他们被其它外表所惑。</p>
<p><img fetchpriority="high" decoding="async" class="wp-image-5253 entered lazyloaded aligncenter" src="https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-1200x195.jpg" sizes="(max-width: 1200px) 100vw, 1200px" srcset="https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-1200x195.jpg 1200w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-300x49.jpg 300w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-768x125.jpg 768w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-1024x167.jpg 1024w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-200x33.jpg 200w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-400x65.jpg 400w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-600x98.jpg 600w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-800x130.jpg 800w" alt="" width="1200" height="195" data-lazy-srcset="https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-1200x195.jpg 1200w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-300x49.jpg 300w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-768x125.jpg 768w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-1024x167.jpg 1024w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-200x33.jpg 200w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-400x65.jpg 400w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-600x98.jpg 600w, https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-800x130.jpg 800w" data-lazy-sizes="(max-width: 1200px) 100vw, 1200px" data-lazy-src="https://www.techyourchance.com/wp-content/uploads/2019/11/async_task_lint_warning-1200x195.jpg" data-ll-status="loaded" /></p>
<p>在我看来，上述 Lint 警告是 AsyncTask 导致内存泄露的神话如此普遍的原因：它是由谷歌自己强加于开发者的。</p>
<p><strong>如何避免多线程代码中的内存泄漏</strong></p>
<p>到目前为止，我们确定 AsyncTask 和内存泄漏之间没有自动的因果关系。此外，你还看到，任何多线程框架都可能发生内存泄漏。因此，现在你可能想知道如何在自己的应用程序中避免内存泄露。</p>
<p>我不会详细回答这个问题，因为我想紧扣主题，但我也不想让你空手而归。因此，请允许我列出在 Android 中编写正确的多线程代码所需要了解的概念。</p>
<ul>
<li>垃圾收集器</li>
<li>垃圾收集器的根源</li>
<li>与垃圾收集有关的线程生命周期</li>
<li>从内部类到父对象的隐式引用</li>
</ul>
<p>如果你理解了这些概念，你的代码中就不太可能出现内存泄漏。另一方面，如果你不理解这些概念，而且你写的是并发代码，那么不管你用什么多线程框架，引入内存泄露只是时间问题。</p>
<p>[由于这对所有的 Android 开发者来说都是很重要的知识，我决定将我的 <a href="https://youtu.be/UPq1LDxL5_w">Android 多线程课程的第一部分</a>上传到 YouTube。它非常详细地涵盖了并发性的基础知识]。</p>
<p><strong>AsyncTask 无缘无故被弃用吗？</strong></p>
<p>由于 AsyncTask 不会自动导致内存泄漏，看起来 Google 错误地废除了它，没有任何理由。嗯，不完全是。</p>
<p>在过去的几年里，AsyncTask 已经被 Android 开发者自己“有效废弃”了。我们中的许多人公开主张不要在应用程序中使用这个 API，而我个人则对那些广泛使用 AsyncTask 的代码库的开发者感到遗憾。很明显，AsyncTask 是非常有问题的 API。如果你问我，谷歌应该更早地废止它。</p>
<p>因此，虽然谷歌仍然对他们自己的创造感到困惑，但废弃本身是非常合适和受欢迎的。至少，它可以让新的安卓开发者知道，他们不需要投入时间来学习这个 API，也不该在他们的应用程序中使用它。</p>
<p>说了这么多，你可能还是不明白到底为什么 AsyncTask 是“坏”的，为什么这么多开发者如此讨厌它。在我看来，这是一个非常有趣的问题，也是一个很有实际意义的问题。毕竟，如果我们不了解 AsyncTask 的问题所在，就无法保证我们不会再次重复同样的错误。</p>
<p>因此，在接下来的章节中，我将解释 AsyncTask 的真正问题。</p>
<p><strong>AsyncTask 问题 1：使多线程更加复杂</strong></p>
<p>AsyncTask 的主要“卖点”之一一直是承诺你不需要自己处理线程类和其他多线程原语。它应该让多线程变得更简单，特别是对于新手 Android 开发者来说。听起来不错，对吧？然而，在实践中，这种“简单性”适得其反。</p>
<p>AsyncTask 的类级 Javadoc 使用了 16 次“线程”一词。如果你不了解什么是线程，你就也根本无法理解它。此外，这个 Javadoc 陈述了一堆 AsyncTask 特有的约束和条件。换句话说，如果你想使用 AsyncTask，你需要了解线程，而且你还需要了解 AsyncTask 本身的许多细微差别。无论怎么想，这都不是“更简单”的意思。</p>
<p>此外，在我看来，并发性是一般软件（而且，硬件也是如此）中最复杂的话题之一。与其他许多概念不同，你不能在多线程中走捷径，因为即使是最小的错误也会导致非常严重的错误，这将是非常难以调查的。有一些应用程序被多线程错误影响了好几个月，哪怕是在开发人员已经知道它们的存在之后。他们只是没能找到这些错误。</p>
<p>因此，在我看来，根本没有办法简化并发性，AsyncTask 的野心从一开始就注定要失败。</p>
<p><strong>AsyncTask 问题 2：糟糕的文档</strong></p>
<p>安卓系统的文档并不是什么秘密（在此试图礼貌一点）。多年来，它得到了改善，但是，即使在今天，我也不会把它称为好的。在我看来，不幸的文档是导致 AsyncTask 陷入困境的主要因素。如果 AsyncTask 只是一个工程量过大、复杂而细微的多线程框架（就像它现在这样），但有好的文档，它就可以继续成为生态系统的一部分。但是 AsyncTask 的文档很糟糕。</p>
<p>最糟糕的是那些例子。它们展示了编写多线程代码的最不幸的方法：所有东西都在 Activity 里面，完全无视生命周期，没有讨论取消的情况，等等。如果你在自己的应用程序中使用这些例子，内存泄漏和不正确的行为将是非常肯定的。</p>
<p><strong>AsyncTask 问题 3：过于复杂</strong></p>
<p>AsyncTask 有三个泛型参数。三个! 如果我没记错的话，我从未见过任何其他类需要这么多泛型。</p>
<p>我仍然记得我第一次接触 AsyncTask 的情景。那时，我已经对 Java 线程有了一些了解，不明白为什么 Android 中的多线程会如此困难。三个泛型参数非常难以理解，感觉很别扭。此外，由于 AsyncTask 的方法是在不同的线程上调用的，我不得不不断地提醒自己，然后通过阅读文档来验证自己的理解是否正确。</p>
<p><strong>AsyncTask 问题 4：滥用继承</strong></p>
<p>AsyncTask 的理念是以继承为基础的：只要你需要在后台执行一个任务，你就可以扩展 AsyncTask。再加上糟糕的文档，继承理念将开发者推向了编写庞大的类的方向，这些类将多线程、领域和 UI 逻辑以最低效和难以维护的方式耦合在一起。</p>
<p>如果遵循 Effective Java 中的“偏爱组合而非继承”的规则，应该会使 AsyncTask 相当不同。</p>
<p><strong>AsyncTask 问题 5：可靠性</strong></p>
<p>简单地说，支持 AsyncTask 的默认 THREAD_POOL_EXECUTOR 是错误的配置，不可靠。多年来，谷歌至少调整了两次配置（<a href="https://github.com/aosp-mirror/platform_frameworks_base/commit/a9be47cea45c19f2869732252e9922bf88fa4d86">这次提交</a>和<a href="https://github.com/aosp-mirror/platform_frameworks_base/commit/719c44e03b97e850a46136ba336d729f5fbd1f47">这次</a>），但它仍然<a href="https://twitter.com/ArtemR/status/912145880760717312">使官方的 Android 设置应用程序崩溃</a>。</p>
<p>大多数安卓应用都不需要这种级别的并发性。然而，你永远不知道一年后你的用例会是什么，所以使用不可靠的解决方案是有问题的。</p>
<p><strong>AsyncTask 问题 6：并发性误解</strong></p>
<p>这一点也与糟糕的文档有关，但我认为它值得单独作为一个要点。executeOnExecutor() 方法的 Javadoc 指出，</p>
<blockquote><p>允许多个任务从线程池中并行运行，通常不是人们想要的，因为它们的操作顺序没有被定义。[……] 这样的变化最好以串行方式执行；为了保证这样的工作被串行化，无论平台版本如何，你可以以 SERIAL_EXECUTOR 来使用这个函数</p></blockquote>
<p>嗯，这是不对的。在大多数情况下，允许多个任务并发运行，正是你从 UI 线程中减负工作时想要的。</p>
<p>例如，假设你发送了一个网络请求，但它由于某种原因超时了。OkHttp 的默认超时时间是 10 秒。如果你确实使用了 SERIAL_EXECUTOR —— 它在任何时候都只执行一个单一的任务 —— 那么你就会停止你应用程序中所有的后台工作 10 秒。如果你碰巧发送了两个请求，并且都超时了呢？那么，后台 20 秒无处理。更糟糕的是，这对几乎所有其他类型的后台任务都是一样的：数据库查询、图像处理、计算、IPC 等等。</p>
<p>正如文档中所说，减负到例如线程池中的操作的顺序没有被定义。然而，这并不是一个问题。事实上，这几乎就是并发性的定义。</p>
<p>因此，在我看来，官方文档中的这句话表明，AsyncTask 的作者对并发性有一些非常严重的错误认识。我实在看不出官方文档中出现这种误导性信息的任何其他解释。</p>
<p><strong>AsyncTask 的未来</strong></p>
<p>希望我说服了你，废除 AsyncTask API 是谷歌的一个好举措。然而，对于现在使用 AsyncTask 的项目来说，这些并不是什么好消息。如果你在这样的项目中工作，你现在应该重构你的代码吗？</p>
<p>首先，我认为你不需要主动从你的代码中删除 AsyncTasks。这个 API 的废弃并不意味着它将停止工作。事实上，如果 AsyncTask 会在安卓系统存在的时间里一直存在，我也不会感到惊讶。太多的应用程序，包括谷歌自己的应用程序，都在使用这个 API。即使它在 5 年后被删除，你也能把它的代码复制粘贴到你自己的项目中，并修改导入语句以保持逻辑的正常运行。</p>
<p>这次废弃的主要影响将是对新的 Android 开发者。今后，他们会明白，他们不需要投入时间去学习 AsyncTask，也不会在新的应用程序中使用它（希望如此）。</p>
<p><strong>AsyncTask 替代品</strong></p>
<p>由于 AsyncTask 现在已被废弃，它留下了一些空白，必须由其他的并发方法来填补。所以，让我们来讨论 AsyncTask 的替代品。</p>
<p>如果你刚刚开始你的 Android 之旅，并且使用 Java，我建议使用裸线程类和 UI 处理程序的组合。许多 Android 开发者会对这个建议感到厌恶，但我自己用过一段时间，它比 AsyncTask 好用得多，好得多。为了收集更多关于这项技术的反馈，我创建了这个 Twitter 投票。在写这篇文章的时候，结果是这样的，</p>
<p><img decoding="async" class="wp-image-5281 entered lazyloaded aligncenter" src="https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-400x349.jpg" sizes="(max-width: 400px) 100vw, 400px" srcset="https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-400x349.jpg 400w, https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-300x262.jpg 300w, https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-200x174.jpg 200w, https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-600x523.jpg 600w, https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback.jpg 741w" alt="" width="400" height="349" data-lazy-srcset="https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-400x349.jpg 400w, https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-300x262.jpg 300w, https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-200x174.jpg 200w, https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-600x523.jpg 600w, https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback.jpg 741w" data-lazy-sizes="(max-width: 400px) 100vw, 400px" data-lazy-src="https://www.techyourchance.com/wp-content/uploads/2019/11/thread_ui_handler_feedback-400x349.jpg" data-ll-status="loaded" /></p>
<p>看起来我不是唯一使用裸线程的人，大多数尝试过这种方法的开发者都认为它还不错。</p>
<p>如果你已经有了一点经验，你可以用一个集中的 ExecutorService 来代替裸线程。对我来说，使用 Thread 类的最大问题是，我经常忘记启动线程，然后不得不花时间调试这些愚蠢的错误。这是很烦人的。ExecutorService 解决了这个问题。</p>
<p>我个人更喜欢使用我自己的 ThreadPoster 库来实现 Java 中的多线程。它是对 ExecutorService 和处理程序的非常轻量级的抽象。这个库使多线程更明确，单元测试更容易。</p>
<p>如果你使用 Kotlin，那么上面的建议仍然有效，但还有一个需要考虑的问题。看起来 Coroutine 框架将成为 Kotlin 的官方并发原语。换句话说，即使 Android 中的 Kotlin 在引擎盖下使用线程，Coroutine 也将成为 Kotlin 应用程序的标准。</p>
<p>[如果你想学习 Coroutine，你会发现我的 Coroutine 课程很有帮助。它将为你提供在你的项目中使用 Coroutine 所需的所有知识和技能]。</p>
<p>最重要的是，无论你选择哪种方法，都要投入时间学习多线程的基础知识。正如你在这篇文章中所看到的，你的并发代码的正确性不是由框架决定的，而是由你对基本原理的理解决定的。</p>
<p><strong>结论</strong></p>
<p>在我看来，AsyncTask 的废弃是早该进行的。这个 API 有太多的问题，多年来造成了很多麻烦。</p>
<p>不幸的是，官方的弃用声明包含了不正确的信息，所以会让未来遇到 AsyncTask 的开发者感到困惑。我希望这篇文章澄清了围绕 AsyncTask 的情况，并让你对 Android 中的并发性有了更高级的了解。</p>
<p>对于现在使用 AsyncTask 的项目来说，这种废弃是有问题的，不需要立即采取行动。AsyncTask 不会很快从 Android 中删除，或者/也许，永远不会。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2022/03/21/asynctask-api-%e8%a2%ab%e5%bc%83%e7%94%a8%ef%bc%8c%e7%bb%a7%e4%bb%bb%e8%80%85%e4%bd%95%ef%bc%9f/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>使用 Gson 序列化容器的问题与解决</title>
		<link>https://www.somedoc.net/2022/03/19/%e4%bd%bf%e7%94%a8-gson-%e5%ba%8f%e5%88%97%e5%8c%96%e5%ae%b9%e5%99%a8%e7%9a%84%e9%97%ae%e9%a2%98%e4%b8%8e%e8%a7%a3%e5%86%b3/</link>
					<comments>https://www.somedoc.net/2022/03/19/%e4%bd%bf%e7%94%a8-gson-%e5%ba%8f%e5%88%97%e5%8c%96%e5%ae%b9%e5%99%a8%e7%9a%84%e9%97%ae%e9%a2%98%e4%b8%8e%e8%a7%a3%e5%86%b3/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Sat, 19 Mar 2022 07:04:22 +0000</pubDate>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[问题解决]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=4979</guid>

					<description><![CDATA[前两天修改一个项目的代码，其序列化数据的方法中，使用了 Gs <a href="https://www.somedoc.net/2022/03/19/%e4%bd%bf%e7%94%a8-gson-%e5%ba%8f%e5%88%97%e5%8c%96%e5%ae%b9%e5%99%a8%e7%9a%84%e9%97%ae%e9%a2%98%e4%b8%8e%e8%a7%a3%e5%86%b3/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>前两天修改一个项目的代码，其序列化数据的方法中，使用了 Gson 来处理。作为数据的管理类，有一个成员是 List<> 数据类型，经过我的扩充，List 中的元素种类由一种变为了多种，于是对这多种数据进行了抽象，将它们都改造为从同一个基类派生而来，希望能达到只需把 List<> 原来的单一实体数据类改成抽象后的基类即可，无需再改动其他诸如插入、删除等的逻辑。简而言之，若之前的列表形如 <code>List<Data1></code> 的话，现在改成 <code>List<DataCore></code>，而 <code>Data1</code> 以及新出现的 <code>Data2</code> 乃至 <code>DataN</code> 都是 <code>DataCore</code> 的子类。</p>
<p>测试的一开始是没有问题的，既可以在运行时添加和移除，也能正常序列化到永久存储，而且打印出的序列化数据都挺完整，不由得小小自鸣得意一下。不过不和谐的声音很快就出现了，发生在反序列化的过程中，会报出大致为“无法实例化 DataCore 抽象类”这样的问题。一开始颇令人惊讶，毕竟数据的完整性是经过了检查的，在遇到具体派生类的数据时，怎么会去实例化基类呢？</p>
<p>查阅资料才知道，Java 的泛型容器，仅在编译时检查元素类型是否符合，至于每个元素真正的运行时实体类型信息，它是不负责的，这些信息在序列化过程中并没有随每一个元素对象保存下来，而当它作为 <code>List<DataCore></code> 这样的类型反序列化的时候，它天然认为所有的数据中每一个都应该被再次实例化为一个对应的 <code>DataCore</code> 对象，而非其当初真实的派生类对象。</p>
<p>在知晓上述基础知识后，着手对数据结构进行改进，第一步是把具体类的对象分别存储到独立的 List 中，把类的名字与之关联起来，也进行永久化，这部分简单；第二步是，由于数据管理类本身不希望提前预知共有多少个具体的派生数据类，因此前述的独立 List，仍然写作 <code>List<DataCore></code> 而不是 <code>List<DataN></code> 这样的形式。当反序列化时，每个列表都根据对应的类名，通过其类对象来处理各个元素的实例化工作。</p>
<p>若非如此的话，在 Gson 的常规操作方式下，应该对每一个具体的数据派生类编制并注册对应的 <code>TypeAdapter</code> 序列化适配器来完成。这个方法的弊端就在于，将来的每一个可能的新的数据派生类，都要写一个对应的适配器，略显繁冗。国人有把 Gson 官方使用指南进行了翻译的译文系列，可以参考：</p>
<ul>
<li><a href="https://www.jianshu.com/p/fc5c9cdf3aab">Gson 全解析（上）—— Gson 基础</a></li>
<li><a href="https://www.jianshu.com/p/8cc857583ff4">Gson 全解析（中）—— TypeAdapter 的使用</a></li>
<li><a href="https://www.jianshu.com/p/17a68d4fffbe">Gson 全解析（下）—— Gson 性能分析</a></li>
</ul>
<p>关于对 List 这样的容器相关的序列化，还有一篇英文 blog 也可以参考：<a href="https://www.baeldung.com/gson-list">Serializing and Deserializing a List with Gson</a>。</p>
<p>在 Gson 对一个类的所有数据成员进行序列化的过程中，如何把指定的某个成员排除在外，一度令人厌烦。乍看之下，Gson 有一个注解，正是为此目的而设，即 <code>@Expose(serialize = boooleanValue, deserialize = boooleanValue)</code>。可惜当老夫兴冲冲在一个成员上加上此注解语句自以为完事大吉的时候，测试发现注解无效，该成员仍然被序列化了。查资料才发现，这个注解<strong>默认无效</strong>！需要提前调用 <code>GsonBuilder</code> 的 <code>excludeFieldsWithoutExposeAnnotation</code> 方法才会生效。可是，一看这个方法名称，你会第二次懵圈：这个调用确实会导致 <code>@Expose</code> 注解被处理，但是！没有此注解的成员则统统会被忽略！一瞬间，我认为 Google 制定此库的策略的架构师，脑子简直是太反常了。</p>
<p>不过好在跟我同感的前辈并不稀缺，已经有人进行了许多努力来换缓解这一逆天的设定，我最后选择了其中一种，并由此又多少体会到了注解的玩法还是很花的。至于都有哪些解决思路，阅读本文的读者，我强烈建议各位收藏这一篇讨论：<a href="https://stackoverflow.com/questions/4802887/gson-how-to-exclude-specific-fields-from-serialization-without-annotations">Gson: How to exclude specific fields from Serialization without annotations</a>，在其中，程序员们合纵连横，推陈出新，是寻求问题最优解的现实典范。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2022/03/19/%e4%bd%bf%e7%94%a8-gson-%e5%ba%8f%e5%88%97%e5%8c%96%e5%ae%b9%e5%99%a8%e7%9a%84%e9%97%ae%e9%a2%98%e4%b8%8e%e8%a7%a3%e5%86%b3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Java 工程的预处理</title>
		<link>https://www.somedoc.net/2022/02/20/java-%e5%b7%a5%e7%a8%8b%e7%9a%84%e9%a2%84%e5%a4%84%e7%90%86/</link>
					<comments>https://www.somedoc.net/2022/02/20/java-%e5%b7%a5%e7%a8%8b%e7%9a%84%e9%a2%84%e5%a4%84%e7%90%86/#respond</comments>
		
		<dc:creator><![CDATA[张三太爷]]></dc:creator>
		<pubDate>Sun, 20 Feb 2022 05:21:11 +0000</pubDate>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[技术]]></category>
		<guid isPermaLink="false">https://www.somedoc.net/?p=4958</guid>

					<description><![CDATA[从语言源代码，到构建出最终的可执行体，在其间的一长串链式操作 <a href="https://www.somedoc.net/2022/02/20/java-%e5%b7%a5%e7%a8%8b%e7%9a%84%e9%a2%84%e5%a4%84%e7%90%86/" class="more-link">[&#8230;]</a>]]></description>
										<content:encoded><![CDATA[<p>从语言源代码，到构建出最终的可执行体，在其间的一长串链式操作环节中，语言有没有提供预处理的能力，有时候对工程化还是有比较大的影响的。</p>
<p>像 Java 这样的语言，诞生之时就携带着极大的野心，要为语言使用者摒绝一切差异，使得代码可以一次编写，处处运行，但显然在实际的使用场景下，已经遭受到了越来越大的挑战。无论是自己在早期有所意识后区分出来的 J2ME、J2SE 以及 J2EE 这样的不同环境，还是眼下标准的 Java 与 Android 平台的差异，都是明证。</p>
<p>前段时间写几个工具类的时候，遇到了这样的困境，而且就是发生在 Android 之中。在其 android support 库向 androidx 库迁移的过程中，你会发现有大量的同名类的 import 需要且仅仅需要换一个 package name。更换之后，也只是一种单向切换，并不能新老兼顾。于是尝试寻找有无现成的脚手架之类的可供使用。</p>
<p>在 Github 上找到几个，筛选后记录两个于此：<br />
&#8211; https://github.com/raydac/java-comment-preprocessor<br />
&#8211; https://github.com/dannyjiajia/gradle-java-preprocessor-plugin</p>
<p>第一个还活跃着，而且看上去功能较强；后一个止于两年前，如其名字，应该只是一个 gradle 的插件。后者的形态，也是我构思的如果找不到现成可用品的话，自己可以实现的最近的可能目标的形态。</p>
<p>在考察各个实现的过程中，发现相当大的比例都是使用在“//”注释符之后再写“#”预处理语句，这跟我的预期还是略有偏差的。既然文本文件处理可以逐行进行，而且预处理又是发生在编译之前，为什么不直接使用“#”开始呢？而且好多在预处理之后的源代码里保留了这些预处理/注释的痕迹。不能理解，只好姑且认为是一种惯例，或者 Java 文化，甚至是实现方式上可能存在什么约束或者局限。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.somedoc.net/2022/02/20/java-%e5%b7%a5%e7%a8%8b%e7%9a%84%e9%a2%84%e5%a4%84%e7%90%86/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
