IT教程 ·

曹工说Spring Boot源码(19)– Spring 带给我们的东西利器,建立代办不必愁(ProxyFactory)

优雅地使用 C++ 制作表格:tabulate

写在前面的话

相干背景及资本:

工程结构图:

曹工说Spring Boot源码(19)-- Spring 带给我们的东西利器,建立代办不必愁(ProxyFactory) IT教程 第1张

提要

本篇是接着前三篇讲的,然则本篇相对自力,纵然不运用spring aop 和spring ioc,我们也能够应用本日要讲的ProxyFactory为我们所用。

前面几篇说到,spring怎样完成aop,行将婚配切点的bean,生成动态代办,并将生成的动态代办放到ioc容器,来替代本来的bean,一系列骚操纵,完成"代办换真身"的操纵。

jdk动态代办

比较老套的话题,然则,我问人人几个问题,看看人人是不是真的充足相识他呢?

在代办对象上,挪用不在接口中的要领

package foo;


public class Performer implements Perform {
    @Override
    public void sing() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("男孩在唱歌");

    }

    public void eat() {
        System.out.println("男孩在用饭");
    }
}

能够看到,我们sing是完成了接口中的要领,而eat不在接口中定义。

那末,以下代码,结果会是啥:

@Test
public void createJdkDynamicProxyManual() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    Object generatedProxy = Proxy.newProxyInstance(loader, new Class[]{Perform.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("proxy:" + proxy.getClass());
            return "hahh";
        }
    });
    Method eat = Perform.class.getMethod("eat");
    eat.setAccessible(true);
    eat.invoke(generatedProxy,null);
}

代码中,我们竖立了一个代办对象:generatedProxy;然后,挪用了其eat要领,结果会是啥呢?

java.lang.NoSuchMethodException: foo.Perform.eat()
at java.lang.Class.getMethod(Class.java:1665)
at java.lang.Class.getMethod(Class.java:1665)

为啥会如许呢?由于我们竖立代办对象时,是在Perform.class这个接口上竖立的。人人能够再细致看看。

jdk 动态代办(Proxy.newProxyInstance)有哪几个步骤

这个问题,有人思索过吗?简朴来讲,实在有3个步骤。

  1. 生成动态代办类的class,虽然不像其他class文件那样,是编译了就有的,这里,是动态生成的;
  2. 加载第一步拿到的字撙节,丢给jvm加载该class,拿到Class对象
  3. 依据第二步的Class对象,反射生成动态代办对象。

我刚细致看了Proxy.newProxyInstance的要领解释:

Returns an instance of a proxy class for the specified interfaces
that dispatches method invocations to the specified invocation
handler.  This method is equivalent to:

Proxy.getProxyClass(loader, interfaces).         // 对应步骤1和2
  getConstructor(new Class[] { InvocationHandler.class }).   // 对应步骤3
  newInstance(new Object[] { handler }); // 对应步骤3

个中,第一步,细问一下,class是怎样生成的,许多人预计又答不上了。我们这里就看一下:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    /*
     * Look up or generate the designated proxy class.
     */
    Class<?> cl = getProxyClass0(loader, interfaces);

    /*
     * Invoke its constructor with the designated invocation handler.
     */
    return newInstance(cons, ih);
}

能够看到,重要的猎取Class,是getProxyClass0要领,这个要领内里代码不少,去掉非中心的缓存等部份,中心的部份以下:

String proxyName = proxyPkg + "$Proxy" + num;

/*
 * Generate the specified proxy class.
 */
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
    proxyName, interfaces);  
proxyClass = defineClass0(loader, proxyName,
                          proxyClassFile, 0, proxyClassFile.length);

