Javassist字节码增强

通常对一个方法增加日志记录,安全检查都会说采用AOP或CGLIB动态代理,但无论哪种方式都必需改变原有的调用方式;
同时,大量的反射调用也必增加系统的开销。下面介绍一种不需要改变客户端调用方式而又能实现对指定方法增加缓存或日志的方式,那就是——字节码增强!

在实际项目中通常需要对一些频繁访问数据库的方法采用对象缓存,从而提高系统性能减少不必要的网络开销。
这时候一般我们会去修改方法的源码,增加Cache的put,get调用,要么采用AspectJ或cglib进行方法执行前或执行后的拦截

但采用无论采用哪种方式都必需改变客户端原有的调用方式,可能涉及变动的模块又零散(如对Account,Bank,Customer对象增加Log或Cache),况且如果对于遗留系统而又没有源码呢?

因此采用字节码增强成立一种可选的手段,允许在不改变原调用方式的情况下进行字节码增强

最直接的改造 Java 类的方法莫过于直接改写 class 文件。Java 规范详细说明了class 文件的格式,直接编辑字节码确实可以改变 Java 类的行为。操作字节码开源实现有ObjectWeb ASM,JBOSS Javassist等,最终选择Javassist是由于它操作简单,直观。

以下是字节码增强的生成器ClassEnhancedGenerator,实现对一个类的多个方法进行字节码增强



  1. public class ClassEnhancedGenerator {
  2. private ClassEnhancedGenerator(){
  3. }
  4. /**
  5. * 类方法增强<BR>
  6. *
  7. * 对指定类的方法进行代码增强(将指定的原方法名改为$enhanced,同时复制原方法名进行代码注入)
  8. * @param className 待增强的类名
  9. * @param methodName 待增强的方法名
  10. * @param injectType {@link InjectType}注入类型
  11. * @param provider {@link ClassInjectProvider}实现类
  12. * @throws Exception
  13. */
  14. public static void enhancedMethod(String className,String[] methods,
  15. InjectType injectType,
  16. ClassInjectProvider provider)throws Exception{
  17. CtClass ctClass = ClassPool.getDefault().get(className);
  18. for(int i=0;i<methods.length;i++){
  19. injectCodeForMethod(ctClass,methods[i],injectType,provider);
  20. }
  21. String resource = className.replace(".", "/") + ".class";
  22. URI uri = ClassLoader.getSystemClassLoader().getResource(resource).toURI();
  23. String classFilePath = uri.getRawPath().substring(0,uri.getRawPath().length() - resource.length());
  24. ctClass.writeFile(classFilePath);
  25. }
  26. private static void injectCodeForMethod(CtClass ctClass,String methodName,InjectType injectType,ClassInjectProvider provider)throws Exception{
  27. CtMethod oldMethod = ctClass.getDeclaredMethod(methodName);
  28. //修改原有的方法名称为"方法名$enhanced",如果已存在该方法则返回
  29. String originalMethod = methodName + "$enhanced";
  30. CtMethod[] methods=ctClass.getMethods();
  31. for(int i=0;i<methods.length;i++){
  32. CtMethod method=methods[i];
  33. if(method.getName().equals(originalMethod)){
  34. return ;
  35. }
  36. }
  37. oldMethod.setName(originalMethod);
  38. //增加代码,复制原来的方法名作为增强的新方法,同时调用原有方法即"方法名$enhanced"
  39. CtMethod enhancedMethod = CtNewMethod.copy(oldMethod, methodName, ctClass, null);
  40. //对复制的方法注入代码
  41. StringBuffer methodBody = new StringBuffer();
  42. methodBody.append("{");
  43. switch(injectType){
  44. case AFTER:
  45. methodBody.append(provider.injectCode(enhancedMethod));
  46. methodBody.append(originalMethod + "($$); ");
  47. break;
  48. case BEFORE:
  49. methodBody.append(originalMethod + "($$); ");
  50. methodBody.append(provider.injectCode(enhancedMethod));
  51. break;
  52. default:
  53. String injectCode=provider.injectCode(enhancedMethod);
  54. methodBody.append(injectCode);
  55. }
  56. methodBody.append("}");
  57. enhancedMethod.setBody(methodBody.toString());
  58. ctClass.addMethod(enhancedMethod);
  59. }
  60. }

 代码注入Provider接口



  1. public interface ClassInjectProvider {
  2. /**
  3. * 对指定的方法注入代码
  4. *
  5. * @param ctMethod CtMethod
  6. * @return
  7. */
  8. public String injectCode(final CtMethod ctMethod)throws Exception;
  9. }


