玩转Java字节码(下)

  接上篇文章,操作字节码的框架有很多Javassist,ASM,BCEL等,这里我用ASM来举例。在了解字节码组成后,很容易通过ASM构建一个Class出来,代码如下:

package classloader;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class HelloClassGeneratorTest {
    public static void main(String[] args) throws IOException {
        ClassWriter classWriter = new ClassWriter(0);
        classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "Hello", null, "java/lang/Object", null);

        // 构造方法
        MethodVisitor constructorMethod = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        constructorMethod.visitCode();
        constructorMethod.visitVarInsn(Opcodes.ALOAD, 0);
        constructorMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        constructorMethod.visitInsn(Opcodes.RETURN);
        constructorMethod.visitMaxs(1, 1);
        constructorMethod.visitEnd();

        // sayHello方法
        MethodVisitor helloMethod = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;", null, null);
        helloMethod.visitCode();
        helloMethod.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        helloMethod.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        helloMethod.visitInsn(Opcodes.DUP);
        helloMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 0);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitLdcInsn(" -> ");
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 1);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 1);
        helloMethod.visitInsn(Opcodes.ARETURN);
        helloMethod.visitMaxs(3, 2);
        helloMethod.visitEnd();

        classWriter.visitEnd();

        byte[] data = classWriter.toByteArray();
        File file = new File("D://Hello.class");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        fileOutputStream.write(data);
        fileOutputStream.close();
    }
}

  内容很简单,通过ClassWriter创建了一个Hello类,定义了一个构造方法和sayHello方法,接收一个String参数,返回String类型。将得到的Class字节码保存到D://Hello.class文件中,这个文件可以用jd-gui反编译回Java源代码。

  ASM设计模式很有意思,访问者模式(Visitor Pattern),我常常把它比作一个拥有多层的抽屉的模具,一层一层打开,放原材料,最后产出一个东西。MethodVisitor的用法可以参考文档,这里不详述,大致上都是一些字节码指令操作。

  以上代码也是模仿了javap反编译出来的内容,运行后无误的话可以看到在D盘有了一个Hello.class文件,接下来我们要加载这个文件,并运行其中的方法。代码如下:

package classloader;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class MyLoader extends ClassLoader {
    public Class<?> defineMyClass(byte[] b, int off, int len) {
        return super.defineClass(null, b, off, len);
    }
}

  自定义一个Classloader,仅调用父类的defineClass即可。

package classloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class MyLoaderTest {
    public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {
        File file = new File("D://Hello.class");
        InputStream input = new FileInputStream(file);
        byte[] result = new byte[1024];

        int count = input.read(result);
        MyLoader loader = new MyLoader();
        Class clazz = loader.defineMyClass(result, 0, count);
        System.out.println(clazz.getCanonicalName());

        Object o = clazz.newInstance();
        try {
            Method method = clazz.getMethod("sayHello", String.class);
            Object returnObject = method.invoke(o, "World!");
            System.out.println("returnObject = " + returnObject);
        } catch (IllegalArgumentException | InvocationTargetException
                | NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        }
    }
}

  将class以字节数组形式读取给Classloader来加载,通过反射调用sayHello方法,专递一个World!参数进去,可以看到输出以下结果:

Hello
Hello -> World!
returnObject = World!

  这样一个从Java代码编译与反编译,自己生成字节码到加载运行,还能反编译回去。这样一个完整的循环就算完成了,虽然谈不上很有深度,但从这里能引发很多值得思考的地方。更多内容还是要参考Java虚拟机规范。

玩转Java字节码(上)

  Java字节码(Byte-code)是指Java源代码编译而成的,供JVM虚拟机执行的代码。用文本编辑器打开将是一团乱码,用十六进制编辑器打开能勉强看懂头部一些规范,例如魔数,主次版本。而用/bin/javap“反编译”之后可以得到一个人类可读的代码段,类似于用Wireshark来分析cap数据包。

  要看懂这个文件必须要知道字节码规范,主要有以下两个表格内容构成。其中表1是Class文件的组成部分,各段依次排列,排列紧密,无多余的分隔符。其中u1~u8表示占用字节数,1表示1个字节,2表示2个字节,4字节,8字节。例如魔数:仅有1个,占用4个字节,位于文件的前4个字节。紧接着4个字节是minor_version,major_version各占用2个字节。接下来2个字节是常量池数量统计,意味着一个Class不能有超过65535个常量池(maybe)。接下来是常量池,常量池的格式由表2来定义,下一段再来分析。依次类推可以将整个Class文件分析出来。