这个中,ProxyGenerator.generateProxyClass 担任生成class的字撙节,对应我们前面讲到的步骤1;defineClass0对应类加载。我们细致说说:

  1. 字撙节生成

    这部份呢,实在就是挪用了ProxyGenerator.generateProxyClass,我们跟踪发明,它的全名为:sun.misc.ProxyGenerator,是sun包下的。这部份没法看源码,还好我之前下载过openjdk的源码,这里我给人人全文贴一下:

     /**
         * Generate a class file for the proxy class.  This method drives the
         * class file generation process.
         */
        private byte[] generateClassFile() {
    
            /* ============================================================
             * Step 1: Assemble ProxyMethod objects for all methods to
             * generate proxy dispatching code for.
             */
    
            /*
             * Record that proxy methods are needed for the hashCode, equals,
             * and toString methods of java.lang.Object.  This is done before
             * the methods from the proxy interfaces so that the methods from
             * java.lang.Object take precedence over duplicate methods in the
             * proxy interfaces.
             */
            addProxyMethod(hashCodeMethod, Object.class);
            addProxyMethod(equalsMethod, Object.class);
            addProxyMethod(toStringMethod, Object.class);
    
            /*
             * Now record all of the methods from the proxy interfaces, giving
             * earlier interfaces precedence over later ones with duplicate
             * methods.
             */
            for (int i = 0; i < interfaces.length; i++) {
                Method[] methods = interfaces[i].getMethods();
                for (int j = 0; j < methods.length; j++) {
                    addProxyMethod(methods[j], interfaces[i]);
                }
            }
    
            /*
             * For each set of proxy methods with the same signature,
             * verify that the methods' return types are compatible.
             */
            for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
                checkReturnTypes(sigmethods);
            }
    
            /* ============================================================
             * Step 2: Assemble FieldInfo and MethodInfo structs for all of
             * fields and methods in the class we are generating.
             */
            try {
                methods.add(generateConstructor());
    
                for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
                    for (ProxyMethod pm : sigmethods) {
    
                        // add static field for method's Method object
                        fields.add(new FieldInfo(pm.methodFieldName,
                            "Ljava/lang/reflect/Method;",
                             ACC_PRIVATE | ACC_STATIC));
    
                        // generate code for proxy method and add it
                        methods.add(pm.generateMethod());
                    }
                }
    
                methods.add(generateStaticInitializer());
    
            } catch (IOException e) {
                throw new InternalError("unexpected I/O Exception");
            }
    
            if (methods.size() > 65535) {
                throw new IllegalArgumentException("method limit exceeded");
            }
            if (fields.size() > 65535) {
                throw new IllegalArgumentException("field limit exceeded");
            }
    
            /* ============================================================
             * Step 3: Write the final class file.
             */
    
            /*
             * Make sure that constant pool indexes are reserved for the
             * following items before starting to write the final class file.
             */
            cp.getClass(dotToSlash(className));
            cp.getClass(superclassName);
            for (int i = 0; i < interfaces.length; i++) {
                cp.getClass(dotToSlash(interfaces[i].getName()));
            }
    
            /*
             * Disallow new constant pool additions beyond this point, since
             * we are about to write the final constant pool table.
             */
            cp.setReadOnly();
    
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            DataOutputStream dout = new DataOutputStream(bout);
    
            try {
                /*
                 * Write all the items of the "ClassFile" structure.
                 * See JVMS section 4.1.
                 */
                                            // u4 magic;
                dout.writeInt(0xCAFEBABE);
                                            // u2 minor_version;
                dout.writeShort(CLASSFILE_MINOR_VERSION);
                                            // u2 major_version;
                dout.writeShort(CLASSFILE_MAJOR_VERSION);
    
                cp.write(dout);             // (write constant pool)
    
                                            // u2 access_flags;
                dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
                                            // u2 this_class;
                dout.writeShort(cp.getClass(dotToSlash(className)));
                                            // u2 super_class;
                dout.writeShort(cp.getClass(superclassName));
    
                                            // u2 interfaces_count;
                dout.writeShort(interfaces.length);
                                            // u2 interfaces[interfaces_count];
                for (int i = 0; i < interfaces.length; i++) {
                    dout.writeShort(cp.getClass(
                        dotToSlash(interfaces[i].getName())));
                }
    
                                            // u2 fields_count;
                dout.writeShort(fields.size());
                                            // field_info fields[fields_count];
                for (FieldInfo f : fields) {
                    f.write(dout);
                }
    
                                            // u2 methods_count;
                dout.writeShort(methods.size());
                                            // method_info methods[methods_count];
                for (MethodInfo m : methods) {
                    m.write(dout);
                }
    
                                             // u2 attributes_count;
                dout.writeShort(0); // (no ClassFile attributes for proxy classes)
    
            } catch (IOException e) {
                throw new InternalError("unexpected I/O Exception");
            }
    
            return bout.toByteArray();
        }

    这里实际上是有面试题的,我之前还被问过,问我用的什么手艺来生成class字撙节,这里实际上是没有用任何第三方东西的,这个类的import语句部份,也没有asm、javaasist等东西。

    import java.io.ByteArrayOutputStream;
    import java.io.DataOutputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.lang.reflect.Array;
    import java.lang.reflect.Method;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.ListIterator;
    import java.util.Map;
    import sun.security.action.GetBooleanAction;
    1. 加载class字撙节为Class

      这部份的代码即为前面提到的:

      try {
          proxyClass = defineClass0(loader, proxyName,
              proxyClassFile, 0, proxyClassFile.length);
      }

      个中,defineClass0 是一个native要领:

      java.lang.reflect.Proxy#defineClass0
      private static native Class defineClass0(ClassLoader loader, String name,
                                               byte[] b, int off, int len);

      让我比较惊奇的是,这个native要领,是在Proxy类里,且除了此处的挪用,没有被其他代码挪用。

      我去看了Classloader这个类的代码,内里也有几个native的defineClass的要领:

      private native Class defineClass0(String name, byte[] b, int off, int len,
                                        ProtectionDomain pd);
      
      private native Class defineClass1(String name, byte[] b, int off, int len,
                                        ProtectionDomain pd, String source);
      
      private native Class defineClass2(String name, java.nio.ByteBuffer b,
                                        int off, int len, ProtectionDomain pd,
                                        String source);

      看来,Proxy是本身自立门户啊,没有运用Classloader类下面的defineClass等要领。

      假如人人想看生成的class的文件的内容,能够加这个虚拟机启动参数:

      -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

      或许main最前面,加这个:

      System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

    2. 反射生成代办对象

      这步就没啥好说的了,经由上面第二步,已拿到Class对象了。反射关于人人,也是驾轻就熟了。

      Constructor<?> cons = cl.getConstructor({ InvocationHandler.class });
      final InvocationHandler ih = h;
      newInstance(cons, ih);
      
      private static Object newInstance(Constructor<?> cons, InvocationHandler h) {
          return cons.newInstance(new Object[] {h} );
      }

      这里,我们看到,猎取的组织函数,就是要吸收一个InvocationHandler对象的。拿到了组织函数后,接下来,就挪用了组织函数的newInstance,来生成代办对象。

      详细的挪用就不说了,横竖你挪用的任何要领(只能挪用接口里有的那些),都邑转到invocationHandler的invoke要领。

