Javassist 字节码操作库

研究 Javassist 的起因是维护的项目是从外部采购的一个系统,有几个核心的类在构造函数里从数据库加载一些配置信息,而这些配置信息基本是不会改变的,应当缓存起来。这些类没有源码,class 文件还是混淆过的。

想来想去,觉得用字节码操作工具来改写是比较合适的,把改写后生成的 class 文件替换原来的,这样使用这些类的地方也不用做任何修改。

1. Javassist 简介

Javassist 是一个字节码操作库,通过它,可以在运行时改写类:添加新的字段、方法和构造函数,改变类、父类和接口的方法。

Javassist 定义了 CtField, CtMethod, CtConstructor, CtClass 来表示 字段、方法、构造函数、类。

继续阅读

Thread, Runnable, Callable

最近观察同事面试,发现他对问题的理解本身就有不对,这样即使面试者完全掌握了那个问题,也可能给不了他想要的回答。

他的问题是:Java 里有哪些创建线程的方式?他期望的答案是 Thread, Runnable, Callable, Future 这些。

这个理解是错误的,他也是网上看的。

一、线程与任务

线程可以理解为 可以执行程序指令的机器,而任务则是要执行的一段指令

在 Java 里用 java.lang.Thread 类来表示线程。用 java.lang.Runnable 接口表示任务。

任务创建出来后是需要放到线程上去执行的,所谓线程驱动任务执行

创建、启动线程

创建线程一般是创建一个 Thread 类的实例,可以在创建时指定任务,也可以覆写 void run() 方法来实现任务的逻辑。

Thread 类实例化后只是在 Java 堆里创建了一个线程对象,并没有跟操作系统的线程关联起来,还不能真正执行任务。需要调用 native void start() 方法(这个方法在不同的 JDK 版本里有所不同)。所以如果是继承了 Thread 类,是不能覆写 start 方法的,否则是没法启动线程的。

start 方法调用后,线程会自动执行 run 方法, run 方法执行完成后线程就会销毁。

二、Callable, Future

java.util.concurrent.Callable 表示一个带返回值的任务,是一种特殊点任务。

java.util.concurrent.Future 表示一个可在未来获得调用结果的存根。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发布组件到 Maven 中央仓库

折腾了几天,终于把一个小组件MyBatis-batch 发布了中央仓库,做个笔记记录下。

  1. 注册 sonatype JIRA 帐号并配置 settings.xml
    我是先在 sonatype 上发布,然后由 sonatype 自动同步到中央仓库的。首先要在 sonatype 注册一个 JIRA 帐号,

$M2_HOME/conf/settings.xmlservers 标签下添加如下配置:

<server>
    <id>sonatype-nexus</id>
    <username>sonatype 登录名</username>
    <password>sonatype 密码</password>
</server>
  1. 在 github 添加 ssh key
    Maven 构建的时候,会自动操作 github,比如创建 tag 。
    用 ssh-keygen 生成一对秘钥,在 https://github.com/settings/keys 页面可以添加 SSH key。

继续阅读

Java finalize 方法与垃圾回收

JVM 对于覆写了 Object.finalize() 方法的类是实例,在垃圾回收时,先放进一个队列里,
然后用一个线程执行这些实例的 finalize 方法,最后才做内存回收。

这个机制的问题是:它会影响垃圾回收,如果实现了 finalize 的实例很多,那么回收队列就会很长,而只有一个线程
在执行这些方法,这些对象就会回收得很慢。

最近碰到的一个问题跟 finalize 方法有关。
厂商提供的代码把 javax.sql.Connection 做了层封装,覆写了 finalize 方法,在里面把 Connection 关闭了。

一个同事调用的方法定义是用封装类作为入参,他调用的时候是直接 new 了一个封装类,把 connection 传进去,后续数据库操作就不定位置出现连接关闭的异常。原因就是这个临时 new 出来的封装类被垃圾回收,把 connection 关闭了。由于垃圾回收的时间是不确定的,所以报异常的时间点也不确定。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

接口与Spring自动注入

接口与自动注入

有业务接口 IService 和两种业务逻辑的实现 Aservice, Bservice 如下:

public interface IService {}

@Component
public class Aservice implements IService {}

@Component
public class Bservice implements IService {}

采用下面的方式自动注入时,会报错:

public class ManageService {
    @Autowired
    private Aservice aservice;

    @Autowired
    private Bservice bservice;

    // ...
}

异常信息类似为:nested exception is java.lang.IllegalArgumentException: Can not set xxx.ManageService field xxx.ManageService.aservice to com.sun.proxy.$Proxy48

继续阅读

Object Extract Mapping 与 网页 API

最近的项目需要访问一些外部的网页,比如百度、必应搜索等,有的只是要求把响应内容的文本提取出来,有的是要求做结构化解析的,比如百度的搜索结果,解析为一个结构化的列表,每个列表项有(标题、概要信息、详细信息的链接)。

开始时感觉是用爬虫去爬网页,然后做解析,但有个问题困扰着:比如要保存百度、必应的搜索结果到数据库,肯定只希望定义一个类来对应搜索结果的一条记录,然后写一套保存到数据库的代码。看了一些爬虫框架对于内容解析部分其实都只能自己写代码来解析网页,这样的话,如果要接搜狗就写一套解析网页的逻辑、接 360 也得写一套。。。。每接一个就得写一套。。。这不是我需要的生活。。。

怎么减少这些匹配代码?

后面看到 webmagic 的文档提到其基于注解实现的 Object/Extraction Mapping 功能,在类的字段上定义一个注解,表示这个字段用什么样的方式抽取,这样在爬虫有关的代码里就不会出现这些解析网页的逻辑。

Object Extract Mapping

