2.JDK设计

1.SPI机制

1.1.介绍

SPI(Service Provider Interface),主要是针对厂商或者插件的,在java.util.ServiceLoader的文档里有比较详细的介绍。

简单的总结下 Java SPI 机制的思想:

  1. 系统中抽象的各个模块,模块之间是通过接口实现双方的调用。具体接口实现类,需要各自分别实现,模块之间是不会出现对方的实现类的。
  2. 一旦代码里涉及具体的实现类,就违反了可拔插的原则。如果需要替换一种实现,就需要修改代码。

Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

Java SPI 的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。 而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。

ServiceLoader的实现原理,是使用类加载器,通过递归的方式,将所有加载到jvm的classpath文件夹下查找配置文件。

1.2.案例

1.common-logging: apache最早提供的日志的门面接口。只有接口,没有实现,具体方案由各提供商实现。 厂商通过将实现类放置到META-INF/services/org.apache.commons.logging.LogFactory文件中,框架就会自动加载。

2.jdbc:jdbc4.0以前,开发人员还需要基于Class.forName("xxx")的方式来装载驱动,jdbc4也基于SPI的机制来发现驱动提供商了, 可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者。

3.自定义扩展:一个内容管理系统有一个搜索模块。是基于接口编程的。搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索

接口定义如下:

package my.xyz.spi; 
import java.util.List; 
public interface Search { 
   public List serch(String keyword); 
}

A公司采用文件系统搜索的方式实现了 Search接口,B公司采用了数据库系统的方式实现了Search接口。

// A公司发布 实现jar包时,则要在jar包中META-INF/services/my.xyz.spi.Search文件中写下如下内容:
com.A.spi.impl.FileSearch

// 那么B公司发布 实现jar包时,则要在jar包中META-INF/services/my.xyz.spi.Search文件中写下如下内容:
com.B.spi.impl.DatabaseSearch

下面是 SPI 测试代码:

package com.xyz.factory;
import java.util.Iterator;
import java.util.ServiceLoader;
import my.xyz.spi.Search;
public class SearchFactory {
   private SearchFactory() {
   }
   public static Search newSearch() {
      Search search = null;
      // 加载所有的实现类
      ServiceLoader<Search> serviceLoader = ServiceLoader.load(Search.class);
      Iterator<Search> searchs = serviceLoader.iterator();
      if (searchs.hasNext()) {
         search = searchs.next();
      }
      return search;
   }
}

2.com.sun

在开发的时候,我们可能引用到com.sun下的类。比如: com.sun.xml.messaging.saaj.packaging.mime.internet.MimeUtility

但是这个类不是sun对外公开承诺的接口,所以根据根据实现的需要随时增减,在不同的jvm中也可能是不同的,甚至可能没有。所以调用这些类,可能不会向后兼容,所以一般不推荐使用。

为了能都使用这个包的类,可以引用下面的依赖。

<dependency>
    <groupId>com.sun.xml.messaging.saaj</groupId>
    <artifactId>saaj-impl</artifactId>
    <version>1.3</version>
    <exclusions>
        <exclusion>
            <groupId>activation</groupId>
            <artifactId>activation</artifactId>
        </exclusion>
    </exclusions>
</dependency>