关于jdk动态代办的思索

实在,人人看到上面,会想下面这个问题不?现在在代办对象上,挪用要领,终究都邑进入到:

 java.lang.reflect.InvocationHandler#invoke
 public Object invoke(Object proxy, Method method, Object[] args)
     throws Throwable;

我假如想在这个逻辑内里,去挪用原始目的的要领,怎样办呢?

我们看看传给我们的几个参数:

1. proxy,代办对象;这个没办法拿到原始对象
2. method,是被挪用的要领,也拿不到原始对象
3. args,给method的参数,也拿不到原始对象。

这就迷离了。那我咋办呢?

答案是,在竖立InvocationHandler时,把原始对象传进去,以及其他统统必要的信息,都传进去。

固然,你也能够不传进去,在invoke要领里,随心所欲,比以下面的要领:

  ClassLoader loader = Thread.currentThread().getContextClassLoader();
  Object generatedProxy = Proxy.newProxyInstance(loader, new Class[]{Perform.class}, new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          System.out.println("到我这为止,不会挪用target了");
          return null;
      }
  });
  // 这里,虽然挪用了sing,但内里的逻辑也不会实行。
  ((Perform)generatedProxy).sing();

实在,这个代办,已相称因而Perform接口的另一个完成了;和之前的完成类,没有半毛钱关联。

假如要让它实行代办的事情,能够如许做:

  @Test
  public  void createJdkDynamicProxyManual() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
      Performer performer = new Performer();
      MyCustomInvocationHandler myCustomInvocationHandler = new MyCustomInvocationHandler(performer);
  
      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      Object generatedProxy = Proxy.newProxyInstance(loader,
              new Class[]{Perform.class}, myCustomInvocationHandler);
  
      ((Perform)generatedProxy).sing();
  
  }
  
  public static class MyCustomInvocationHandler implements InvocationHandler {
      Performer performer;
  
      public MyCustomInvocationHandler(Performer performer) {
          this.performer = performer;
      }
  
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          System.out.println("我是一个称职的代办");
          return method.invoke(performer,args);
      }
  }

上面这个代码,就没问题了。会输出以下:

我是一个称职的代办

男孩在唱歌

jdk动态代办完成思绪的案例代码