如果我只需要解析百度的搜索结果,这是很好的方式,但我还要解析必应、搜狗等更多的搜索结果时就不行了。因此需要换一种方式来实现:把抽取方式的定义放到代码外面来

我就实现了一个基于 XML 的 Object-Extrac-Mapping (OEM) 小工具。

继续阅读

Spring 导入资源文件

Spring 有多种方式可以导入资源文件,可以把这些属性文件里的属性值注入到 bean 的属性上。

1. 通过 <context:property-placeholder> 标签导入

通过这种方式导入的所有属性在同一个命名空间下,如果多个属性文件里有相同的属性名,以先导入的属性文件的属性为准,不会出现覆盖。可以通过 @Value("${propName}") 的方式注入到 bean 的属性上。

这种方式不能对属性文件指定 id,没法引用指定属性文件的属性,如果有同名的属性,引用到的总是第一个导入的属性。

配置如下:

<!-- 导入外部的资源文件,可以通过 @Value("${propName}") 的方式注入到 bean 的属性上 。 -->
<context:property-placeholder
    location="classpath:/learn/properties/1.properties"
    ignore-unresolvable="true" />
<!-- 要使用 context:property-placeholder 导入多个资源文件,必须指定 ignore-unresolvable="true" -->
<!-- 多个属性文件中,属性名相同的,以第一个加载的为准,不会出现后面加载的覆盖前面的 -->
<context:property-placeholder location="learn/properties/2.properties"
    ignore-unresolvable="true" />

继续阅读

基于POI检查 excel 文件的行数、列数是否超出限制

用 POI 解析 xlsx 时,如果用这样的方式 Workbook workbook = WorkbookFactory.create(new File("super-many-row.xlsx")); 解析具有超过 100 万行的 excel ,很可能内存就被耗尽了。

因为这样虽然简单,但是要等整个 excel 都解析完成、转换成 POI 定义的对象后才会返回到业务代码,这时候才判断行数是否超过限制就晚了。

需要一种更高效的方式,在解析完成前就判断是否超过限制,了解了 POI 的 API 和 xlsx 的基本格式后,就有了下面这个工具类,基于 POI 3.13。

2016.02.28 更新:增加对 sheet 数量的校验才够完整。

继续阅读

隐式类型转换导致全表扫描

最近系统改版了一部分(其实就是重做。。。),上线之后又开始出现数据库 IO 告警了。

DBA 抓出一个全表扫描的 SQL,是把范围在两个钟内的数据找出来做更新的,这个查询的时间字段是有索引的,结果却没有用上,
跟另一个表关联的时候,关联字段也是有索引的,但还是没有用上。

在两个钟内的数据量应该是很小的,相对于总量来说,时间字段的索引没理由不用啊;从主表找出来的数据量不大,再去关联表查询也应该走索引的。
感觉就是执行计划不对了,去找 DBA 做固化。

找 DBA 先用 hint 生成一个走索引的执行计划,然后做了固化。

这个 SQL 是两个钟才执行一次的,结果固化之后值班 DBA 说主表还是全表扫描。找 DBA 再做一次固化,还是主表全表扫描。最后开了 tunning ,提示说有隐式类型转换,主表没法走索引。

大致扫了一下代码,Java 里用的数据类型是 Date 的,而表的列的类型也是 Date,应该不需要做隐式类型转换啊。

问题出在 MyBatis 的参数类型指定为 timestamp 而不是 Date;由于查询需要时分秒,而 Date 会抹去这些值,所以被指定为 timestamp,
而这个类型刚好对应到 Oracle 的 timestamp 类型,所以就出现隐式类型转换。

细节吶。。。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

Spring MVC 与线程

最近又被一个连环坑坑惨了,我踩的坑是与 Spring MVC 的线程模型有关的。

我们的系统现在有两个 war 包,分别部署在不同的 JBoss 上,第一个是互联网可访问的,叫 front 吧,第二个是在防火墙之后的,只能通过 front 来访问,叫 backend,是可以访问数据库的。

front 通过 Hessian 调用 backend 的服务。

现在有个需求要求 backend 记录互联网用户的 IP 和其他一些参数,接口太多,不可能每个方法再添加参数,由于 Hessian 也运行是在 HTTP 上的,所以我想在 Hessian 的 HTTP 请求头里把这些需要的参数传过去。

根据 debug,Hessian 采用的是 JDK 的 HessianURLConnectionFactory 来创建连接,处理 HTTP 请求,所以我就扩展了它的方法 public HessianConnection open(URL url) throws IOException ,在里面把参数放到请求头里。由于要取的是客户端实际 IP 等信息,所以还需要获取客户端的请求 HttpServletRequest ,刚好 Spring MVC 里面有个 RequestContextHolder 工具,持有了 HttpServletRequest 。然后就有了下面这行代码:HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); 。坑就挖成了。

点开 RequestContextHolder.getRequestAttributes() 方法来看看:

public static RequestAttributes getRequestAttributes() {
     RequestAttributes attributes = requestAttributesHolder.get();
     if (attributes == null) {
          attributes = inheritableRequestAttributesHolder.get();
     }
     return attributes;
}

requestAttributesHolder 是这样的:

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
          new NamedThreadLocal<RequestAttributes>("Request attributes");

NamedThreadLocal 是这样定义的: public class NamedThreadLocal<T> extends ThreadLocal<T> {
也就是说这个请求是跟线程绑定的,在别的线程是获取不到的。

其他的坑是这样的:
在 front 有个地方用了异步去调用 backend,其实是根本没必要用异步的(因为当前线程发出请求后是在等待响应的);而这个异步调用在测试环境时没有启用,被开关切去走另一个同步的分支,而生产环境确是必须走这个异步分支的。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。