jdk包下的结构介绍: java, javax, org和com.sun包都是jdk提供的类包,且都是在rt.jar中。 rt.jar是JAVA基础类库(java核心框架中很重要的包), 包含lang在内的大部分功能,而且rt.jar默认就在根classloader的加载路径里面,

  1. java.:Java SE的标准库,是java标准的一部分,是对外承诺的java开发接口,通常要保持向后兼容,一般不会轻易修改。 包括其他厂家(IBMJDK/HPJDK/OpenJDK)在内,所有jdk的实现,在java.上都是一样的。
  2. javax.*:是Java标准的一部分,但是没有包含在标准库中,一般属于标准库的扩展。通常属于某个特定领域,不是一般性的api。 所以以扩展的方式提供api,以避免jdk的标准库过大。当然某些早期的javax,后来被并入到标准库中,所有也应该属于新版本JDK的标准库。 比如jmx,Java 5以前是以扩展方式提供,但是jdk5以后就做为标准库的一部分了,所有javax.management也是jdk5的标准库的一部分。
  3. com.sun.* :是sun的hotspot虚拟机中java.* 和javax.*的实现类。因为包含在rt中,所以我们也可以调用。 但是因为不是sun对外公开承诺的接口,所以根据根据实现的需要随时增减,因此在不同版本的hotspot中可能是不同的, 而且在其他的jdk实现中是没有的,调用这些类,可能不会向后兼容,所以一般不推荐使用。
  4. org.* :是由企业或者组织提供的java类库,大部分不是sun公司提供的,同com.sun.*,不具备向后兼容性,会根据需要随时增减。 其中比较常用的是w3c提供的对XML、网页、服务器的类和接口。

3.基础工具类

涉及到的工具类:

  • BigDecimal
  • Arrays
  • SimpleDateFormat
  • Math
  • Random
  • StringBuilder,StringBuffer
  • TimeZone

3.1.String 格式化

  1. format(String format, Object… args)
    • format:字符串格式。
    • args…:字符串格式中由格式说明符引用的参数。如果还有格式说明符以外的参数,则忽略这些额外的参数。参数的数目是可变的,可以为0。
  2. format(Locale locale, String format, Object… args)
    • locale:指定的语言环境。
    • format:字符串格式。
    • args…:字符串格式中由格式说明符引用的参数。如果还有格式说明符以外的参数,则忽略这些额外的参数。参数的数目是可变的,可以为0。
转 换 符 说 明 示 例
%s String "mingrisoft"
%c char ‘m'
%b boolean true
%d 整数类型(十进制) 99
%x 整数类型(十六进制) FF
%o 整数类型(八进制) 77
%f 浮点类型 99.99
%a 十六进制浮点类型 FF.35AE
%e 指数类型 9.38e+5
%g 通用浮点类型(f和e类型中较短的)  
%h 散列码  
%% 百分比类型 %  
%n 换行符  
%tx 日期与时间类型(x代表不同的日期与时间转换符)  

4.枚举

5.浅复制与深复制

  • 浅复制(浅克隆):被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。 换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
  • 深复制(深克隆):为了获取对象的一份拷贝,我们可以利用Object类的clone()方法。

6.反编译

6.1.JD-GUI

常用的反编译工具 JD-GUI ,直接把编译好的jar丢进去,大部分都能反编译看到源码

源码https://github.com/java-decompiler/jd-gui

6.2.DJ Java Decompiler

DJ Java Decompiler 3.7。 下面是一个有用的参考文档,说明如何批量编译

http://www.udl.es/usuaris/jordim/Progs/Readme.txt

安装好之后,找到它的安装路径,我的是:E:\Program Files\decomp 底下有一个jad.exe文件,将它设置到环境变量path, 或者拷到其他已经了环境变量path的地方,或者在cmd中转到此文件目录下。

简单的方法是

jad -o -r -dF:\src -sjava F:\classes\**\*.class
-o  - overwrite output files without confirmation (default: no) 无需确定覆写文件
-r  - restore package directory structrure 恢复包目录结构
-s <ext></ext>- output file extension (by default '.jad') 如果不设置为-sjava,则默认扩展名为.jad

其他的,F:\classes***.class 中的两颗接连的星,表示任意层次的子目录。 OK,这么多就够完成我们的任务了。

7.代码混淆-防反编译

  1. class隔离: 通过接口的方式,将class文件远程获得。
  2. class加密: 通过将class文件加密,使用的时候,通过附加程序解密后加载
  3. 代码混淆:
    • 代码混淆是对Class文件进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能(语义)。
    • 但是混淆后的代码很难被反编译,即反编译后得出的代码是非常难懂、晦涩的,因此反编译人员很难得出程序的真正语义。
    • 从理论上来说,黑客如果有足够的时间,被混淆的代码仍然可能被破解,甚至目前有些人正在研制反混淆的工具。
    • 但是从实际情况来看,由于混淆技术的多元化发展,混淆理论的成熟,经过混淆的Java代码还是能够很好地防止反编译