表1Class文件组成

  常量池中的每一个常量项通常有2~3个项目组成。例如:CONSTANT_Utf8_info这一项,第1个字节是表示定义(tag),值为1,紧接着2个字节表示该项将占用的长度(length),意味着接下来的几个字节长度将是字符串的实际内容。

  再来分析下CONSTANT_Integer_info项,从下表可以看出头一个字节tag值为3,接下来4个字节安高位在前编码表示int值,所以在Java中int数据类型的最大值是去掉一个符号位的0x7FFFFFFF。Utf8,Integer,Float,Long,Double这5个都是类似的,直接以值的形式存储。其余常量项都是存储着引用值(index),分别指向这5个值或其它地方,这里不一一介绍了。

  等等java不是八大基本数据类型吗,这里怎么只有提到了4个,没错,小于int的都当作int处理了。也就是boolean,byte,char,short都“拉长”至int级别来对待。例如:boolean a = “true” 对应的字节码指令是:iconst_1,putfield a,将int型常量值1进栈赋值给变量a。

  介于编译器可能会做一些性能优化,例如int值超过2字节(32768)才会加入到常量项,小于2字节由sipush指令在运行时分配。

表2Class 14个常量项

  分析这玩意实在有些无聊,既然本文是以玩转为主题,自然就说点有趣的。首先一个简单的Java代码如下:

package classloader;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class Hello {
    public String sayHello(String name) {
        System.out.println(this.getClass().getName() + " -> " + name);
        return name;
    }
}

  这段代码自然再简单不过了,随便一个文本编辑器敲进去,javac编译,找个main方法调用完事。那能不能不编译直接调用?当然没问题,先不谈这个,先来看看编译后究竟是个什么样子。我们用WinHex来打开编译好的Class文件,截图如下:

Hello.class

  黑色标记的是全部的常量项tag值(一个个标出来,该是有多蛋疼~),自己可以对照表2慢慢分析。当然也可以借助javap来“反编译”下,输出更为直观的表格:

Constant pool:
   #1 = Utf8               classloader.Hello
   #2 = Class              #1             // "classloader.Hello"
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               sayHello
  #10 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #11 = Utf8               java/lang/System
  #12 = Class              #11            // java/lang/System
  #13 = Utf8               out
  #14 = Utf8               Ljava/io/PrintStream;
  #15 = NameAndType        #13:#14        // out:Ljava/io/PrintStream;
  #16 = Fieldref           #12.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Utf8               java/lang/StringBuilder
  #18 = Class              #17            // java/lang/StringBuilder
  #19 = Methodref          #18.#7         // java/lang/StringBuilder."<init>":()V
  #20 = Utf8               getClass
  #21 = Utf8               ()Ljava/lang/Class;
  #22 = NameAndType        #20:#21        // getClass:()Ljava/lang/Class;
  #23 = Methodref          #4.#22         // java/lang/Object.getClass:()Ljava/lang/Class;
  #24 = Utf8               java/lang/Class
  #25 = Class              #24            // java/lang/Class
  #26 = Utf8               getName
  #27 = Utf8               ()Ljava/lang/String;
  #28 = NameAndType        #26:#27        // getName:()Ljava/lang/String;
  #29 = Methodref          #25.#28        // java/lang/Class.getName:()Ljava/lang/String;
  #30 = Utf8               append
  #31 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #32 = NameAndType        #30:#31        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #33 = Methodref          #18.#32        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #34 = Utf8                ->
  #35 = String             #34            //  ->
  #36 = Utf8               toString
  #37 = NameAndType        #36:#27        // toString:()Ljava/lang/String;
  #38 = Methodref          #18.#37        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #39 = Utf8               java/io/PrintStream
  #40 = Class              #39            // java/io/PrintStream
  #41 = Utf8               println
  #42 = Utf8               (Ljava/lang/String;)V
  #43 = NameAndType        #41:#42        // println:(Ljava/lang/String;)V
  #44 = Methodref          #40.#43        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #45 = Utf8               Code

  这里我仅放上常量池部分,这下很好对了把。Class文件就是这样一个紧凑结构,不愧是为嵌入式而打造的一门语言。回到之前的问题,有没有可能自己来生成字节码?答案是肯定的,下回分解。