缓存注入的实现类:


  1. public class CacheInjectProvider implements ClassInjectProvider{
  2. private static MyLogger logger=new MyLogger(CacheInjectProvider.class);
  3. public CacheInjectProvider(){
  4. }
  5. /**
  6. * 注入缓存,缓存键值以类名+#+方法名(参数值1...参数值n)为key
  7. *
  8. * @param method CtMethod
  9. */
  10. public String injectCode(final CtMethod method) throws Exception{
  11. StringBuffer cacheCode=new StringBuffer();
  12. try{
  13. if(method.getReturnType()==CtClass.voidType){
  14. cacheCode.append(method.getName()+"$enhanced($$); ");
  15. }
  16. else{
  17. cacheCode.append("StringBuilder cacheKeyBuilder=new StringBuilder(); ");
  18. cacheCode.append("MethodCacheKey cacheKey=new MethodCacheKey(); ");
  19. cacheCode.append("cacheKey.setClassName("").append(method.getDeclaringClass().getName()).append(""); ");
  20. cacheCode.append("cacheKey.setMethodName("").append(method.getName()).append(""); ");
  21. CtClass[] ctClass=method.getParameterTypes();
  22. cacheCode.append("Object[] parameters=new Object[").append(ctClass.length).append("]; ");
  23. for(int i=0;i<ctClass.length;i++){
  24. cacheCode.append("parameters["+i+"]=").append("($w)$"+(i+1)).append("; ");
  25. }
  26. cacheCode.append("cacheKey.setParameters(parameters); ");
  27. cacheCode.append("Cache cache=CacheFactory.getCache();").append(" ");
  28. cacheCode.append("if(cache.get(cacheKey)==null)").append(" ");
  29. cacheCode.append("{").append(" ");
  30. if(method.getReturnType().isPrimitive()){
  31. cacheCode.append("Object result=($w)").append(method.getName()+"$enhanced($$);").append(" ");
  32. }
  33. else{
  34. cacheCode.append("Object result=").append(method.getName()+"$enhanced($$);").append(" ");
  35. }
  36. cacheCode.append("cache.put(cacheKey,result);").append(" ");
  37. cacheCode.append("}").append(" ");
  38. cacheCode.append("return ").append("($r)cache.get(cacheKey);");
  39. }
  40. }catch(Exception e){
  41. }
  42. logger.log("inject sourcecode>>>: "+cacheCode.toString());
  43. return cacheCode.toString();
  44. }
  45. }


说明:
$1,$2 表示方法的第一个参数值,第二个参数值
$w 表示将指定的基本类型变量转换为对象,如($w)$1 即为Integer i=new Integer(1);
$$ 表示方法的所有参数
$r 方法的返回类型

缺点:
1)类重新编译后,字节码增强生成器要重新执行

这一点可以通过StartupRuntime 接口的方式在系统启动的时候运行指定方法,如

  1. public interface StartupRuntime {
  2. /**
  3. * 系统启动加载执行方法
  4. * @throws Throwable
  5. */
  6. public void execute()throws SystemRuntimeException;
  7. }


转自:http://gocom.primeton.com/blog3936_16703.htm


你可能感兴趣的:(javassist)