到目前为止,对于Java程序的保护,混淆技术还是最基本的保护方法。Java混淆工具也非常多, 包括商业的、免费的、开放源代码的。Sun公司也提供了自己的混淆工具。 它们大多都是对Class文件进行混淆处理,也有少量工具首先对源代码进行处理,然后再对Class进行处理,这样加大了混淆处理的力度。

目前,商业上比较成功的混淆工具包括

  • JProof公司的1stBarrier
  • Eastridge公司的JShrink
  • 4thpass.com的SourceGuard

主要的混淆技术按照混淆目标可以进行如下分类,

  • 符号混淆(Lexical Obfuscation)
  • 数据混淆(Data Obfuscation)
  • 控制混淆(Control Obfuscation)
  • 预防性混淆(Prevent Transformation)

7.1.【混淆】ProGuard

ProGuard 是一个免费的 Java 类文件的压缩、优化、混肴器。常常用于Android开发用于混淆最终的项目,增加项目被反编译的难度。 作用:

  • 它删除没有用的类、字段、方法与属性。
  • 可以删除没用的注释,最大限度地优化字节码文件。
  • 使字节码最大程度地优化, 使用简短且无意义的名字来重命名类、字段和方法。

maven插件:此处需要注意,proguard plugin需要放在repackage plugin之前,否则混淆没有效果。原理就是在打包之前将代码混淆,然后再打包