我们上面说了怎样准确地完成代办的思绪,就是要把target/原始bean,在new invocationHandler的时刻,通报给它,后续在invoke里再运用。我们看看框架对invocationHandler的其他完成,是怎样做的吧?

我在project里找了下InvocationHandler的完成类,发明了jdbc中的一个完成类。

org.springframework.jdbc.datasource.ConnectionProxy

public interface ConnectionProxy extends Connection {

   /**
    * Return the target Connection of this proxy.
    * <p>This will typically be the native driver Connection
    * or a wrapper from a connection pool.
    * @return the underlying Connection (never {@code null})
    */
   Connection getTargetConnection();

}

这个是Connection的子接口,经由历程这个接口,猎取真正的数据库衔接。我们看看其代办完成:

org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy#getConnection(java.lang.String, java.lang.String)
public Connection getConnection(String username, String password) throws SQLException {
   return (Connection) Proxy.newProxyInstance(
         ConnectionProxy.class.getClassLoader(),
         new Class[] {ConnectionProxy.class},
         new LazyConnectionInvocationHandler(username, password));
}

这个代办完成,重如果耽误猎取数据库衔接,比及运用的时刻,才去猎取衔接;而不是启动时,即竖立衔接池。

private class LazyConnectionInvocationHandler implements InvocationHandler {

   private String username;

   private String password;

   private Boolean readOnly = Boolean.FALSE;

   private Integer transactionIsolation;

   private Boolean autoCommit;

   private boolean closed = false;

   private Connection target;

   public LazyConnectionInvocationHandler() {
      this.autoCommit = defaultAutoCommit();
      this.transactionIsolation = defaultTransactionIsolation();
   }

   public LazyConnectionInvocationHandler(String username, String password) {
      this();
      this.username = username;
      this.password = password;
   }

   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // Invocation on ConnectionProxy interface coming in...

      if (method.getName().equals("equals")) {
         // We must avoid fetching a target Connection for "equals".
         // Only consider equal when proxies are identical.
         return (proxy == args[0]);
      }
      ...
      else if (method.getName().equals("getTargetConnection")) {
         // Handle getTargetConnection method: return underlying connection.
         return getTargetConnection(method);
      }
      ...
   }
   
   

这里呢,假如要领为getTargetConnection,则挪用了以下要领:

private Connection getTargetConnection(Method operation) throws SQLException {
      if (this.target == null) {
         // No target Connection held -> fetch one.
         if (logger.isDebugEnabled()) {
            logger.debug("Connecting to database for operation '" + operation.getName() + "'");
         }

         // 经由历程用户名,暗码去猎取数据库衔接
         this.target = (this.username != null) ?
               getTargetDataSource().getConnection(this.username, this.password) :
               getTargetDataSource().getConnection();

         // If we still lack default connection properties, check them now.
         checkDefaultConnectionProperties(this.target);

         // Apply kept transaction settings, if any.
         if (this.readOnly) {
            try {
               this.target.setReadOnly(this.readOnly);
            }
            catch (Exception ex) {
               // "read-only not supported" -> ignore, it's just a hint anyway
               logger.debug("Could not set JDBC Connection read-only", ex);
            }
         }
         if (this.transactionIsolation != null &&
               !this.transactionIsolation.equals(defaultTransactionIsolation())) {
            this.target.setTransactionIsolation(this.transactionIsolation);
         }
         if (this.autoCommit != null && this.autoCommit != this.target.getAutoCommit()) {
            this.target.setAutoCommit(this.autoCommit);
         }
      }

      return this.target;
   }
}

人人从上面代码,能够看到,是有经由历程用户名暗码去猎取数据库衔接的。

所以,看来,准确的完成代办的思绪就是,在组织proxy的时刻,把你须要的东西,都经由历程组织函数或setter,通报给invocationHandler。然后再在invoke要领内去运用这些东西,来完成你的逻辑。

Spring供应给我们的壮大东西类:ProxyFactory

人人看了上面,以为生成代办,简朴,照样庞杂呢?或许还不是很难。但假如是运用cglib的体式格局去竖立代办,代码可就要多好一些了。(这个留到背面讲)

实在,spring里给我们供应了神器的,即我们要说的:ProxyFactory。其解释以下,意义是,aop代办工场,不必经由历程bean factory,能够直接运用。这个类供应一个简朴的猎取和设置aop代办的体式格局。

* Factory for AOP proxies for programmatic use, rather than via a bean
* factory. This class provides a simple way of obtaining and configuring
* AOP proxies in code.