Flat-UI 正确打开方式

  Flat-UI 是一款基于Bootstrap扁平风格的UI工具包,个人觉得风格比较美观,简洁,控件齐全,加之扁平化趋势,故用在了花图影铺项目中。有免费版和Pro版($39)可供选择,Pro版多了PSD原件,也用不到,选择免费就可以。

  众所周知,前端的项目一直以来都是拷贝HTML,CSS,JS到工程下直接引用就完事了,花图影铺之前就是这么做的,但这种做法已经非常过时了,自己做了修改后将无法再与官方的新版合并,而且有些细微的调整涉及到很多个地方要修改。对于Bootstrap这种巨复杂的设计,要定制化就几乎是不可能的事了。为了解决这些问题,前端开始有了新的玩法,从开发到测试再到构建都需要配套跟上。而Flat-UI 就是采用Grunt来进行自动化构建,用Bower来管理JS依赖,用Less来与编译CSS,等等等等。简单说下是如何构建Flat-UI的。

  Git clone下来的Flat-UI 包含了以下一些重要东西:

bower.json

这个是bower要用到的依赖声明文件,可以看到里面依赖了jQuery,bower会自动下载指定版本的jQuery到bower_components目录之下,以备后用。全局安装bower:npm install -g bower,新项目也可以用bower init来生成。

package.json

工程说明文件,包括工程名,作者,版本,Grunt依赖包等一些信息。如果一个新的项目,可以用grunt init打开一个向导一步一步填写。

Gruntfile.js

这个是Grunt执行脚本,会从上面的文件中读取值,里面定义了一些处理事件,比如清理之前构建,测试,压缩CSS,JS,生成文档,复制等等,甚至可以起一个静态服务器来调试。无比强大,类似于Maven pom文件。

  以上文件在Flat-UI工程目录中基本上不用修改,只需要在当前目录下执行bower install 下载依赖js包,grunt install 下载grunt工具,再执行grunt dist 就能在dist/目录下获得最新的可发布版本了,将该目录文件复制到web工程中引用就好了。

  关于less目录就包含了Flat-UI 所有CSS配置地方,主要修改的就是这里了,比如改变导航背景颜色等等。其中按模块归类的非常好,一眼就能看出该修改哪里,variables.less 定义了所用到的颜色,全局样式,排版,小图标,表格等等。修改起来非常容易,尤其还有一些联动的取色,通过一个基准色,计算出偏亮或者偏暗值,而不需要一个一个去查色板。修改完后只需要grunt dist 就能生成好CSS。

  Flat-UI 不仅是一个优秀的UI工具包,同时也是一个学习前后端分离构建的好例子,当然这只是针对我这以后端为主的开发者而言。

  参考:https://github.com/designmodo/Flat-UI

使用阿里云OSS WEB直传

  最近在忙着将snapast.com的图片迁移到阿里OSS服务器上,并采用WEB直传方案,以加快客户端上传速度。并使用CDN访问方式来提高图片加载速度。目前所有的图片都是存储在ECS云服务器上,用的是FastDFS存储,纯Java的图片缩放处理,速度比较慢。并且单线程就将CPU消耗完了,2M的带宽也经不起并发访问,于是决定将图片迁移到OSS上。

  WEB直传的架构设计还是很优秀的,客户端直接POST请求到OSS服务器,无需经过ECS服务器,ECS只是计算一个Policy,并接收OSS回调,整个图片上传流程就完成了。OSS具有阿里的海量CDN支持,各种网络下都能很好的优化。

这里主要记录下一些细节:

  1. OSS要设置Cors规则,以便跨域请求
  2. 2. Bucket区域要和ECS区域一致,方便内网拉取数据
  3. 使用uploadifive上传组件且auto设置为true时,在onAddQueueItem方法中获取Policy,已换plupload,在BeforeUpload中获取Policy。
  4. 由于OSS会忽略请求中文件域以后的字段,因此在封装参数的时候,将文件域(Content-Disposition: form-data; name=”file”; filename=”xxx.jpg”)置于最后,这货貌似完全照搬Amazon S3,否则会找不到key

  Upload差不多就这样了,先写到这。