<plugins>
   <!--代码混淆插件【proguard】-->
   <plugin>
       <groupId>com.github.wvengen</groupId>
       <artifactId>proguard-maven-plugin</artifactId>
       <version>2.6.0</version>
       <executions>
           <!-- 以下配置说明执行mvn的package命令时候,会执行proguard-->
           <execution>
               <phase>package</phase>
               <goals>
                   <goal>proguard</goal>
               </goals>
           </execution>
       </executions>
       <configuration>
           <!-- 就是输入Jar的名称,我们要知道,代码混淆其实是将一个原始的jar,生成一个混淆后的jar,那么就会有输入输出。 -->
           <injar>${project.build.finalName}.jar</injar>
           <!-- 输出jar名称,输入输出jar同名的时候就是覆盖,也是比较常用的配置。 -->
           <outjar>${project.build.finalName}.jar</outjar>
           <!-- 是否混淆 默认是true -->
           <obfuscate>true</obfuscate>
           <!-- 配置一个文件,通常叫做proguard.cfg,该文件主要是配置options选项,也就是说使用proguard.cfg那么options下的所有内容都可以移到proguard.cfg中 -->
           <proguardInclude>${project.basedir}/proguard.cfg</proguardInclude>
           <!-- 额外的jar包,通常是项目编译所需要的jar -->
           <libs>
               <lib>${java.home}/lib/rt.jar</lib>
               <lib>${java.home}/lib/jce.jar</lib>
               <lib>${java.home}/lib/jsse.jar</lib>
           </libs>
           <!-- 对输入jar进行过滤比如,如下配置就是对META-INFO文件不处理。 -->
           <inLibsFilter>!META-INF/**,!META-INF/versions/9/**.class</inLibsFilter>
           <!-- 这是输出路径配置,但是要注意这个路径必须要包括injar标签填写的jar -->
           <outputDirectory>${project.basedir}/target</outputDirectory>
           <!--这里特别重要,此处主要是配置混淆的一些细节选项,比如哪些类不需要混淆,哪些需要混淆-->
           <options>
               <!-- 可以在此处写option标签配置,不过我上面使用了proguardInclude,故而我更喜欢在proguard.cfg中配置 -->
           </options>
       </configuration>
   </plugin>
   <plugin>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
</plugins>

proguard.cfg

#指定Java的版本。高版本jdk暂不支持,maven的jdk版本也需要调整
-target 1.8
#proguard会对代码进行优化压缩,他会删除从未使用的类或者类成员变量等,shrink这个功能一般最好别用,所以这里添加了<option>-dontshrink</option>,我就遇到过启动jar的时候不支持压缩jar的问题
-dontshrink
#是否关闭字节码级别的优化,如果不开启则设置如下配置
-dontoptimize
#混淆时不生成大小写混合的类名,默认是可以大小写混合
-dontusemixedcaseclassnames
# 对于类成员的命名的混淆采取唯一策略
-useuniqueclassmembernames
#混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代
-adaptclassstrings

#对异常、注解信息予以保留
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
# 此选项将保存接口中的所有原始名称(不混淆)-->
-keepnames interface ** { *; }
# 此选项将保存所有软件包中的所有原始接口文件(不进行混淆)
#-keep interface * extends * { *; }
#保留参数名,因为控制器,或者Mybatis等接口的参数如果混淆会导致无法接受参数,xml文件找不到参数
-keepparameternames
# 保留枚举成员及方法
-keepclassmembers enum * { *; }
# 不混淆所有的set/get方法
-keepclassmembers public class * {void set*(***);*** get*();}


# 不混淆所有包含Component等注解的类
-keep @org.springframework.context.annotation.Bean class * {*;}
-keep @org.springframework.context.beans.factory.annotation.Autowired class * {*;}
-keep @org.springframework.context.beans.factory.annotation.Value class * {*;}
-keep @org.springframework.stereotype.Service class * {*;}
-keep @org.springframework.stereotype.Component class * {*;}
-keep @org.springframework.web.bind.annotation.RestController class * {*;}
-keep @org.springframework.context.annotation.Configuration class * {*;}

# 不混淆spring包下的类,以及类中的方法属性等
-keep class org.springframework.*.**  {*;}

# 不混淆controller入口类
#-keep class com.zx.modules.store.dto.*
#-keep class com.zx.modules.**.*Impl
#-keep class com.zx.modules.store.web.* {*;}
#-keep class com.zx.modules.store.entity.* {*;}

#忽略warn消息
-ignorewarnings
#忽略note消息
-dontnote
#打印配置信息
-printconfiguration

### spring boot的启动类不能混淆
-keep class com.zx.maven.Application {
  public static void main(java.lang.String[]);
}

7.2.【加密】Xjar

Spring Boot JAR 安全加密运行工具,同时支持的原生JAR。 基于对JAR包内资源的加密以及拓展ClassLoader来构建的一套程序加密启动, 动态解密运行的方案, 避免源码泄露以及反编译。

作用:

  • 无代码侵入, 只需要把编译好的JAR包通过工具加密即可。
  • 完全内存解密, 降低源码以及字节码泄露或反编译的风险。
  • 支持所有JDK内置加解密算法。
  • 可选择需要加解密的字节码或其他资源文件。
  • 支持Maven插件, 加密更加便捷。
  • 动态生成Go启动器, 保护密码不泄露。

maven插件:将插件放在最后一个执行。依赖仓库:jitpack.io

<plugin>
    <groupId>com.github.core-lib</groupId>
    <artifactId>xjar-maven-plugin</artifactId>
    <version>v2.0.7</version>
    <executions>
        <execution>
            <goals>
                <goal>build</goal>
            </goals>
            <phase>package</phase>
            <configuration>
                <password>44889951235894612351265ABD123</password>
                <mode>1</mode>
                <sourceDir>${project.build.directory}</sourceDir>
                <targetJar>${project.build.finalName}_x.jar</targetJar>
                <includes>
                    <include>com/zx/**</include>
                </includes>
            </configuration>
        </execution>
    </executions>
</plugin>

<pluginRepositories>
   <pluginRepository>
      <id>jitpack.io</id>
      <url>https://jitpack.io</url>
   </pluginRepository>
</pluginRepositories>

password不建议写到pom中,可以在编译的时候,通过命令传入密码。 mvn clean package -Dxjar.password=这个是密码

编译

  1. 使用maven编译之后,会生成一个xxx_x.jar(加密后的jar内部代码都是空的)和xjar.go(真实加密后的内容)。
  2. 将 xjar.go 在不同的平台进行编译即可得到不同平台的启动器可执行文件, 其中Windows下文件名为 xjar.exe 而Linux下为 xjar。
go build xjar.go

运行:密码已经植入到xjar.go这个文件中了,就不需要我们手动输入密码了。

linux:   ./xjar java  -jar lab_034_04_proguard-0.0.1-SNAPSHOT_x.jar
windows:   xjar.exe java  -jar lab_034_04_proguard-0.0.1-SNAPSHOT_x.jar

7.3.【加密】ClassFinal

是一款Java class文件安全加密工具,支持直接加密jar包或war包,无需修改任何项目代码,兼容spring,可避免源码泄漏或字节码被反编译。

功能特性

  • 无需修改原项目代码,只要把编译好的jar/war包用本工具加密即可。
  • 运行加密项目时,无需求修改tomcat,spring等源代码。
  • 支持普通jar包、springboot jar包以及普通java web项目编译的war包。
  • 支持spring framework、swagger等需要在启动过程中扫描注解或生成字节码的框架。
  • 支持maven插件,添加插件后在打包过程中自动加密。
  • 支持加密WEB-INF/lib或BOOT-INF/lib下的依赖jar包。

maven插件:

<plugin>
    <groupId>net.roseboy</groupId>
    <artifactId>classfinal-maven-plugin</artifactId>
    <version>1.2.1</version>
    <configuration>
        <password>#</password><!-- #表示启动时不需要密码,事实上对于代码混淆来说,这个密码没什么用,它只是一个启动密码 -->
        <packages>com.zx.maven</packages><!-- 加密的包名,多个包用逗号分开-->
        <excludes>org.spring</excludes>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>classFinal</goal>
            </goals>
        </execution>
    </executions>
</plugin>

package之后,看到后缀是xxx-encrypted.jar 就是加密后的jar。

源码和反编译后的效果对比。

源码
@RestController
public class DemoController {

    @RequestMapping("/")
    public int index() {
        Random R = new Random();
        return R.nextInt();
    }
}

反编译后的
@RestController
public class DemoController {
    public DemoController() {
    }

    @RequestMapping({"/"})
    public int index() {
        return 0;
    }
}

classfinal其实也可以起到代码加密的效果,功能也很强大,被classfinal加密过后的jar包,反编译了以后,方法返回值会return null或者0,方法内部会自动去掉。 并且用classfinal加密过后的jar包启动方式需要用javaagnet启动。而相比较xjar,反编译以后,反编译后,直接显示Internal Error.

启动方式:

1)无密码启动
java -javaagent:lab_034_08_classfinal-0.0.1-encrypted.jar               -jar lab_034_08_classfinal-0.0.1-encrypted.jar

2)有密码启动
java -javaagent:lab_034_08_classfinal-0.0.1-encrypted.jar="-pwd 123456" -jar lab_034_08_classfinal-0.0.1-encrypted.jar

jdk升级

jdk升级到9以上的版本,移除部分jar, 添加以下4个jar解决依赖问题。

<dependency>
   <groupId>javax.xml.bind</groupId>
   <artifactId>jaxb-api</artifactId>
   <version>2.3.0</version>
</dependency>
<dependency>
   <groupId>com.sun.xml.bind</groupId>
   <artifactId>jaxb-impl</artifactId>
   <version>2.3.0</version>
</dependency>
<dependency>
   <groupId>com.sun.xml.bind</groupId>
   <artifactId>jaxb-core</artifactId>
   <version>2.3.0</version>
</dependency>
<dependency>
   <groupId>javax.activation</groupId>
   <artifactId>activation</artifactId>
   <version>1.1.1</version>
</dependency>

文章信息

张雪随笔
master
Homepage
Issues
Download

This Software is under the terms of The Unlicense.