netomi <a href="/about">Thomas</a> is a software engineer specialized in byte code engineering, scientific applications, simulations and anything that <a href="/projects">interests</a> him. https://netomi.github.io/ Wed, 06 Nov 2024 19:06:54 +0000 Wed, 06 Nov 2024 19:06:54 +0000 Jekyll v4.3.3 Automatically adding comments to GitHub PRs, safely and reliably <p>Using GitHub Actions offers a lot of possibilities to automate your development processes. Imagine you would like to get a coverage report for any PR that gets created for your repository and add a coverage summary directly to the PR as a comments to make allow reviewers to immediately see if this PR meets the requirements wrt code coverage.</p> <p>There are a lot of convenient actions available in the <a href="https://github.com/marketplace?type=actions">GitHub Marketplace</a> that perform some action and add a comment to the PR. That usually works perfectly unless you come across the notorious error:</p> <pre><code class="language-{verbatim}">HttpError: Resource not accessible by integration </code></pre> <p>and your perfectly working workflow failed to add a comment to the PR that you just created. After some initial investigation you find out that it is due to a PR created from a fork. Further digging reveals that GitHub treats PRs from forks differently for <a href="https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/">security reasons</a>, namely that in such case any workflow triggered by a <code class="language-plaintext highlighter-rouge">pull_request</code> trigger will not have access to any <a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflows-in-forked-repositories">secrets</a> or <a href="https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token">write tokens</a>.</p> <h3 id="trying-to-be-smart">Trying to be smart</h3> <p>GitHub takes “secure by default” seriously, so you don’t easily get <strong>pwned</strong> by a malicious actor, however there are still ways to shoot yourself in the foot. There is an additional workflow trigger <a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target">pull_request_target</a> that will run a workflow from a PR in the context of the target repository, and as a consequence, have access to secrets and write tokens.</p> <p>There are cases where doing something like that is acceptable and can be considered safe (see <a href="https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/">Keeping your GitHub Actions safe</a>):</p> <blockquote> <p>Generally speaking, when the PR contents are treated as passive data, i.e. not in a position of influence over the build/testing process, it is safe.</p> </blockquote> <p>A very nice writeup about utilizing the <code class="language-plaintext highlighter-rouge">pull_request_target</code> trigger with some additional user permission checks can be found in this <a href="https://michaelheap.com/access-secrets-from-forks/">blog entry</a>. However, the downside of that approach is that you need to think hard about the attack surface of your workflows to prevent any unexpected side effects, something we ultimately want to avoid.</p> <h3 id="git-sic-better">Git (sic!) better</h3> <p>Some investigation of available <a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows">workflow triggers</a> reveals the <code class="language-plaintext highlighter-rouge">workflow_run</code> trigger, that allows to react once a workflow has completed, and workflows triggered by that run in the context of the repository itself. That would allow us to do the following:</p> <ul> <li>run a workflow with <code class="language-plaintext highlighter-rouge">pull_request</code> trigger to perform some action, e.g. calculate the code coverage of the code in a PR</li> <li>store the result of this workflow as an artifact attached to the workflow run</li> <li>trigger another workflow upon completion of the first workflow using the <code class="language-plaintext highlighter-rouge">workflow_run</code> trigger with action <code class="language-plaintext highlighter-rouge">completed</code></li> <li>download the artifact attached to the completed workflow run</li> <li>add a comment to the associated PR based on the artifact data</li> </ul> <p>We have implemented this in a real-world example for the <a href="https://github.com/eclipse-uprotocol/up-java/">up-java repo</a> of the Eclipse uProtocol project.</p> <h3 id="gen-aimundane">Gen(-AI+Mundane)</h3> <p>The <a href="https://github.com/eclipse-uprotocol/up-java/blob/main/.github/workflows/coverage.yml">first workflow</a> will generate the relevant data for the PR, and upload the generated data as an artifact. See the snippet below for the relevant steps:</p> <figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">name</span><span class="pi">:</span> <span class="s">Java Test and Coverage</span> <span class="nn">...</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Generate coverage comment</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/github-script@v7</span> <span class="na">with</span><span class="pi">:</span> <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span> <span class="s">const fs = require('fs');</span> <span class="s">const COVERAGE_PERCENTAGE = `${{ env.COVERAGE_PERCENTAGE }}`;</span> <span class="s">const COVERAGE_REPORT_PATH = `https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/`;</span> <span class="no"> </span> <span class="s">fs.mkdirSync('./pr-comment', { recursive: true });</span> <span class="no"> </span> <span class="s">var pr_number = `${{ github.event.number }}`;</span> <span class="s">var body = `</span> <span class="s">Code coverage report is ready! :chart_with_upwards_trend:</span> <span class="no"> </span> <span class="s">- **Code Coverage Percentage:** ${COVERAGE_PERCENTAGE}%</span> <span class="s">- **Code Coverage Report:** [View Coverage Report](${COVERAGE_REPORT_PATH})</span> <span class="s">`;</span> <span class="no"> </span> <span class="s">fs.writeFileSync('./pr-comment/pr-number.txt', pr_number);</span> <span class="s">fs.writeFileSync('./pr-comment/body.txt', body);</span> <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3</span> <span class="c1"># v4.3.1</span> <span class="na">with</span><span class="pi">:</span> <span class="na">name</span><span class="pi">:</span> <span class="s">pr-comment</span> <span class="na">path</span><span class="pi">:</span> <span class="s">pr-comment/</span></code></pre></figure> <p>The comment that shall be added to the PR is stored in a file <strong>body.txt</strong> while the associated PR number is stored in a file <strong>pr-number.txt</strong> to know to which PR that comment should be added in the second workflow.</p> <h3 id="any-comment">Any comment?</h3> <p>Now that we have a workflow to calculate the code coverage of the PR and generate a comment that we would like to add to it, we just need a <a href="https://github.com/eclipse-uprotocol/up-java/blob/main/.github/workflows/coverage-comment-pr.yml">workflow</a> that gets triggered when the first workflow (filtered by its name) is completed:</p> <figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="nn">...</span> <span class="na">on</span><span class="pi">:</span> <span class="na">workflow_run</span><span class="pi">:</span> <span class="na">workflows</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">Java</span><span class="nv"> </span><span class="s">Test</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">Coverage"</span><span class="pi">]</span> <span class="na">types</span><span class="pi">:</span> <span class="pi">-</span> <span class="s">completed</span> <span class="na">jobs</span><span class="pi">:</span> <span class="na">add-coverage-comment</span><span class="pi">:</span> <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span> <span class="na">permissions</span><span class="pi">:</span> <span class="na">pull-requests</span><span class="pi">:</span> <span class="s">write</span> <span class="na">if</span><span class="pi">:</span> <span class="pi">&gt;</span> <span class="s">github.event.workflow_run.event == 'pull_request' &amp;&amp;</span> <span class="s">github.event.workflow_run.conclusion == 'success'</span> <span class="na">steps</span><span class="pi">:</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Download</span><span class="nv"> </span><span class="s">artifact'</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea</span> <span class="c1"># v7.0.1</span> <span class="na">with</span><span class="pi">:</span> <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span> <span class="s">var artifacts = await github.rest.actions.listWorkflowRunArtifacts({</span> <span class="s">owner: context.repo.owner,</span> <span class="s">repo: context.repo.repo,</span> <span class="s">run_id: ${{github.event.workflow_run.id }},</span> <span class="s">});</span> <span class="s">var matchArtifact = artifacts.data.artifacts.filter((artifact) =&gt; {</span> <span class="s">return artifact.name == "pr-comment"</span> <span class="s">})[0];</span> <span class="s">var download = await github.rest.actions.downloadArtifact({</span> <span class="s">owner: context.repo.owner,</span> <span class="s">repo: context.repo.repo,</span> <span class="s">artifact_id: matchArtifact.id,</span> <span class="s">archive_format: 'zip',</span> <span class="s">});</span> <span class="s">var fs = require('fs');</span> <span class="s">fs.writeFileSync('${{github.workspace}}/pr-comment.zip', Buffer.from(download.data));</span> <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">unzip pr-comment.zip</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Comment</span><span class="nv"> </span><span class="s">on</span><span class="nv"> </span><span class="s">PR'</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea</span> <span class="c1"># v7.0.1</span> <span class="na">with</span><span class="pi">:</span> <span class="na">github-token</span><span class="pi">:</span> <span class="s">${{ secrets.GITHUB_TOKEN }}</span> <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span> <span class="s">var fs = require('fs');</span> <span class="s">const issue_number = Number(fs.readFileSync('./pr-number.txt'));</span> <span class="s">const body = fs.readFileSync('./body.txt', { encoding: 'utf8', flag: 'r' });</span> <span class="s">await github.rest.issues.createComment({</span> <span class="s">owner: context.repo.owner,</span> <span class="s">repo: context.repo.repo,</span> <span class="s">issue_number: issue_number,</span> <span class="s">body: body</span> <span class="s">});</span></code></pre></figure> <p>The workflow will download the artifact attached to the workflow run that triggered it and add a comment to the associated PR with the attached content.</p> <blockquote> <p><strong><em>NOTE</em></strong>: There are pre-made <a href="https://github.com/marketplace?query=download+artifact+from+workflow+run">3rd party actions</a> to download artifacts from previous workflow runs that could be used to further simplify such a workflow.</p> </blockquote> <h3 id="closing-words">Closing words</h3> <p><a href="https://github.com">GitHub.com</a> is a very powerful platform to host open-source projects, but you need to be aware that its open and collaborative model also attracts malicious actors to attack or infiltrate (popular) repositories. Keep your repositories and workflows safe by following secure development best practices.</p> <p>More information can be found in the <a href="https://eclipse-csi.github.io/security-handbook/">Security Handbook @ Eclipse Foundation</a>.</p> Wed, 21 Aug 2024 18:00:00 +0000 https://netomi.github.io/eclipse/2024/08/21/adding-comments-to-pr.html https://netomi.github.io/eclipse/2024/08/21/adding-comments-to-pr.html github github-actions eclipse Controlling access to macOS large runners for GitHub Actions <p>In 2023, GitHub introduced new powerful <code class="language-plaintext highlighter-rouge">macOS runners</code> for GitHub Actions. These <a href="https://docs.github.com/en/actions/using-github-hosted-runners/about-larger-runners/running-jobs-on-larger-runners?platform=mac#available-macos-larger-runners">runners</a> have a considerable higher amount of processors / memory and disk space allocated to them to speed up the execution of workflows. This advantage comes at a cost though, as billing per minute of executed workflow time is considerably higher as compared to normal runners (see <a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions">billing for runners</a>), on top of the usual minute multiplier for macOS runners (each minute of executed workflow time on a macOS runner counts as 10 minutes for billing purposes).</p> <h3 id="going-to-speed-up">Going to speed up</h3> <p>In order to use such a <code class="language-plaintext highlighter-rouge">macOS large runner</code>, you can simply add a <code class="language-plaintext highlighter-rouge">runs-on: &lt;runner-type&gt;</code> to your job definition, e.g. using <code class="language-plaintext highlighter-rouge">macos-latest-large</code> as runner type:</p> <figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">name</span><span class="pi">:</span> <span class="s">learn-github-actions-testing</span> <span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span> <span class="na">jobs</span><span class="pi">:</span> <span class="na">build</span><span class="pi">:</span> <span class="na">runs-on</span><span class="pi">:</span> <span class="s">macos-latest-large</span> <span class="na">steps</span><span class="pi">:</span> <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build</span> <span class="na">run</span><span class="pi">:</span> <span class="s">swift build</span> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run tests</span> <span class="na">run</span><span class="pi">:</span> <span class="s">swift test</span></code></pre></figure> <p><br /> Additionally, your organization needs to have a <code class="language-plaintext highlighter-rouge">GitHub Team</code> or <code class="language-plaintext highlighter-rouge">GitHub Enterprise Cloud</code> plan to be able to use such a <code class="language-plaintext highlighter-rouge">macOS large runner</code> (which is now the case for all Eclipse projects hosted on GitHub as of 2024), otherwise workflows using such a runner will fail to run. Once your organization is eligible to use large runners, you probably want to control the access to such runners for the repositories in your organization to avoid surprises when you receive your next invoice. GitHub offers a convenient way to define <a href="https://docs.github.com/en/actions/using-github-hosted-runners/about-larger-runners/controlling-access-to-larger-runners">runner groups</a> to control which repositories can access such large runners.</p> <h3 id="what-the-">What the …</h3> <p>Unfortunately, such runner groups can only be defined for <code class="language-plaintext highlighter-rouge">linux</code> and <code class="language-plaintext highlighter-rouge">windows</code> runners, there is simply no way to prevent that <code class="language-plaintext highlighter-rouge">macOS large runners</code> are being used by any of your repositories once their use is configured in a workflow as described above. This poses a problem for non-profit organizations (like the <a href="https://www.eclipse.org">Eclipse Foundation</a>) that host a lot of projects and their associated repositories on GitHub as it might result in higher than expected billing expenses as some projects try using such large runners to speed up their workflows without realizing the consequences.</p> <p><br /> While it is possible to monitor the incurred costs of using GitHub Action minutes, this is a tedious and manual task and requires communication with projects to change their workflows if occurrences have been identified.</p> <h3 id="gaming-the-system">Gaming the system</h3> <p>The idea was born to add some automation to prevent the execution of workflows on such <code class="language-plaintext highlighter-rouge">macOS large runners</code> unless the project / repository is entitled to use such a runner.</p> <p><br /> After studying the available <a href="https://docs.github.com/en/rest?apiVersion=2022-11-28">GitHub Rest API</a> and preliminary testing, we figured out the following logic reliably prevents the execution of workflows on <code class="language-plaintext highlighter-rouge">macOS large runners</code>:</p> <ul> <li>listen to <a href="https://docs.github.com/en/webhooks/webhook-events-and-payloads?actionType=queued#workflow_job">workflow_job events</a> with action <code class="language-plaintext highlighter-rouge">queued</code></li> <li>check whether the included <code class="language-plaintext highlighter-rouge">workflow_job</code> object has <code class="language-plaintext highlighter-rouge">labels</code> that indicate that the job is supposed to run on a <code class="language-plaintext highlighter-rouge">macOS large runner</code></li> <li>if the above evaluates to true and the repository is not eligible to use such a runner, <a href="https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#cancel-a-workflow-run">cancel the workflow_run</a></li> </ul> <p><br /> To receive the necessary webhook events from GitHub in case a workflow is being queued to run, you have to set up an organization or repository webhook, listen for the event and apply the logic.</p> <h3 id="all-good">All good</h3> <p>At the <a href="https://www.eclipse.org">Eclipse Foundation</a> we are operating an open-source project called <a href="https://github.com/eclipse-csi/otterdog">Otterdog</a> in order to configure our numerous organizations and repositories hosted on GitHub at scale. This tool is effectively a GitHub App and is installed for all our projects / organizations on GitHub and already can listen to various events sent from GitHub. So naturally we added the above logic to this tool and allowed to define which organizations are allowed to use such large runners via a configuration file (see <a href="https://github.com/eclipse-tractusx/.eclipsefdn/blob/main/otterdog/policies/macos_large_runners.yml">this</a> example).</p> <p><br /> This allows us to control the use of <code class="language-plaintext highlighter-rouge">macOS large runners</code> which unfortunately is not yet possible through any of the administration consoles at GitHub. On the other hand, our implemented workaround showcases the power of GitHub Apps on how you can utilize them to adjust your GitHub experience to your organizational needs.</p> <p><br /> If you are member of an Eclipse project and would like to utilize macOS large runners for your workflows, reach out to us via the <a href="https://gitlab.eclipse.org/eclipsefdn/helpdesk/">HelpDesk</a>.</p> Tue, 06 Aug 2024 10:00:00 +0000 https://netomi.github.io/eclipse/2024/08/06/macos-large-runners.html https://netomi.github.io/eclipse/2024/08/06/macos-large-runners.html github github-actions eclipse Bat - library to process and analyze byte code <p>The last couple of years I was busy with processing byte code with an amazing tool called ProGuard / DexGuard. After deciding to step back and take a break, I used the available spare time to work on my own tool to process any kind of byte code (class file, dex file) and perform some useful analysis on them.</p> <p>You can access my initial take on this in my GitHub repo <a href="https://github.com/netomi/bat">bat</a> which is an acronym for <em>byte code analysis toolkit</em>.</p> <p>The first useful utility that I created is a tool that copies the behavior of <em>dexdump</em> with some additional options, e.g. ability to filter the output for specific classes only:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="n">java</span> <span class="o">-</span><span class="n">cp</span> <span class="o">/</span><span class="n">path</span><span class="o">/</span><span class="n">to</span><span class="o">/</span><span class="n">commands</span><span class="o">-</span><span class="mf">1.0</span><span class="o">-</span><span class="no">SNAPSHOT</span><span class="o">.</span><span class="na">jar</span> <span class="n">com</span><span class="o">.</span><span class="na">github</span><span class="o">.</span><span class="na">netomi</span><span class="o">.</span><span class="na">bat</span><span class="o">.</span><span class="na">DexDumpCommand</span> <span class="o">-</span><span class="n">c</span> <span class="err">'</span><span class="o">**</span><span class="nc">Hello</span><span class="o">**</span><span class="err">'</span> <span class="n">classes</span><span class="o">.</span><span class="na">dex</span> <span class="o">...</span> <span class="nc">Class</span> <span class="err">#</span><span class="mi">0</span> <span class="nl">header:</span> <span class="n">class_idx</span> <span class="o">:</span> <span class="mi">10</span> <span class="n">access_flags</span> <span class="o">:</span> <span class="mi">1</span> <span class="o">(</span><span class="mh">0x0001</span><span class="o">)</span> <span class="n">superclass_idx</span> <span class="o">:</span> <span class="mi">2</span> <span class="n">interfaces_off</span> <span class="o">:</span> <span class="mi">0</span> <span class="o">(</span><span class="mh">0x0000</span><span class="o">)</span> <span class="n">source_file_idx</span> <span class="o">:</span> <span class="mi">0</span> <span class="n">annotations_off</span> <span class="o">:</span> <span class="mi">0</span> <span class="o">(</span><span class="mh">0x0000</span><span class="o">)</span> <span class="n">class_data_off</span> <span class="o">:</span> <span class="mi">2209</span> <span class="o">(</span><span class="mh">0x08a1</span><span class="o">)</span> <span class="n">static_fields_size</span> <span class="o">:</span> <span class="mi">3</span> <span class="nl">instance_fields_size:</span> <span class="mi">0</span> <span class="n">direct_methods_size</span> <span class="o">:</span> <span class="mi">3</span> <span class="nl">virtual_methods_size:</span> <span class="mi">1</span> <span class="nc">Class</span> <span class="err">#</span><span class="mi">0</span> <span class="nc">Class</span> <span class="n">descriptor</span> <span class="o">:</span> <span class="err">'</span><span class="nc">Lcom</span><span class="o">/</span><span class="n">example</span><span class="o">/</span><span class="nc">HelloWorldActivity</span><span class="o">;</span><span class="err">'</span> <span class="nc">Access</span> <span class="n">flags</span> <span class="o">:</span> <span class="mh">0x0001</span> <span class="o">(</span><span class="no">PUBLIC</span><span class="o">)</span> <span class="nc">Superclass</span> <span class="o">:</span> <span class="err">'</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">app</span><span class="o">/</span><span class="nc">Activity</span><span class="o">;</span><span class="err">'</span> <span class="nc">Interfaces</span> <span class="o">-</span> <span class="nc">Static</span> <span class="n">fields</span> <span class="o">-</span> <span class="o">...</span></code></pre></figure> <p>The next thing to work on will be an <em>explainer</em> tool, which is intended as a utility to help with the shrinking capability of tools like ProGuard or R8. In general these tools work amazingly well, but it is quite difficult to understand why something is actually kept from being shrunk or which specific rule is responsible for that. The <em>explainer</em> will trace the exact reason why a certain class / method / field is being kept. Something that might also be very useful is the other way around: what items will be kept because of a specific method or class. A tool like that might help to reduce dependencies or complexity in the code.</p> <p>Feel free to leave comments on other useful things that you would like to see in the near future.</p> Mon, 20 Jul 2020 13:00:00 +0000 https://netomi.github.io/2020/07/20/bat.html https://netomi.github.io/2020/07/20/bat.html byte-code-engineering dex Backporting lambda expressions <p>Let’s take a look into lambda expressions as part of the Java language specifiction, what they are, how they are represented, and how existing bytecode can be backported to earlier class files versions without losing any functionality.</p> <h1 id="introduction">Introduction</h1> <p>A lambda expressions is basically syntactic sugar that allows you to define inline anonymous classes that implement specific interfaces. Take a look at the following example:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="o">...</span> <span class="nc">Button</span> <span class="n">button</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Button</span><span class="o">)</span> <span class="n">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">button</span><span class="o">);</span> <span class="n">button</span><span class="o">.</span><span class="na">setOnClickListener</span><span class="o">((</span><span class="n">view</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="s">"Hello World!"</span><span class="o">,</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_LONG</span><span class="o">).</span><span class="na">show</span><span class="o">());</span> <span class="o">...</span> </code></pre></figure> <p>In this example we set a click listener for a specific button using a lambda expression. This is equivalent to writing this code:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="o">...</span> <span class="nc">Button</span> <span class="n">button</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Button</span><span class="o">)</span> <span class="n">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">button</span><span class="o">);</span> <span class="n">button</span><span class="o">.</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="nc">OnClickListener</span><span class="o">()</span> <span class="o">{</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onClick</span><span class="o">(</span><span class="nc">View</span> <span class="n">view</span><span class="o">)</span> <span class="o">{</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="s">"Hello World!"</span><span class="o">,</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_LONG</span><span class="o">).</span><span class="na">show</span><span class="o">();</span> <span class="o">}</span> <span class="o">});</span> <span class="o">...</span> </code></pre></figure> <p>The use of lambda expressions makes the whole code a bit more concise and less bloated. I dont want to go into more details on how to write lambda expressions in the java language. There are many resources out there for this purpose. In this blog post I would like to focus on how such lambda expressions are represented in byte code, and how it can be backported to earlier class file versions, which is important when targeting platforms like Android.</p> <h1 id="representation">Representation</h1> <p>Let’s analyse how the <em>javac</em> compiler translates this java code into the corresponding class file format:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onCreate</span><span class="o">(</span><span class="n">android</span><span class="o">.</span><span class="na">os</span><span class="o">.</span><span class="na">Bundle</span><span class="o">);</span> <span class="nl">descriptor:</span> <span class="o">(</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">os</span><span class="o">/</span><span class="nc">Bundle</span><span class="o">;)</span><span class="no">V</span> <span class="nl">flags:</span> <span class="o">(</span><span class="mh">0x0001</span><span class="o">)</span> <span class="no">ACC_PUBLIC</span> <span class="nl">Code:</span> <span class="o">...</span> <span class="mi">23</span><span class="o">:</span> <span class="n">invokedynamic</span> <span class="err">#</span><span class="mi">29</span><span class="o">,</span> <span class="mi">0</span> <span class="c1">// InvokeDynamic #0:onClick:(Lxxx/yyy/HelloWorldActivity;)Landroid/view/View$OnClickListener;</span> <span class="mi">28</span><span class="o">:</span> <span class="n">invokevirtual</span> <span class="err">#</span><span class="mi">33</span> <span class="c1">// Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V</span> <span class="o">...</span></code></pre></figure> <p>We can see that an instruction of type <em>invokedynamic</em> is executed which returns an object of type <em>OnClickListener</em> and passes this on to the <em>setOnClickListener</em> method of the <em>Button</em> object. The <em>invokedynamic</em> instruction calls the <em>BootstrapMethod</em> #0 as defined in the class:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nl">BootstrapMethods:</span> <span class="mi">0</span><span class="o">:</span> <span class="err">#</span><span class="mi">71</span> <span class="n">REF_invokeStatic</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="n">invoke</span><span class="o">/</span><span class="nc">LambdaMetafactory</span><span class="o">.</span><span class="na">metafactory</span><span class="o">:(</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="n">invoke</span><span class="o">/</span><span class="nc">MethodHandles</span><span class="n">$Lookup</span><span class="o">;</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">String</span><span class="o">;</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="n">invoke</span><span class="o">/</span><span class="nc">MethodType</span><span class="o">;</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="n">invoke</span><span class="o">/</span><span class="nc">MethodType</span><span class="o">;</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="n">invoke</span><span class="o">/</span><span class="nc">MethodHandle</span><span class="o">;</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="n">invoke</span><span class="o">/</span><span class="nc">MethodType</span><span class="o">;)</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="n">invoke</span><span class="o">/</span><span class="nc">CallSite</span><span class="o">;</span> <span class="nc">Method</span> <span class="nl">arguments:</span> <span class="err">#</span><span class="mi">78</span> <span class="o">(</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">view</span><span class="o">/</span><span class="nc">View</span><span class="o">;)</span><span class="no">V</span> <span class="err">#</span><span class="mi">79</span> <span class="n">REF_invokeSpecial</span> <span class="n">xxx</span><span class="o">/</span><span class="n">yyy</span><span class="o">/</span><span class="nc">HelloWorldActivity</span><span class="o">.</span><span class="na">lambda</span><span class="n">$onCreate</span><span class="err">$</span><span class="mi">0</span><span class="o">:(</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">view</span><span class="o">/</span><span class="nc">View</span><span class="o">;)</span><span class="no">V</span> <span class="err">#</span><span class="mi">78</span> <span class="o">(</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">view</span><span class="o">/</span><span class="nc">View</span><span class="o">;)</span><span class="no">V</span></code></pre></figure> <p>The definition of this <em>BootstrapMethod</em> contains information about the actual method to be called, its parameter and return types. The JVM will then dynamically create at runtime a <em>CallSite</em> targeting the specified target method and execute it.</p> <p>In our case the target method is also stored in the class file itself as private method (with the name as specified in the BootstrapMethod):</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"> <span class="kd">private</span> <span class="kt">void</span> <span class="n">lambda$onCreate</span><span class="err">$</span><span class="mi">0</span><span class="o">(</span><span class="n">android</span><span class="o">.</span><span class="na">view</span><span class="o">.</span><span class="na">View</span><span class="o">);</span> <span class="nl">descriptor:</span> <span class="o">(</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">view</span><span class="o">/</span><span class="nc">View</span><span class="o">;)</span><span class="no">V</span> <span class="nl">flags:</span> <span class="o">(</span><span class="mh">0x1002</span><span class="o">)</span> <span class="no">ACC_PRIVATE</span><span class="o">,</span> <span class="no">ACC_SYNTHETIC</span> <span class="nl">Code:</span> <span class="o">...</span> <span class="n">aload_0</span> <span class="n">ldc</span> <span class="err">#</span><span class="mi">10</span> <span class="c1">// String: "Hello World!"</span> <span class="n">iconst_1</span> <span class="n">invokestatic</span> <span class="err">#</span><span class="mi">41</span> <span class="c1">// Method android/widget/Toast.makeText:(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;</span> <span class="n">invokevirtual</span> <span class="err">#</span><span class="mi">45</span> <span class="c1">// Method android/widget/Toast.show:()V</span> <span class="k">return</span></code></pre></figure> <p>Now this is the most simple example of a lambda expression. There are different cases to consider (e.g. capturing or non-capturing, references to constructors, …). I don’t want to go into too much detail here, lets dive into ways to backport these constructs in a way that they can be represented in earlier class file versions.</p> <h1 id="backporting">Backporting</h1> <p>There is a very popular and amazing tool called <a href="https://github.com/luontola/retrolambda">retrolambda</a> which does exactly that, backport any lambda expression to java version 5, 6 or 7. For various technical reasons, I had the need to implement such a mechanism myself as the product I have been working on (<a href="https://github.com/Guardsquare/proguard">ProGuard / DexGuard</a>) also had to work in full standalone mode and is generally self-contained, thus has no external dependencies.</p> <p>So the basic idea is to extract the generated private method into a separate lambda class that implements the necessary interface(s). Then replace the <em>invokedynamic</em> instruction calls in a way to create an instance of this lambda class.</p> <p>The code to perform this is open-source and accessible at the <a href="https://github.com/Guardsquare/proguard/tree/master/base/src/proguard/backport">ProGuard github repo</a>.</p> <p>Applied on the above example, the result would look like in the snippets below. An additional accessor method has been added to avoid modifying the visibility of the lambda methods, but that is not a technical necessity its rather a design choice.</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"> <span class="kd">static</span> <span class="kt">void</span> <span class="n">accessor$HelloWorldActivity$lambda0</span><span class="o">(</span><span class="n">xxx</span><span class="o">.</span><span class="na">yyy</span><span class="o">.</span><span class="na">HelloWorldActivity</span><span class="o">,</span> <span class="n">android</span><span class="o">.</span><span class="na">view</span><span class="o">.</span><span class="na">View</span><span class="o">);</span> <span class="nl">descriptor:</span> <span class="o">(</span><span class="nc">Lxxx</span><span class="o">/</span><span class="n">yyy</span><span class="o">/</span><span class="nc">HelloWorldActivity</span><span class="o">;</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">view</span><span class="o">/</span><span class="nc">View</span><span class="o">;)</span><span class="no">V</span> <span class="nl">flags:</span> <span class="o">(</span><span class="mh">0x1008</span><span class="o">)</span> <span class="no">ACC_STATIC</span><span class="o">,</span> <span class="no">ACC_SYNTHETIC</span> <span class="nl">Code:</span> <span class="n">stack</span><span class="o">=</span><span class="mi">2</span><span class="o">,</span> <span class="n">locals</span><span class="o">=</span><span class="mi">2</span><span class="o">,</span> <span class="n">args_size</span><span class="o">=</span><span class="mi">2</span> <span class="n">aload_0</span> <span class="n">aload_1</span> <span class="n">invokespecial</span> <span class="err">#</span><span class="mi">15</span> <span class="c1">// Method lambda$onCreate$0:(Landroid/view/View;)V</span> <span class="k">return</span> <span class="kd">private</span> <span class="kt">void</span> <span class="n">lambda$onCreate</span><span class="err">$</span><span class="mi">0</span><span class="o">(</span><span class="n">android</span><span class="o">.</span><span class="na">view</span><span class="o">.</span><span class="na">View</span><span class="o">);</span> <span class="nl">descriptor:</span> <span class="o">(</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">view</span><span class="o">/</span><span class="nc">View</span><span class="o">;)</span><span class="no">V</span> <span class="nl">flags:</span> <span class="o">(</span><span class="mh">0x1002</span><span class="o">)</span> <span class="no">ACC_PRIVATE</span><span class="o">,</span> <span class="no">ACC_SYNTHETIC</span> <span class="nl">Code:</span> <span class="n">stack</span><span class="o">=</span><span class="mi">3</span><span class="o">,</span> <span class="n">locals</span><span class="o">=</span><span class="mi">2</span><span class="o">,</span> <span class="n">args_size</span><span class="o">=</span><span class="mi">2</span> <span class="n">aload_0</span> <span class="n">ldc</span> <span class="err">#</span><span class="mi">10</span> <span class="c1">// String: "Hello World!"</span> <span class="n">iconst_1</span> <span class="n">invokestatic</span> <span class="err">#</span><span class="mi">28</span> <span class="c1">// Method android/widget/Toast.makeText:(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;</span> <span class="n">invokevirtual</span> <span class="err">#</span><span class="mi">31</span> <span class="c1">// Method android/widget/Toast.show:()V</span> <span class="k">return</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onCreate</span><span class="o">(</span><span class="n">android</span><span class="o">.</span><span class="na">os</span><span class="o">.</span><span class="na">Bundle</span><span class="o">);</span> <span class="nl">descriptor:</span> <span class="o">(</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">os</span><span class="o">/</span><span class="nc">Bundle</span><span class="o">;)</span><span class="no">V</span> <span class="nl">flags:</span> <span class="o">(</span><span class="mh">0x0001</span><span class="o">)</span> <span class="no">ACC_PUBLIC</span> <span class="nl">Code:</span> <span class="n">stack</span><span class="o">=</span><span class="mi">4</span><span class="o">,</span> <span class="n">locals</span><span class="o">=</span><span class="mi">2</span><span class="o">,</span> <span class="n">args_size</span><span class="o">=</span><span class="mi">2</span> <span class="mi">0</span><span class="o">:</span> <span class="n">aload_0</span> <span class="mi">1</span><span class="o">:</span> <span class="n">aload_1</span> <span class="mi">2</span><span class="o">:</span> <span class="n">invokespecial</span> <span class="err">#</span><span class="mi">35</span> <span class="c1">// Method android/app/Activity.onCreate:(Landroid/os/Bundle;)V</span> <span class="mi">5</span><span class="o">:</span> <span class="n">aload_0</span> <span class="mi">6</span><span class="o">:</span> <span class="n">ldc</span> <span class="err">#</span><span class="mi">36</span> <span class="c1">// int 2130968576</span> <span class="mi">8</span><span class="o">:</span> <span class="n">invokevirtual</span> <span class="err">#</span><span class="mi">40</span> <span class="c1">// Method setContentView:(I)V</span> <span class="mi">11</span><span class="o">:</span> <span class="n">aload_0</span> <span class="mi">12</span><span class="o">:</span> <span class="n">ldc</span> <span class="err">#</span><span class="mi">41</span> <span class="c1">// int 2130903040</span> <span class="mi">14</span><span class="o">:</span> <span class="n">invokevirtual</span> <span class="err">#</span><span class="mi">45</span> <span class="c1">// Method findViewById:(I)Landroid/view/View;</span> <span class="mi">17</span><span class="o">:</span> <span class="n">checkcast</span> <span class="err">#</span><span class="mi">47</span> <span class="c1">// class android/widget/Button</span> <span class="mi">20</span><span class="o">:</span> <span class="k">new</span> <span class="err">#</span><span class="mi">49</span> <span class="c1">// class xxx/yyy/HelloWorldActivity$$Lambda$0</span> <span class="mi">23</span><span class="o">:</span> <span class="n">dup</span> <span class="mi">24</span><span class="o">:</span> <span class="n">aload_0</span> <span class="mi">25</span><span class="o">:</span> <span class="n">invokespecial</span> <span class="err">#</span><span class="mi">52</span> <span class="c1">// Method xxx/yyy/HelloWorldActivity$$Lambda$0."&lt;init&gt;":(Lxxx/yyy/HelloWorldActivity;)V</span> <span class="mi">28</span><span class="o">:</span> <span class="n">invokevirtual</span> <span class="err">#</span><span class="mi">56</span> <span class="c1">// Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V</span></code></pre></figure> <p>and the corresponding generated lambda class:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"> <span class="kd">private</span> <span class="kd">final</span> <span class="n">xxx</span><span class="o">.</span><span class="na">yyy</span><span class="o">.</span><span class="na">HelloWorldActivity</span> <span class="n">arg</span><span class="err">$</span><span class="mi">0</span><span class="o">;</span> <span class="nl">descriptor:</span> <span class="nc">Lxxx</span><span class="o">/</span><span class="n">yyy</span><span class="o">/</span><span class="nc">HelloWorldActivity</span><span class="o">;</span> <span class="nl">flags:</span> <span class="o">(</span><span class="mh">0x0012</span><span class="o">)</span> <span class="no">ACC_PRIVATE</span><span class="o">,</span> <span class="no">ACC_FINAL</span> <span class="kd">public</span> <span class="n">xxx</span><span class="o">.</span><span class="na">yyy</span><span class="o">.</span><span class="na">HelloWorldActivity</span><span class="err">$</span><span class="n">$Lambda</span><span class="err">$</span><span class="mi">0</span><span class="o">(</span><span class="n">xxx</span><span class="o">.</span><span class="na">yyy</span><span class="o">.</span><span class="na">HelloWorldActivity</span><span class="o">);</span> <span class="nl">descriptor:</span> <span class="o">(</span><span class="nc">Lxxx</span><span class="o">/</span><span class="n">yyy</span><span class="o">/</span><span class="nc">HelloWorldActivity</span><span class="o">;)</span><span class="no">V</span> <span class="nl">flags:</span> <span class="o">(</span><span class="mh">0x0001</span><span class="o">)</span> <span class="no">ACC_PUBLIC</span> <span class="nl">Code:</span> <span class="n">stack</span><span class="o">=</span><span class="mi">2</span><span class="o">,</span> <span class="n">locals</span><span class="o">=</span><span class="mi">2</span><span class="o">,</span> <span class="n">args_size</span><span class="o">=</span><span class="mi">2</span> <span class="mi">0</span><span class="o">:</span> <span class="n">aload_0</span> <span class="mi">1</span><span class="o">:</span> <span class="n">invokespecial</span> <span class="err">#</span><span class="mi">13</span> <span class="c1">// Method java/lang/Object."&lt;init&gt;":()V</span> <span class="mi">4</span><span class="o">:</span> <span class="n">aload_0</span> <span class="mi">5</span><span class="o">:</span> <span class="n">aload_1</span> <span class="mi">6</span><span class="o">:</span> <span class="n">putfield</span> <span class="err">#</span><span class="mi">15</span> <span class="c1">// Field arg$0:Lxxx/yyy/HelloWorldActivity;</span> <span class="mi">9</span><span class="o">:</span> <span class="k">return</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onClick</span><span class="o">(</span><span class="n">android</span><span class="o">.</span><span class="na">view</span><span class="o">.</span><span class="na">View</span><span class="o">);</span> <span class="nl">descriptor:</span> <span class="o">(</span><span class="nc">Landroid</span><span class="o">/</span><span class="n">view</span><span class="o">/</span><span class="nc">View</span><span class="o">;)</span><span class="no">V</span> <span class="nl">flags:</span> <span class="o">(</span><span class="mh">0x0001</span><span class="o">)</span> <span class="no">ACC_PUBLIC</span> <span class="nl">Code:</span> <span class="n">stack</span><span class="o">=</span><span class="mi">2</span><span class="o">,</span> <span class="n">locals</span><span class="o">=</span><span class="mi">2</span><span class="o">,</span> <span class="n">args_size</span><span class="o">=</span><span class="mi">2</span> <span class="mi">0</span><span class="o">:</span> <span class="n">aload_0</span> <span class="mi">1</span><span class="o">:</span> <span class="n">getfield</span> <span class="err">#</span><span class="mi">15</span> <span class="c1">// Field arg$0:Lxxx/yyy/HelloWorldActivity;</span> <span class="mi">4</span><span class="o">:</span> <span class="n">aload_1</span> <span class="mi">5</span><span class="o">:</span> <span class="n">invokestatic</span> <span class="err">#</span><span class="mi">24</span> <span class="c1">// Method xxx/yyy/HelloWorldActivity.accessor$HelloWorldActivity$lambda0:(Lxxx/yyy/HelloWorldActivity;Landroid/view/View;)V</span> <span class="mi">8</span><span class="o">:</span> <span class="k">return</span></code></pre></figure> <p>Of course, to fully support all possible variants of lambda expressions (and its sibling method references) some effort has to be put forward, but the necessary code is quite limited, take a look at the referenced source code, especially the class <em>LambdaExpressionConverter</em>. ProGuard offers a lot of functionality to make any kind of byte code engineering quite simple and effective.</p> <h1 id="addendum">Addendum</h1> <p>In this blog post I am focusing on lambda expressions, but ProGuard is capable of backporting various other language features of Java, like static interface methods, default methods, try-with-resources, string concatenations introduced in Java 9 and even the use of Java 8 APIs like the popular stream and time APIs.</p> <p>Take a look at the source code, its very clean and can easily be read and understood imho.</p> Tue, 07 Jul 2020 12:00:00 +0000 https://netomi.github.io/2020/07/07/backporting-lambda-expressions.html https://netomi.github.io/2020/07/07/backporting-lambda-expressions.html java byte-code-engineering lambda-expressions Analysing string encryption of stringer <p><a href="https://jfxstore.com/stringer/">Stringer</a> is one of many commercial tools to apply obfuscation on java byte code level. It supports various obfuscation techniques, but in this blog post I would like to analyse its ability to encrypt strings and explore ways to automatically deobfuscate jars obfuscated by <strong>stringer</strong>.</p> <p>Let’s take a look at how the actual string encryption applied by <strong>stringer</strong> looks like. The following snippets are disassembled bytecode in <strong>jasmin</strong> notation:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="n">ldc</span> <span class="s">"\u5d5a\ub1c7\u0712"</span> <span class="n">invokestatic</span> <span class="n">xxx</span><span class="o">/</span><span class="n">yy</span><span class="o">/</span><span class="n">z</span><span class="o">(</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">Object</span><span class="o">;)</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">String</span><span class="o">;</span></code></pre></figure> <p>As you can see, an encrypted string, is pushed onto the stack and a method with a revealing signature is called, returning the decrypted string a runtime. My first attempt is to call the decrypt method myself with the same argument in the hope that it returns the decrypted string.</p> <p>Unfortunately, this does not lead to a properly decrypted string, so there must be some additional mechanisms in place in order to prevent code lifting and to easily decrypt encrypted strings inside the jar file.</p> <p>Looking at the decrypt method (<em>xxx/yy/z</em>), I can see the following instructions:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="n">invokestatic</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">Thread</span><span class="o">/</span><span class="n">currentThread</span><span class="o">()</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">Thread</span><span class="o">;</span> <span class="n">invokevirtual</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">Thread</span><span class="o">/</span><span class="n">getStackTrace</span><span class="o">()[</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">StackTraceElement</span><span class="o">;</span> <span class="o">...</span> <span class="n">invokestatic</span> <span class="n">sun</span><span class="o">/</span><span class="n">misc</span><span class="o">/</span><span class="nc">SharedSecrets</span><span class="o">/</span><span class="n">getJavaLangAccess</span><span class="o">()</span><span class="nc">Lsun</span><span class="o">/</span><span class="n">misc</span><span class="o">/</span><span class="nc">JavaLangAccess</span><span class="o">;</span> <span class="n">aload</span> <span class="mi">1</span> <span class="n">iconst_2</span> <span class="n">aaload</span> <span class="n">invokevirtual</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">StackTraceElement</span><span class="o">/</span><span class="n">getClassName</span><span class="o">()</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">String</span><span class="o">;</span> <span class="n">invokestatic</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">Class</span><span class="o">/</span><span class="n">forName</span><span class="o">(</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">String</span><span class="o">;)</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">Class</span><span class="o">;</span> <span class="n">invokeinterface</span> <span class="n">sun</span><span class="o">/</span><span class="n">misc</span><span class="o">/</span><span class="nc">JavaLangAccess</span><span class="o">/</span><span class="n">getConstantPool</span><span class="o">(</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">Class</span><span class="o">;)</span><span class="nc">Lsun</span><span class="o">/</span><span class="n">reflect</span><span class="o">/</span><span class="nc">ConstantPool</span><span class="o">;</span> <span class="mi">1</span> <span class="n">invokevirtual</span> <span class="n">sun</span><span class="o">/</span><span class="n">reflect</span><span class="o">/</span><span class="nc">ConstantPool</span><span class="o">/</span><span class="n">getSize</span><span class="o">()</span><span class="no">I</span> <span class="o">...</span> <span class="n">invokevirtual</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">StackTraceElement</span><span class="o">/</span><span class="n">getClassName</span><span class="o">()</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">String</span><span class="o">;</span> <span class="n">invokevirtual</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">StringBuilder</span><span class="o">/</span><span class="n">append</span><span class="o">(</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">String</span><span class="o">;)</span><span class="nc">Ljava</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">StringBuilder</span><span class="o">;</span> <span class="o">...</span> <span class="n">invokevirtual</span> <span class="n">java</span><span class="o">/</span><span class="n">lang</span><span class="o">/</span><span class="nc">String</span><span class="o">/</span><span class="n">hashCode</span><span class="o">()</span><span class="no">I</span></code></pre></figure> <p>It looks like the decrypt method analyses the calling stack and uses information from this class/method to setup its decryption key. Translating the same snippet to java code makes it quite clear:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nc">StringBuilder</span> <span class="n">sb</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StringBuilder</span><span class="o">();</span> <span class="nc">StackTraceElement</span><span class="o">[]</span> <span class="n">stackTraceElements</span> <span class="o">=</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getStackTrace</span><span class="o">();</span> <span class="nc">JavaLangAccess</span> <span class="n">javaLangAccess</span> <span class="o">=</span> <span class="n">sun</span><span class="o">.</span><span class="na">misc</span><span class="o">.</span><span class="na">SharedSecrets</span><span class="o">.</span><span class="na">getJavaLangAccess</span><span class="o">();</span> <span class="kt">int</span> <span class="n">constantPoolSize</span> <span class="o">=</span> <span class="n">javaLangAccess</span><span class="o">.</span><span class="na">getConstantPool</span><span class="o">(</span><span class="nc">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="n">stackTraceElements</span><span class="o">[</span><span class="mi">2</span><span class="o">].</span><span class="na">getClassName</span><span class="o">())).</span><span class="na">getSize</span><span class="o">();</span> <span class="n">sb</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">constantPoolSize</span><span class="o">);</span> <span class="o">...</span> <span class="n">sb</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">stackTraceElements</span><span class="o">[</span><span class="mi">2</span><span class="o">].</span><span class="na">getClassName</span><span class="o">());</span> <span class="o">...</span> <span class="n">sb</span><span class="o">.</span><span class="na">append</span><span class="o">(</span><span class="n">stackTraceElements</span><span class="o">[</span><span class="mi">2</span><span class="o">].</span><span class="na">getMethodName</span><span class="o">());</span> <span class="o">...</span> <span class="kt">int</span> <span class="n">key</span> <span class="o">=</span> <span class="n">sb</span><span class="o">.</span><span class="na">hashCode</span><span class="o">();</span> <span class="o">...</span></code></pre></figure> <p>So various information from the calling class/method is being used to generate a key that is needed for the actual decryption. If the key does not match the expected values, only garbage is returned, making code lifting more challenging.</p> <p>Let’s try to use <a href="https://github.com/Guardsquare/proguard-core">proguard-core</a> to modify the byte code in a way that we can easily execute it and get the decrypted strings during static analysis. For readability I will not post the full source code, but the relevant pieces of code to understand how this can be done.</p> <p>We know that the decrypt method deduces its decryption key from the calling method. Now lets try to find all places in the jar file that seems to decrypt a string:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">DECRYPT_METHOD_TYPE</span> <span class="o">=</span> <span class="s">"(Ljava/lang/Object;)Ljava/lang/String;"</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Constant</span><span class="o">[]</span> <span class="no">CONSTANTS</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Constant</span><span class="o">[]</span> <span class="o">{</span> <span class="k">new</span> <span class="nf">MethodrefConstant</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">),</span> <span class="k">new</span> <span class="nf">ClassConstant</span><span class="o">(</span><span class="no">X</span><span class="o">,</span> <span class="kc">null</span><span class="o">),</span> <span class="k">new</span> <span class="nf">NameAndTypeConstant</span><span class="o">(</span><span class="no">Y</span><span class="o">,</span> <span class="mi">3</span><span class="o">),</span> <span class="k">new</span> <span class="nf">Utf8Constant</span><span class="o">(</span><span class="no">DECRYPT_METHOD_TYPE</span><span class="o">),</span> <span class="o">};</span> <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Instruction</span><span class="o">[]</span> <span class="no">INSTRUCTIONS</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Instruction</span><span class="o">[]</span> <span class="o">{</span> <span class="k">new</span> <span class="nf">ConstantInstruction</span><span class="o">(</span><span class="nc">Instruction</span><span class="o">.</span><span class="na">OP_LDC</span><span class="o">,</span> <span class="no">Z</span><span class="o">),</span> <span class="k">new</span> <span class="nf">ConstantInstruction</span><span class="o">(</span><span class="nc">Instruction</span><span class="o">.</span><span class="na">OP_INVOKESTATIC</span><span class="o">,</span> <span class="mi">0</span><span class="o">),</span> <span class="o">};</span> <span class="kd">private</span> <span class="kd">final</span> <span class="nc">InstructionSequenceMatcher</span> <span class="n">matcher</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">InstructionSequenceMatcher</span><span class="o">(</span><span class="no">CONSTANTS</span><span class="o">,</span> <span class="no">INSTRUCTIONS</span><span class="o">);</span> <span class="o">...</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">visitAnyInstruction</span><span class="o">(</span><span class="nc">Clazz</span> <span class="n">clazz</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">CodeAttribute</span> <span class="n">codeAttribute</span><span class="o">,</span> <span class="kt">int</span> <span class="n">offset</span><span class="o">,</span> <span class="nc">Instruction</span> <span class="n">instruction</span><span class="o">)</span> <span class="o">{</span> <span class="n">instruction</span><span class="o">.</span><span class="na">accept</span><span class="o">(</span><span class="n">clazz</span><span class="o">,</span> <span class="n">method</span><span class="o">,</span> <span class="n">codeAttribute</span><span class="o">,</span> <span class="n">offset</span><span class="o">,</span> <span class="n">matcher</span><span class="o">);</span> <span class="c1">// did we find a match?</span> <span class="k">if</span> <span class="o">(</span><span class="n">matcher</span><span class="o">.</span><span class="na">isMatching</span><span class="o">())</span> <span class="o">{</span> <span class="nc">InstructionSequenceMatcher</span> <span class="n">matcher</span> <span class="o">=</span> <span class="n">matcher1</span><span class="o">.</span><span class="na">isMatching</span><span class="o">()</span> <span class="o">?</span> <span class="n">matcher1</span> <span class="o">:</span> <span class="n">matcher2</span><span class="o">;</span> <span class="kt">int</span> <span class="n">classIndex</span> <span class="o">=</span> <span class="n">matcher</span><span class="o">.</span><span class="na">matchedConstantIndex</span><span class="o">(</span><span class="no">X</span><span class="o">);</span> <span class="kt">int</span> <span class="n">nameIndex</span> <span class="o">=</span> <span class="n">matcher</span><span class="o">.</span><span class="na">matchedConstantIndex</span><span class="o">(</span><span class="no">Y</span><span class="o">);</span> <span class="kt">int</span> <span class="n">stringIndex</span> <span class="o">=</span> <span class="n">matcher</span><span class="o">.</span><span class="na">matchedConstantIndex</span><span class="o">(</span><span class="no">Z</span><span class="o">);</span> <span class="o">...</span> <span class="o">}</span> <span class="o">...</span></code></pre></figure> <p>With the code snippet above, we look for code patterns that load a constant string onto the stack and invoke a static method which takes an <strong>Object</strong> as input and return a <strong>String</strong>. In the analysed samples of jars obfuscated with <strong>stringer</strong> all encrypted strings were encrypted using the same pattern, and its also quite uncommon for generic code to use similar patterns, thus we can easily find such encrypted strings in this case.</p> <p>Now that we know which classes and methods contain encrypted strings, we can prepare the referenced decrypt method in such a way that the protection mechanism is not effective. For this purpose, we replace the aforementioned code in the decrypt method with the values of the actual calling method:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nc">InstructionSequenceBuilder</span> <span class="n">____</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">InstructionSequenceBuilder</span><span class="o">();</span> <span class="n">instructions</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Instruction</span><span class="o">[][][]</span> <span class="o">{</span> <span class="o">{</span> <span class="n">____</span><span class="o">.</span><span class="na">invokestatic</span><span class="o">(</span><span class="s">"sun/misc/SharedSecrets"</span><span class="o">,</span> <span class="s">"getJavaLangAccess"</span><span class="o">,</span> <span class="s">"()Lsun/misc/JavaLangAccess;"</span><span class="o">)</span> <span class="o">.</span><span class="na">aload</span><span class="o">(</span><span class="mi">1</span><span class="o">)</span> <span class="o">.</span><span class="na">iconst_2</span><span class="o">()</span> <span class="o">.</span><span class="na">aaload</span><span class="o">()</span> <span class="o">.</span><span class="na">invokevirtual</span><span class="o">(</span><span class="s">"java/lang/StackTraceElement"</span><span class="o">,</span> <span class="s">"getClassName"</span><span class="o">,</span> <span class="s">"()Ljava/lang/String;"</span><span class="o">)</span> <span class="o">.</span><span class="na">invokestatic</span><span class="o">(</span><span class="s">"java/lang/Class"</span><span class="o">,</span> <span class="s">"forName"</span><span class="o">,</span> <span class="s">"(Ljava/lang/String;)Ljava/lang/Class;"</span><span class="o">)</span> <span class="o">.</span><span class="na">invokeinterface</span><span class="o">(</span><span class="s">"sun/misc/JavaLangAccess"</span><span class="o">,</span> <span class="s">"getConstantPool"</span><span class="o">,</span> <span class="s">"(Ljava/lang/Class;)Lsun/reflect/ConstantPool;"</span><span class="o">)</span> <span class="o">.</span><span class="na">invokevirtual</span><span class="o">(</span><span class="s">"sun/reflect/ConstantPool"</span><span class="o">,</span> <span class="s">"getSize"</span><span class="o">,</span> <span class="s">"()I"</span><span class="o">)</span> <span class="o">.</span><span class="na">invokevirtual</span><span class="o">(</span><span class="s">"java/lang/StringBuilder"</span><span class="o">,</span> <span class="s">"append"</span><span class="o">,</span> <span class="s">"(I)Ljava/lang/StringBuilder;"</span><span class="o">).</span><span class="na">__</span><span class="o">(),</span> <span class="n">____</span><span class="o">.</span><span class="na">pushInt</span><span class="o">(</span><span class="n">constantPoolSize</span><span class="o">)</span> <span class="o">.</span><span class="na">invokevirtual</span><span class="o">(</span><span class="s">"java/lang/StringBuilder"</span><span class="o">,</span> <span class="s">"append"</span><span class="o">,</span> <span class="s">"(I)Ljava/lang/StringBuilder;"</span><span class="o">).</span><span class="na">__</span><span class="o">(),</span> <span class="o">},</span> <span class="o">....</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">visitCodeAttribute</span><span class="o">(</span><span class="nc">Clazz</span> <span class="n">clazz</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">CodeAttribute</span> <span class="n">codeAttribute</span><span class="o">)</span> <span class="o">{</span> <span class="nc">CodeAttributeEditor</span> <span class="n">codeAttributeEditor</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CodeAttributeEditor</span><span class="o">();</span> <span class="n">clazz</span><span class="o">.</span><span class="na">accept</span><span class="o">(</span> <span class="k">new</span> <span class="nf">AllMethodVisitor</span><span class="o">(</span> <span class="k">new</span> <span class="nf">AllAttributeVisitor</span><span class="o">(</span> <span class="k">new</span> <span class="nf">PeepholeEditor</span><span class="o">(</span><span class="n">codeAttributeEditor</span><span class="o">,</span> <span class="k">new</span> <span class="nf">InstructionSequencesReplacer</span><span class="o">(</span><span class="n">constants</span><span class="o">,</span> <span class="n">instructions</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">codeAttributeEditor</span><span class="o">,</span> <span class="k">new</span> <span class="nf">InstructionCounter</span><span class="o">())))));</span> <span class="o">}</span></code></pre></figure> <p>As you can see in this code snippet, we replace certain instruction patterns with known values so that the decryption works regardless from which method it is being called. Now the only thing left to do is to copy the original code to a new class, modify it as described, load the generated class and execute the method with the given encrypted string:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nc">ProgramClass</span> <span class="n">duplicatedClass</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">ProgramClass</span><span class="o">(</span><span class="n">originalClass</span><span class="o">.</span><span class="na">u4version</span><span class="o">,</span> <span class="mi">1</span><span class="o">,</span> <span class="k">new</span> <span class="nc">Constant</span><span class="o">[</span><span class="mi">1</span><span class="o">],</span> <span class="n">originalClass</span><span class="o">.</span><span class="na">u2accessFlags</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="mi">0</span><span class="o">);</span> <span class="nc">ConstantPoolEditor</span> <span class="n">constantPoolEditor</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConstantPoolEditor</span><span class="o">(</span><span class="n">duplicatedClass</span><span class="o">);</span> <span class="n">duplicatedClass</span><span class="o">.</span><span class="na">u2thisClass</span> <span class="o">=</span> <span class="n">constantPoolEditor</span><span class="o">.</span><span class="na">addClassConstant</span><span class="o">(</span><span class="n">originalClass</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="kc">null</span><span class="o">);</span> <span class="n">duplicatedClass</span><span class="o">.</span><span class="na">u2superClass</span> <span class="o">=</span> <span class="n">constantPoolEditor</span><span class="o">.</span><span class="na">addClassConstant</span><span class="o">(</span><span class="nc">ClassConstants</span><span class="o">.</span><span class="na">NAME_JAVA_LANG_OBJECT</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span> <span class="c1">// Copy over the class members.</span> <span class="nc">MemberAdder</span> <span class="n">memberAdder</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MemberAdder</span><span class="o">(</span><span class="n">duplicatedClass</span><span class="o">);</span> <span class="n">originalClass</span><span class="o">.</span><span class="na">fieldsAccept</span><span class="o">(</span><span class="n">memberAdder</span><span class="o">);</span> <span class="n">originalClass</span><span class="o">.</span><span class="na">methodsAccept</span><span class="o">(</span><span class="n">memberAdder</span><span class="o">);</span> <span class="n">modifiedClass</span><span class="o">.</span><span class="na">accept</span><span class="o">(</span> <span class="k">new</span> <span class="nf">ProtectionRemover</span><span class="o">(</span><span class="nc">ClassUtil</span><span class="o">.</span><span class="na">externalClassName</span><span class="o">(</span><span class="n">clazz</span><span class="o">.</span><span class="na">getName</span><span class="o">()),</span> <span class="n">method</span><span class="o">.</span><span class="na">getName</span><span class="o">(</span><span class="n">clazz</span><span class="o">),</span> <span class="n">constantPoolLength</span><span class="o">));</span> <span class="n">modifiedClass</span><span class="o">.</span><span class="na">accept</span><span class="o">(</span><span class="k">new</span> <span class="nc">ProgramClassWriter</span><span class="o">(</span><span class="n">os</span><span class="o">))</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">content</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="na">toByteArray</span><span class="o">();</span> <span class="o">...</span> <span class="c1">// load the class contents with a custom ClassLoader which effectively calls defineClass.</span> <span class="nc">ClassLoader</span> <span class="n">classLoader</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">ByteClassLoader</span><span class="o">(</span><span class="k">new</span> <span class="no">URL</span><span class="o">[]</span> <span class="o">{</span> <span class="n">inputURL</span> <span class="o">},</span> <span class="nc">CodeLifter</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">(),</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">singletonMap</span><span class="o">(</span><span class="n">externalClassName</span><span class="o">,</span> <span class="n">content</span><span class="o">));</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">loadedClass</span> <span class="o">=</span> <span class="nc">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="n">externalClassName</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="n">classLoader</span><span class="o">);</span> <span class="o">...</span></code></pre></figure> <p>Now that we have the modified decrypt method, we can just call it via reflection using the encrypted string as parameter. The returned result can be used to replace the original code in the obfuscated jar with a simple ldc instruction.</p> <p>After we have implemented all this logic we see that the decrypted string still results in garbage, so there must be another protection mechanism against code lifting. Some more debugging reveals that the <strong>CodeSource</strong> of the <a href="https://docs.oracle.com/javase/7/docs/api/java/security/ProtectionDomain.html">ProtectionDomain</a> of the class containing the decrypt method is also verified. If this does not return the expected value, the decryption is unsuccessful. Luckily this protection mechanism can easily be avoided by modifying the <strong>CodeSource</strong> of a given class using reflection:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="no">URL</span> <span class="n">inputURL</span> <span class="o">=</span> <span class="o">...</span> <span class="c1">// original file name of the obfuscated jar</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">modifiedClass</span> <span class="o">=</span> <span class="o">....</span> <span class="nc">CodeSource</span> <span class="n">codeSource</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">CodeSource</span><span class="o">(</span><span class="n">inputURL</span><span class="o">,</span> <span class="o">(</span><span class="nc">CodeSigner</span><span class="o">[])</span> <span class="kc">null</span><span class="o">);</span> <span class="nc">ProtectionDomain</span> <span class="n">domain</span> <span class="o">=</span> <span class="n">modifiedClass</span><span class="o">.</span><span class="na">getProtectionDomain</span><span class="o">();</span> <span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">reflect</span><span class="o">.</span><span class="na">Field</span> <span class="n">field</span> <span class="o">=</span> <span class="n">domain</span><span class="o">.</span><span class="na">getClass</span><span class="o">().</span><span class="na">getDeclaredField</span><span class="o">(</span><span class="s">"codesource"</span><span class="o">);</span> <span class="n">field</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="n">field</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">domain</span><span class="o">,</span> <span class="n">codeSource</span><span class="o">);</span></code></pre></figure> <p>So we change the <strong>CodeSource</strong> of the modified class to the one that the decrypt method expects, and finally, we can decrypt any strings in the obfuscated jar simply by the means of static code analysis.</p> <p>The full source code to decrypt jars obfuscated by <strong>stringer</strong> will be made available at a later time. More to come!</p> Fri, 05 Jun 2020 16:00:00 +0000 https://netomi.github.io/2020/06/05/analysing-stringer-obfuscation.html https://netomi.github.io/2020/06/05/analysing-stringer-obfuscation.html java byte-code-engineering deobfuscation Minimizing shaded dependencies with gradle and R8 <p>Sometimes you need or want to include 3rd party dependencies into your own jar file e.g. to provide your users with an uber jar. These dependencies can additionally be repackaged to avoid any potential classpath issues. There exist various plugins for build systems like gradle or maven to achieve this:</p> <ul> <li><a href="http://maven.apache.org/plugins/maven-shade-plugin/index.html">Android shade plugin for maven</a></li> <li><a href="https://imperceptiblethoughts.com/shadow/">Shadow jar plugin for gradle</a></li> </ul> <p>Both plugins also offer a simple minimization feature, which tries to shrink the 3rd party dependencies as much as possible. For this purpose a small and nice <a href="https://github.com/tcurdt/jdependency">library</a> is being used that is able to analyse transitive dependencies of class files.</p> <p>This already leads to some good results. Lets take a simple example. In my project <a href="https://github.com/netomi/uom">Units of Mesurement</a> I have the need to include some code from other projects, namely <a href="https://github.com/google/guava">guava</a> and <a href="https://github.com/hibernate/hibernate-validator">hibernate validator</a>. These two projects contain some nice utility classes that I would like to use in my own library:</p> <ul> <li><a href="https://github.com/hibernate/hibernate-validator/blob/master/engine/src/main/java/org/hibernate/validator/internal/util/ConcurrentReferenceHashMap.java">ConcurrentReferenceHashMap</a></li> <li><a href="https://github.com/google/guava/blob/master/guava/src/com/google/common/base/Suppliers.java#L101">Suppliers#memoize()</a></li> </ul> <p>In case only such a limited number of classes / methods are being used, copy&amp;pasting the relevant code to your project is a viable option. But I wanted to explore if the minimize feature of the shadow jar plugin can help me without copying the code which is tedious and makes it more difficult to integrate potential bugfixes (apart from the testing coverage which might drop for your own code).</p> <p>So I added a configuration like that to my project to add the 2 libraries as dependency, adding a relocate config, and enable the standard minimize feature:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell">shadowJar <span class="o">{</span> exclude <span class="s1">'META-INF/**'</span> exclude <span class="s1">'**/**.properties'</span> exclude <span class="s1">'**/*.swp'</span> relocate <span class="s1">'com.google.common'</span>, <span class="s1">'org.netomi.uom.util.shadow.guava'</span> relocate <span class="s1">'org.hibernate.validator.internal'</span>, <span class="s1">'org.netomi.uom.util.shadow.hibernate'</span> minimize<span class="o">()</span> <span class="o">}</span> dependencies <span class="o">{</span> compile<span class="o">(</span><span class="s1">'com.google.guava:guava:28.2-jre'</span><span class="o">)</span> compile<span class="o">(</span><span class="s1">'org.hibernate.validator:hibernate-validator:6.1.3.Final'</span><span class="o">)</span> ... <span class="o">}</span></code></pre></figure> <p>The resulting jar contains the following classes from the 2 libraries:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell">tn@proteus:~/workspace/netomi/uom/build/libs<span class="nv">$ </span>unzip <span class="nt">-l</span> uom-1.0-SNAPSHOT-all.jar | <span class="nb">grep </span>shadow 0 2020-04-20 15:43 org/netomi/uom/util/shadow/ 0 2020-04-20 15:43 org/netomi/uom/util/shadow/guava/ 0 2020-04-20 15:43 org/netomi/uom/util/shadow/guava/annotations/ 616 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/annotations/Beta.class 670 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/annotations/GwtCompatible.class 678 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/annotations/GwtIncompatible.class 304 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/annotations/VisibleForTesting.class 0 2020-04-20 15:43 org/netomi/uom/util/shadow/guava/base/ 4184 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/Absent.class 851 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/AbstractIterator<span class="nv">$1</span>.class 1388 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/AbstractIterator<span class="nv">$State</span>.class 2381 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/AbstractIterator.class 1068 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/CharMatcher<span class="nv">$1</span>.class 2104 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/CharMatcher<span class="nv">$And</span>.class ....</code></pre></figure> <p>in total 131 classes:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell">tn@proteus:~/workspace/netomi/uom/build/libs<span class="nv">$ </span>unzip <span class="nt">-l</span> uom-1.0-SNAPSHOT-all.jar | <span class="nb">grep </span>shadow | <span class="nb">wc</span> <span class="nt">-l</span> 131</code></pre></figure> <p>This would definitely be too much, and I would prefer to copy&amp;paste the relevant classes to my own library and update my NOTICE.txt file accordingly. I have worked for several years on a tool called <a href="https://www.guardsquare.com/en/products/proguard">ProGuard</a>, so I was pretty sure that we can do better than that. Due to license problems (ProGuard is licensed under GPL) I decided to use <a href="https://r8.googlesource.com/r8">R8</a> to extend the shadow jar plugin to use a full blown shrinker to improve the results of its minimization feature. R8 is the default minimization tool of the Android SDK and licensed under Apache v2.0 license.</p> <p>The result of this proof-of-concept can be found in this <a href="https://github.com/johnrengelman/shadow/pull/566">pull request</a> for the shadow jar plugin. Using R8 instead of just simply collecting transitive class dependencies results in a shaded jar that contains only this number of shaded classes:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell">tn@proteus:~/workspace/netomi/uom/build/libs<span class="nv">$ </span>unzip <span class="nt">-l</span> uom-1.0-SNAPSHOT-all.jar | <span class="nb">grep </span>shadow 0 2020-04-20 15:52 org/netomi/uom/util/shadow/ 0 2020-04-20 15:52 org/netomi/uom/util/shadow/guava/ 0 2020-04-20 15:52 org/netomi/uom/util/shadow/guava/base/ 408 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/Preconditions.class 194 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/Supplier.class 1760 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/Suppliers<span class="nv">$MemoizingSupplier</span>.class 1812 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/Suppliers<span class="nv">$NonSerializableMemoizingSupplier</span>.class 850 2019-12-26 22:08 org/netomi/uom/util/shadow/guava/base/Suppliers.class 0 2020-04-20 15:52 org/netomi/uom/util/shadow/hibernate/ 0 2020-04-20 15:52 org/netomi/uom/util/shadow/hibernate/util/ 520 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$Option</span>.class 9488 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap.class 2082 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$EntrySet</span>.class 1214 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$KeyIterator</span>.class 1689 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$EntryIterator</span>.class 10076 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$Segment</span>.class 1158 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$SoftKeyReference</span>.class 1225 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$SoftValueReference</span>.class 221 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$KeyReference</span>.class 3650 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$HashEntry</span>.class 2885 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$HashIterator</span>.class 2168 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$SimpleEntry</span>.class 1225 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$WeakValueReference</span>.class 624 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$ReferenceType</span>.class 1494 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$Values</span>.class 1395 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$WriteThroughEntry</span>.class 1222 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$ValueIterator</span>.class 1158 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$WeakKeyReference</span>.class 1676 2020-04-10 11:22 org/netomi/uom/util/shadow/hibernate/util/ConcurrentReferenceHashMap<span class="nv">$KeySet</span>.class total 29</code></pre></figure> <p>Most of them are inner classes of the <em>ConcurrentReferenceHashMap</em> which are simply needed and can not be shrunk. This result is certainly much better and would allow me to go the route of shading off-the-shelf dependencies into my own library rather than copy&amp;pasting code.</p> <p>If you would like to see this feature in the shadow jar plugin, star this <a href="https://github.com/johnrengelman/shadow/issues/565">issue</a>.</p> <p>The test that I made with my own library can be found <a href="https://github.com/netomi/uom/tree/shadow-dependencies">here</a>.</p> <p>Note: I am of course perfectly aware that under normal circumstances it is not advised to shade any 3rd party dependency as it might result in duplicated code being loaded by consumers of your library. In this specific use-case I think a technique like this makes sense as I want to include a very small subset of a larger dependency without cutting the ties to upstream releases of these dependencies (and potentially using more features).</p> Mon, 20 Apr 2020 13:00:00 +0000 https://netomi.github.io/2020/04/20/minimizing-shaded-dependencies.html https://netomi.github.io/2020/04/20/minimizing-shaded-dependencies.html shrinking gradle r8 Managing multiple jdk versions using sdkman <p>It is a common use-case to have multiple versions of various tools installed on your computer. Notable examples are different JDK versions or gradle. For a long time I installed them manually and added aliases to my shell configuration like that:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell"><span class="nb">alias </span><span class="nv">gradle48</span><span class="o">=</span><span class="s2">"~/bin/gradle-4.8.1/bin/gradle"</span></code></pre></figure> <p>This works well for tools like gradle, but installing and maintaining multiple JDK versions was always a bit of a pain. Recently I found a very neat and amazing solution to this problem: <a href="https://sdkman.io/">sdkman</a>.</p> <p>It is a small tool that manages multiple versions of development kits on your computer. Installation is very easy:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell"><span class="nv">$ </span>curl <span class="nt">-s</span> <span class="s2">"https://get.sdkman.io"</span> | bash</code></pre></figure> <p>and afterwards add something like this to your <em>.bashrc</em> or respective profile script:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell"><span class="o">[[</span> <span class="nt">-s</span> <span class="s2">"~/.sdkman/bin/sdkman-init.sh"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">source</span> <span class="s2">"~/.sdkman/bin/sdkman-init.sh"</span></code></pre></figure> <p>Now you should be able to use the <em>sdk</em> command to install different SDKs or change the current version in use:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell">sdk list java</code></pre></figure> <p>will print something like this:</p> <figure class="highlight"><pre><code class="language-shell" data-lang="shell"><span class="o">================================================================================</span><span class="se">\n</span> Available Java Versions <span class="o">================================================================================</span> Vendor | Use | Version | Dist | Status | Identifier <span class="nt">--------------------------------------------------------------------------------</span> AdoptOpenJDK | | 14.0.0.j9 | adpt | | 14.0.0.j9-adpt | | 14.0.0.hs | adpt | | 14.0.0.hs-adpt | | 13.0.2.j9 | adpt | | 13.0.2.j9-adpt | | 13.0.2.hs | adpt | | 13.0.2.hs-adpt | | 12.0.2.j9 | adpt | | 12.0.2.j9-adpt | | 12.0.2.hs | adpt | | 12.0.2.hs-adpt | | 11.0.6.j9 | adpt | | 11.0.6.j9-adpt | | 11.0.6.hs | adpt | installed | 11.0.6.hs-adpt | | 8.0.242.j9 | adpt | installed | 8.0.242.j9-adpt | <span class="o">&gt;&gt;&gt;</span> | 8.0.242.hs | adpt | installed | 8.0.242.hs-adpt ...</code></pre></figure> <p>You can see that I have installed different JDK versions and version <em>8.0.242.hs-adpt</em> is currently the default. To switch to another version by default you can use for example <code class="language-plaintext highlighter-rouge">sdk use java 11.0.6.hs-adpt</code>.</p> <p>Its an amazing tool and I can recommand it for anyone using a unix-based workstation.</p> <p>Note: in case you are wondering about the suffixes: <em>hs</em> and <em>j9</em>. They refer to the actual JVM in use:</p> <ul> <li>good old Hotspot</li> <li><a href="https://en.wikipedia.org/wiki/OpenJ9">OpenJ9</a></li> </ul> <p>The suffix <em>fx</em> indicates whether the JDK comes pre-packages with the JavaFX / OpenJFX toolkit (which is not part of the Oracle / OpenJDK distributions anymore).</p> Mon, 20 Apr 2020 12:00:00 +0000 https://netomi.github.io/2020/04/20/sdkman.html https://netomi.github.io/2020/04/20/sdkman.html cli tools Handling default methods with dynamic proxies in java <p>Java has a very powerful feature to create <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html">dynamic proxies</a> for an object implementing a given set of interfaces at runtime. This can be used to modify some implementations or adding some aspects the the object, like logging, access checks or caching. The dynamic proxy pattern is used by a number of popular libraries to implement the core of their functionality. Some well-known libraries in this regard are:</p> <ul> <li>spring (for its aspect oriented features)</li> <li>retrofit</li> </ul> <p>There is a runtime cost associated with the use of dynamic proxies, but it is usually not that bad, several benachmarks have been performed to prove their suitability for practical use-cases:</p> <ul> <li><a href="https://spring.io/blog/2007/07/19/debunking-myths-proxies-impact-performance/">Debunking myths: proxies impact performance</a></li> <li><a href="http://ordinaryjava.blogspot.com/2008/08/benchmarking-cost-of-dynamic-proxies.html">Benchmarking the cost of dynamic proxies</a></li> </ul> <p>As a rule of thumb and based on my own experiments a dynamic proxy impacts performance such that an invocation of a proxied method is roughly 1.6 times slower compared to direct invocation of the same method. Which is not too bad when you consider what happens under the hood to make this working and comparing it with the performance of general reflection calls.</p> <p>Let’s get to some code. In order to create a dynamic proxy for a given interface, one has to code the following:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nc">Object</span> <span class="n">delegate</span> <span class="o">=</span> <span class="o">...</span> <span class="nc">MyInterface</span> <span class="n">impl</span> <span class="o">=</span> <span class="o">(</span><span class="nc">MyInterface</span><span class="o">)</span> <span class="nc">Proxy</span><span class="o">.</span><span class="na">newProxyInstance</span><span class="o">(</span><span class="nc">MyInterface</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">(),</span> <span class="k">new</span> <span class="nc">Class</span><span class="o">[]</span> <span class="o">{</span> <span class="nc">MyInterface</span><span class="o">.</span><span class="na">class</span> <span class="o">},</span> <span class="k">new</span> <span class="nc">InvocationHandler</span><span class="o">()</span> <span class="o">{</span> <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">invoke</span><span class="o">(</span><span class="nc">Object</span> <span class="n">proxy</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="k">return</span> <span class="n">method</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="n">delegate</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InvocationTargetException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// this is needed to throw the original exception to the caller.</span> <span class="k">throw</span> <span class="n">ex</span><span class="o">.</span><span class="na">getCause</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">});</span></code></pre></figure> <p>This will create a dynamic proxy for the interface <em>MyInterface</em> and delegate all the method calls it receives to the Object <em>delegate</em>. To make this work, the <em>delegate</em> object must be able to process the delegated calls, i.e. it must implement the requested interface.</p> <p>Now we do not want to go into more details of dynamic proxies and what you can do with them, but rather take a look into a problem introduced with some new feature in Java 8. This version of java added support for default methods in interfaces. But what has this to do with dynamic proxies? If you take the example from above and use it with an interface containing default methods, you will get a runtime exception like this:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nc">Exception</span> <span class="n">in</span> <span class="n">thread</span> <span class="s">"main"</span> <span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">IllegalArgumentException</span><span class="o">:</span> <span class="n">object</span> <span class="n">is</span> <span class="n">not</span> <span class="n">an</span> <span class="n">instance</span> <span class="n">of</span> <span class="n">declaring</span> <span class="kd">class</span> <span class="nc">at</span> <span class="n">java</span><span class="o">.</span><span class="na">base</span><span class="o">/</span><span class="n">jdk</span><span class="o">.</span><span class="na">internal</span><span class="o">.</span><span class="na">reflect</span><span class="o">.</span><span class="na">NativeMethodAccessorImpl</span><span class="o">.</span><span class="na">invoke0</span><span class="o">(</span><span class="nc">Native</span> <span class="nc">Method</span><span class="o">)</span> <span class="n">at</span> <span class="n">java</span><span class="o">.</span><span class="na">base</span><span class="o">/</span><span class="n">jdk</span><span class="o">.</span><span class="na">internal</span><span class="o">.</span><span class="na">reflect</span><span class="o">.</span><span class="na">NativeMethodAccessorImpl</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="nc">NativeMethodAccessorImpl</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">62</span><span class="o">)</span> <span class="n">at</span> <span class="n">java</span><span class="o">.</span><span class="na">base</span><span class="o">/</span><span class="n">jdk</span><span class="o">.</span><span class="na">internal</span><span class="o">.</span><span class="na">reflect</span><span class="o">.</span><span class="na">DelegatingMethodAccessorImpl</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="nc">DelegatingMethodAccessorImpl</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">43</span><span class="o">)</span> <span class="n">at</span> <span class="n">java</span><span class="o">.</span><span class="na">base</span><span class="o">/</span><span class="n">java</span><span class="o">.</span><span class="na">lang</span><span class="o">.</span><span class="na">reflect</span><span class="o">.</span><span class="na">Method</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="nc">Method</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">566</span><span class="o">)</span> <span class="n">at</span> <span class="n">org</span><span class="o">.</span><span class="na">netomi</span><span class="o">.</span><span class="na">uom</span><span class="o">.</span><span class="na">util</span><span class="o">.</span><span class="na">Proxies</span><span class="err">$</span><span class="mi">1</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="nc">Proxies</span><span class="o">.</span><span class="na">java</span><span class="o">:</span><span class="mi">52</span><span class="o">)</span> <span class="n">at</span> <span class="n">com</span><span class="o">.</span><span class="na">sun</span><span class="o">.</span><span class="na">proxy</span><span class="o">.</span><span class="n">$Proxy0</span><span class="o">.</span><span class="na">getSystemUnit</span><span class="o">(</span><span class="nc">Unknown</span> <span class="nc">Source</span><span class="o">)</span> <span class="o">...</span></code></pre></figure> <p>This happens because the default method will be delegated to the actual object which usually does not have an implementation for this method (unless it has overridden it of course). In order to support such interfaces with dynamic proxies there is a workaround available for Java 8:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="kn">import</span> <span class="nn">java.lang.invoke.MethodHandles</span><span class="o">;</span> <span class="o">...</span> <span class="nc">Constructor</span><span class="o">&lt;</span><span class="nc">MethodHandles</span><span class="o">.</span><span class="na">Lookup</span><span class="o">&gt;</span> <span class="n">constructor</span> <span class="o">=</span> <span class="nc">MethodHandles</span><span class="o">.</span><span class="na">Lookup</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getDeclaredConstructor</span><span class="o">(</span><span class="nc">Class</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="k">if</span> <span class="o">(!</span><span class="n">constructor</span><span class="o">.</span><span class="na">isAccessible</span><span class="o">())</span> <span class="o">{</span> <span class="n">constructor</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="o">}</span> <span class="nc">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">constructor</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">method</span><span class="o">.</span><span class="na">getDeclaringClass</span><span class="o">())</span> <span class="o">.</span><span class="na">unreflectSpecial</span><span class="o">(</span><span class="n">method</span><span class="o">,</span> <span class="n">method</span><span class="o">.</span><span class="na">getDeclaringClass</span><span class="o">())</span> <span class="o">.</span><span class="na">bindTo</span><span class="o">(</span><span class="n">proxy</span><span class="o">)</span> <span class="o">.</span><span class="na">invokeWithArguments</span><span class="o">(</span><span class="n">args</span><span class="o">);</span> <span class="o">...</span></code></pre></figure> <p>This will access a private constructor of the <em>MethodHandles.Lookup</em> class via reflection and use it to get a <em>MethodHandle</em> object for the default method, bind it to the actual proxy instance and invoke the method. A bit hacky but the only way to support that in Java 8.</p> <p>Unfortunately, this approach fails with Java 9, but the good news is that there is supported API that we can use for the same purpose and does not required the use of reflection:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nc">MethodHandles</span><span class="o">.</span><span class="na">Lookup</span> <span class="n">lookup</span> <span class="o">=</span> <span class="nc">MethodHandles</span><span class="o">.</span><span class="na">privateLookupIn</span><span class="o">(</span><span class="n">method</span><span class="o">.</span><span class="na">getDeclaringClass</span><span class="o">(),</span> <span class="nc">MethodHandles</span><span class="o">.</span><span class="na">lookup</span><span class="o">())</span> <span class="nc">MethodHandle</span> <span class="n">handle</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="nc">MethodType</span> <span class="n">methodType</span> <span class="o">=</span> <span class="nc">MethodType</span><span class="o">.</span><span class="na">methodType</span><span class="o">(</span><span class="n">method</span><span class="o">.</span><span class="na">getReturnType</span><span class="o">(),</span> <span class="n">method</span><span class="o">.</span><span class="na">getParameterTypes</span><span class="o">());</span> <span class="k">if</span> <span class="o">(</span><span class="nc">Modifier</span><span class="o">.</span><span class="na">isStatic</span><span class="o">(</span><span class="n">method</span><span class="o">.</span><span class="na">getModifiers</span><span class="o">()))</span> <span class="o">{</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">lookup</span><span class="o">.</span><span class="na">findStatic</span><span class="o">(</span><span class="n">method</span><span class="o">.</span><span class="na">getDeclaringClass</span><span class="o">(),</span> <span class="n">method</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">methodType</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">lookup</span><span class="o">.</span><span class="na">findSpecial</span><span class="o">(</span><span class="n">method</span><span class="o">.</span><span class="na">getDeclaringClass</span><span class="o">(),</span> <span class="n">method</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">methodType</span><span class="o">,</span> <span class="n">method</span><span class="o">.</span><span class="na">getDeclaringClass</span><span class="o">());</span> <span class="o">}</span> <span class="nc">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">handle</span><span class="o">.</span><span class="na">bindTo</span><span class="o">(</span><span class="n">proxy</span><span class="o">).</span><span class="na">invokeWithArguments</span><span class="o">(</span><span class="n">args</span><span class="o">);</span> <span class="o">...</span></code></pre></figure> <p>This is a bit more complex but it is supported with all versions since Java 9+. To wrap things up we have to modify our initial code for a delegating proxy to something like that:</p> <figure class="highlight"><pre><code class="language-java" data-lang="java"><span class="nc">Object</span> <span class="n">delegate</span> <span class="o">=</span> <span class="o">...</span> <span class="nc">MyInterface</span> <span class="n">impl</span> <span class="o">=</span> <span class="o">(</span><span class="nc">MyInterface</span><span class="o">)</span> <span class="nc">Proxy</span><span class="o">.</span><span class="na">newProxyInstance</span><span class="o">(</span><span class="nc">MyInterface</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">(),</span> <span class="k">new</span> <span class="nc">Class</span><span class="o">[]</span> <span class="o">{</span> <span class="nc">MyInterface</span><span class="o">.</span><span class="na">class</span> <span class="o">},</span> <span class="k">new</span> <span class="nc">InvocationHandler</span><span class="o">()</span> <span class="o">{</span> <span class="kd">public</span> <span class="nc">Object</span> <span class="nf">invoke</span><span class="o">(</span><span class="nc">Object</span> <span class="n">proxy</span><span class="o">,</span> <span class="nc">Method</span> <span class="n">method</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Throwable</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(!</span><span class="n">method</span><span class="o">.</span><span class="na">isDefault</span><span class="o">())</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="k">return</span> <span class="n">method</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="n">delegate</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">InvocationTargetException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// this is needed to throw the original exception to the caller.</span> <span class="k">throw</span> <span class="n">ex</span><span class="o">.</span><span class="na">getCause</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="k">return</span> <span class="nc">DefaultMethodHandler</span><span class="o">.</span><span class="na">getMethodHandle</span><span class="o">(</span><span class="n">method</span><span class="o">).</span><span class="na">bindTo</span><span class="o">(</span><span class="n">proxy</span><span class="o">).</span><span class="na">invokeWithArguments</span><span class="o">(</span><span class="n">args</span><span class="o">);</span> <span class="o">}</span> <span class="o">});</span></code></pre></figure> <p>The class <em>DefaultMethodHandler</em> wraps up the different mechanisms to get a method handle for a default method depending on the current JVM, the code can be found <a href="https://github.com/netomi/uom/blob/master/src/main/java/org/netomi/uom/util/Proxies.java#L63">here</a>. It is a derived work from the <a href="https://github.com/spring-projects/spring-framework">spring framework</a>, released under Apache v2.0 license.</p> <p>Taking default methods correctly into account opens up a lot of possibilities for creating dynamic proxies. In my own project <a href="https://github.com/netomi/uom">Units of Measurement</a> I use proxies to create concrete quantity implementations for different datatypes (double, BigDecimal) at runtime. This is a very flexible approach which can also easily be extended in the future with only a minimal performance impact.</p> Fri, 17 Apr 2020 10:00:00 +0000 https://netomi.github.io/2020/04/17/default-methods.html https://netomi.github.io/2020/04/17/default-methods.html java dynamic-proxy default-methods