修改原有的方法名称(字节码增强)

通常对一个方法增加日志记录,安全检查都会说采用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. }

[java] view plaincopyprint?

代码注入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

时间: 2024-10-19 12:03:15

修改原有的方法名称(字节码增强)的相关文章

字节码增强技术探索

1.字节码 1.1 什么是字节码? Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统.平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用.因此,也可以看出字节码对于Java生态的重要性.之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取.在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图1所示. 图1 Java运

JVM——字节码增强技术简介

Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改.Java字节码增强主要是为了减少冗余代码,提高性能等. 实现字节码增强的主要步骤为: 1.修改字节码 在内存中获取到原来的字节码,然后通过一些工具(如 ASM,Javaasist)来修改它的byte[]数组,得到一个新的byte数组. 2.使修改后的字节码生效 有两种方法: 1) 自定义ClassLoader来加载修改后的字节码: 2)替换掉原来的字节码:在JVM加载用户的C

javaAgent和Java字节码增强技术的学习与实践

参考文章: https://www.cnblogs.com/chiangchou/p/javassist.html https://blog.csdn.net/u010039929/article/details/62881743 https://www.jianshu.com/p/0f64779cdcea [本文代码下载] 下载代码 [背景] 最近在工作中进行程序的性能调优时,想起之前同事的介绍的阿里的Java在线诊断工具 —— arthas,决定试用一下. 这玩意,是真的好用,能在对被检测程

JVM字节码增强

JVM——字节码增强技术简介 Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改.Java字节码增强主要是为了减少冗余代码,提高性能等. 实现字节码增强的主要步骤为: 1.修改字节码 在内存中获取到原来的字节码,然后通过一些工具(如 ASM,Javaasist)来修改它的byte[]数组,得到一个新的byte数组. 2.使修改后的字节码生效 有两种方法: 1) 自定义ClassLoader来加载修改后的字节码: 2)替换掉原来

字节码增强

之前看了美团技术团队推送的一篇文章,介绍了字节码增强技术,可的很好,自己也记录一下,增强一下记忆,也方便日后巩固学习,有兴趣的可以去搜索美团技术团队的原文 字节码是JVM的底层基础知识,如果能够掌握对于排查问题会有更深层次的理解 1.什么是字节码 首先我们看看什么是字节码,找到一个.class文件,看看长什么样子 Java之所以可以一次编译,到处运行,首先是因为JVM针对各种操作系统和平台都进行了定制,二是无论在什么平台,都可以通过javac命令将一个.java文件编译成固定格式的字节码(.cl

Javassist字节码增强示例

概述 Javassist是一款字节码编辑工具,可以直接编辑和生成Java生成的字节码,以达到对.class文件进行动态修改的效果.熟练使用这套工具,可以让Java编程更接近与动态语言编程. 下面一个方法的目的是获取一个类加载器(ClassLoader),以加载指定的.jar或.class文件,在之后的代码中会使用到. [java] view plaincopy private static ClassLoader getLocaleClassLoader() throws Exception {

逆水行舟 —— jdk动态代理和Cglib字节码增强

JDK动态代理 利用拦截器加上反射机制生成一个实现代理接口的匿名类,在调用具体方法时,调用InvocationHandler来处理 JDK动态代理只需要JDK环境就可以进行代理,流程为: 实现InvocationHandler 使用Proxy.newProxyInstance产生代理对象 被代理的对象必须实现接口 具体列子如下: public class UserServiceImpl implements UserService { @Override public void eat() {

Jpa/Hibernate 字节码增强:字段延迟加载

JPA提供了@Basic注解,实现延迟加载字段的功能,如下: @Basic(fetch = FetchType.LAZY) @Column(name = "REMARK_CONTENT" ) private String remarkContent; 但是实际上延迟加是不是起作用的,依然能够出这个字段的数据. 为了延迟加载生效,需要使用字节码增加的插件: 在pom文件中配置: <plugin> <groupId>org.hibernate.orm.tooling

WCF中修改接口或方法名称而不影响客户端程序

本篇接着"从Web Service和Remoting Service引出WCF服务"中有关WCF的部分. 运行宿主应用程序. 运行Web客户端中的网页. 输入内容,点击按钮,能获取到WCF所提供的服务. 现在,WCF的接口如下: namespace HelloWcf { // 注意: 使用"重构"菜单上的"重命名"命令,可以同时更改代码和配置文件中的接口名"IFirstWcf". [ServiceContract] publi