意义是,我们日常平凡,完成aop,重要依托spring的aop,即经由历程注解或许xml的体式格局,声明式地竖立aop(比方设置事件时)。这里的意义是,我们能够经由历程代码体式格局来完成一样的结果,即,竖立代办。

人人把这个类,明白为代办工场即可,工场嘛,就是给它东西,它给你返回产物,这个产物,就是代办对象。

怎样应用ProxyFactory竖立代办

我们看看,把它当做黑盒的话,怎样应用它,来简化我们竖立代办的历程:

 @Test
    public void createJdkDynamicProxy() {
        ProxyFactory proxyFactory = new ProxyFactory();
//        Performer performer = new Performer();
//        proxyFactory.setTarget(performer);

        proxyFactory.addInterface(Perform.class);

        Perform proxy = (Perform) proxyFactory.getProxy();

        log.info("proxy class:{}",proxy.getClass().getName());
        proxy.sing();
        log.info("proxy:{}",proxy);
    }

一般情况下,根据我们前面临jdk动态代办的明白,上面如许就够了。然则,上面代码会报错,说没有指定target 对象。所以,我们实际上,须要把上面那两行解释给摊开,不然报以下毛病。


org.springframework.aop.framework.AopConfigException: No advisors and no TargetSource specified

    at org.springframework.aop.framework.JdkDynamicAopProxy.<init>(JdkDynamicAopProxy.java:103)
    at org.springframework.aop.framework.DefaultAopProxyFactory.createAopProxy(DefaultAopProxyFactory.java:65)
    at org.springframework.aop.framework.ProxyCreatorSupport.createAopProxy(ProxyCreatorSupport.java:105)
    at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:98)
    at ProxyFactoryTest.createJdkDynamicProxy(ProxyFactoryTest.java:44)

上面摊开谁人解释代码后,默许就会去挪用target的对应要领,会有以下输出:

2020-02-25 08:32:29.828 [main] INFO  ProxyFactoryTest - proxy class:com.sun.proxy.$Proxy5
男孩在唱歌
2020-02-25 08:32:30.910 [main] INFO  ProxyFactoryTest - proxy:foo.Performer@502775a1

怎样竖立代办的同时,织入切面

我们上面只是竖立了代办,默许去挪用了target的对应要领,假定我们要切一下,怎样办?

不慌!

@Test
    public void createJdkDynamicProxyWithAdvisor() {
        ProxyFactory proxyFactory = new ProxyFactory();
        Performer performer = new Performer();
        proxyFactory.setTarget(performer);

        proxyFactory.addInterface(Perform.class);
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setAdvice(new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                Object result = invocation.proceed();
                System.out.println("男孩唱完要施礼");
                return result;
            }
        });

        proxyFactory.addAdvisor(advisor);

        Perform proxy = (Perform) proxyFactory.getProxy();

        ProxyFactoryTest.log.info("proxy class:{}",proxy.getClass().getName());
        proxy.sing();
    }

这里的重点代码就是:

        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setAdvice(new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                Object result = invocation.proceed();
                System.out.println("男孩唱完要施礼");
                return result;
            }
        });

        proxyFactory.addAdvisor(advisor);

这上面的几行代码,重如果竖立了一个advisor,一个advisor 险些即是切点+关照。

advisor的setAdvice呢,重要接收一个Advice范例的参数。而MethodInterceptor就是它的子接口。

曹工说Spring Boot源码(19)-- Spring 带给我们的东西利器,建立代办不必愁(ProxyFactory) IT教程 第2张

固然了,实在advice的完成许多,包含spring里都有许多内部完成。我这里找了一个,对要领实行耗时,举行监测的。

我把上面的代码改动了一行:

advisor.setAdvice(new PerformanceMonitorInterceptor());

这个类,的继承关联以下:

重要功能就是纪录耗时,此时,输出以下:

2020-02-25 08:40:06.825 [main] INFO ProxyFactoryTest - proxy class:com.sun.proxy.$Proxy5
男孩在唱歌
2020-02-25 08:40:07.868 [main] TRACE o.s.aop.interceptor.PerformanceMonitorInterceptor - StopWatch 'foo.Perform.sing': running time (millis) = 1006

总结

本日也许讲了jdk动态代办的道理,和ProxyFactory的运用。下一讲,继承aop之旅,重要解说ProxyFactory的道理。

论文翻译:Speech Enhancement Based on the General Transfer Function GSC and Postfiltering

参与评论