轻量级ORM框架DbUtils和OrmLite

  最近在树莓派上部署了一套数据抓取工具,需要将抓取的数据录入到MySQL,特意找了下一些轻量级的ORM框架,这里简单介绍下DbUtils和OrmLite的配置和使用。

1. DbUtils

  Apache旗下的,速度与稳定性不言而喻,但其配置还是有点啰嗦,依赖dbcp连接池。

  地址:http://commons.apache.org/proper/commons-dbutils/

添加pom依赖:

<dependency>
	<groupId>commons-dbutils</groupId>
	<artifactId>commons-dbutils</artifactId>
	<version>1.6</version>
</dependency>
<dependency>
	<groupId>commons-dbcp</groupId>
	<artifactId>commons-dbcp</artifactId>
	<version>1.4</version>
</dependency>

添加dbcp.properties配置文件,放在classpath中:

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://192.168.91.128:3306/db
username=steven
password=steven
initialSize=10
maxActive=50
maxIdle=20
minIdle=5
maxWait=60000
#附带连接属性
connectionProperties=useUnicode=true;characterEncoding=utf8
#自动提交(auto-commit)
defaultAutoCommit=true
#只读(read-only)
defaultReadOnly=
#事务级别(TransactionIsolation)
defaultTransactionIsolation=READ_COMMITTED

新增JdbcUtil.java来加载属性文件

package com.dorole.utils;

import java.io.InputStream;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;

import com.mysql.jdbc.Connection;

public class JdbcUtil {
	private static DataSource ds;
	private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

	static {
		try {
			Properties prop = new Properties();
			InputStream in = JdbcUtil.class.getClassLoader()
					.getResourceAsStream("dbcp.properties");
			prop.load(in);
			ds = BasicDataSourceFactory.createDataSource(prop);
		} catch (Exception e) {
			throw new ExceptionInInitializerError(e);
		}
	}

	public static DataSource getDataSource() {
		return ds;
	}

	public static Connection getConnection() throws SQLException {
		try {
			Connection conn = tl.get();
			if (conn == null) {
				conn = (Connection) ds.getConnection();
				tl.set(conn);
			}
			return conn;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

插入数据:

QueryRunner runner = new QueryRunner(JdbcUtil.getDataSource());
String sql = "INSERT INTO `orders` (`storeName`, `telecomNumber`) VALUES (?, ?)";
Object params[] = { order.getStoreName(), order.getTelecomNumber(), };
try {
	runner.update(sql, params);
} catch (SQLException e) {
	e.printStackTrace();
}

2. OrmLite

  这个在Android上用的比较多,结合轻量级的数据库HSQLDB,非常适合嵌入式设备,但这里我们使用MySQL做演示,有意思的是连BaseDao都给封装好了,配置简单,易于使用。

  地址:http://ormlite.com/

添加pom依赖:

<dependency>
	<groupId>com.j256.ormlite</groupId>
	<artifactId>ormlite-jdbc</artifactId>
	<version>4.9</version>
</dependency>

新增OrderDao接口,默认的增删改查已经有了,故可以留空:

package com.dorole.dao;

import com.dorole.model.Order;
import com.j256.ormlite.dao.Dao;

public interface OrderDao extends Dao<Order, Integer> {

}

新增OrderDaoImpl实现类

package com.dorole.dao.impl;

import java.sql.SQLException;

import com.dorole.dao.OrderDao;
import com.dorole.model.Order;
import com.j256.ormlite.dao.BaseDaoImpl;
import com.j256.ormlite.support.ConnectionSource;

public class OrderDaoImpl extends BaseDaoImpl<Order, Integer> implements
		OrderDao {
	public OrderDaoImpl(ConnectionSource connectionSource,
			Class<Order> dataClass) throws SQLException {
		super(connectionSource, dataClass);
	}
}

初始化:

OrderDao orderDao = null;
ConnectionSource connectionSource;
try {
	connectionSource = new JdbcConnectionSource(
			"jdbc:mysql://192.168.91.128:3306/db");
	((JdbcConnectionSource) connectionSource).setUsername("steven");
	((JdbcConnectionSource) connectionSource).setPassword("steven");
	orderDao = new OrderDaoImpl(connectionSource, Order.class);
} catch (SQLException e) {
	e.printStackTrace();
}

插入数据:

try {
	Order order = new Order();
	orderDao.create(order);
} catch (SQLException e) {
	e.printStackTrace();
}