曹工说Spring Boot源码(19)– Spring 带给我们的东西利器,建立代办不必愁(ProxyFactory)
优雅地使用 C++ 制作表格:tabulate
写在前面的话
相干背景及资本:
工程结构图:
提要
本篇是接着前三篇讲的,然则本篇相对自力,纵然不运用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个步骤。
- 生成动态代办类的class,虽然不像其他class文件那样,是编译了就有的,这里,是动态生成的;
- 加载第一步拿到的字撙节,丢给jvm加载该class,拿到Class对象
- 依据第二步的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对应类加载。我们细致说说:
- 字撙节生成
这部份呢,实在就是挪用了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;
- 加载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");
- 反射生成代办对象
这步就没啥好说的了,经由上面第二步,已拿到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要领。
- 加载class字撙节为Class
关于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就是它的子接口。
固然了,实在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