前言

java动态代理主要有2种,Jdk动态代理、Cglib动态代理,本文主要讲解Jdk动态代理的使用、运行机制、以及源码分析。当spring没有手动开启Cglib动态代理,即:<aop:aspectj-autoproxy proxy-target-class="true"/>@EnableAspectJAutoProxy(proxyTargetClass = true),默认使用的就是Jdk动态代理。动态代理的应用范围很广,例如:日志、事务管理、缓存等。本文将模拟@Cacheable,即缓存在动态代理中的应用进行讲解。需要注意的是,Jdk动态代理相比起cglib动态代理,Jdk动态代理的对象必须实现接口,否则将报错。我们也将带着这个问题在源码分析中寻找答案

当@Cacheable注解在方法上时

  1. 在方法执行前,将调用Jdk动态代理优先查找Redis(或其他缓存)
  2. 当缓存不存在时,执行方法,例如查询数据库
  3. 在方法执行后,再次调用Jdk动态代理,将结果缓存到Redis中

一、使用

步骤

  1. 创建接口UserService
  2. 创建接口实现类UserServiceImpl
  3. 创建Jdk动态代理JdkCacheHandler,用于增强UserServiceImpl方法前后的缓存逻辑

代码

  1. 创建接口UserService
public interface UserService {public String getUserByName(String name);
}
  1. 创建实现类UserServiceImpl
public class UserServiceImpl implements UserService {@Overridepublic String getUserByName(String name) {System.out.println("从数据库中查询到:" + name);return name;}
}
  1. 创建Jdk动态代理JdkCacheHandler
public class JdkCacheHandler implements InvocationHandler {// 目标类对象private Object target;// 获取目标类对象public JdkCacheHandler(Object target) {this.target = target;}// 创建JDK代理public Object createJDKProxy() {Class clazz = target.getClass();// 创建JDK代理需要3个参数,目标类加载器、目标类接口、代理类对象(即本身)return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("查找数据库前,在缓存中查找是否存在:" + args[0]);// 触发目标类方法Object result = method.invoke(target, args);System.out.printf("查找数据库后,将%s加入到缓存中\r\n", result);return result;}
}
  1. 创建测试类
public class JdkTest {@Testpublic void test() {UserService userService = new UserServiceImpl();JdkCacheHandler jdkCacheHandler = new JdkCacheHandler(userService);UserService proxy = (UserService) jdkCacheHandler.createJDKProxy();System.out.println("==========================");proxy.getUserByName("bugpool");System.out.println("==========================");System.out.println(proxy.getClass());}
}
  1. 输出
==========================
查找数据库前,在缓存中查找是否存在:bugpool
从数据库中查询到:bugpool
查找数据库后,将bugpool加入到缓存中
==========================
class com.sun.proxy.$Proxy4

二、调用机制

查看$Proxy代码

可以看到当经过Jdk动态代理以后,生产的proxy已经不再是UserService类型了,而是$Proxy4类型,想要了解其调用机制,得先获取到proxy类的代码

System.out.println(proxy.getClass());class com.sun.proxy.$Proxy4
  1. 修改JVM运行参数,添加-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

    修改JVM运行参数.png

    添加JVM运行参数.png

  2. 运行test即可在com.sun.proxy查看代码,此时生产的是class,idea打开会自动反编译

    Proxy代码.png

  3. 在上方输出中可以看到代理类是$Proxy4,至此获取到$Proxy4的源代码,接下去分析代理类的调用机制
public final class $Proxy4 extends Proxy implements UserService {private static Method m1;private static Method m3;private static Method m2;private static Method m0;public $Proxy4(InvocationHandler var1) throws  {super(var1);}public final boolean equals(Object var1) throws  {try {return (Boolean)super.h.invoke(this, m1, new Object[]{var1});} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);}}public final String getUserByName(String var1) throws  {try {return (String)super.h.invoke(this, m3, new Object[]{var1});} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);}}public final String toString() throws  {try {return (String)super.h.invoke(this, m2, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final int hashCode() throws  {try {return (Integer)super.h.invoke(this, m0, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}static {try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m3 = Class.forName("proxy.jdk.UserService").getMethod("getUserByName", Class.forName("java.lang.String"));m2 = Class.forName("java.lang.Object").getMethod("toString");m0 = Class.forName("java.lang.Object").getMethod("hashCode");} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(var2.getMessage());} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(var3.getMessage());}}
}

调用机制

  1. 从proxy调用开始
// JdkTest.java
proxy.getUserByName("bugpool");
  1. proxy是$Proxy4类型,因此进入$Proxy4的getUserByName方法
// $Proxy4.class
public final class $Proxy4 extends Proxy implements UserService {...// 构造器,传入JdkCacheHandler类的对象,正是下方调用的super.h属性public $Proxy4(InvocationHandler var1) throws  {super(var1);}public final String getUserByName(String var1) throws  {try {/***   调用父类的h属性的invoke方法*   在下面的源码分析中,会发现h属性正是JdkCacheHandler类createJDKProxy方法中所传入的this*   Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);*   而this指代的正是JdkCacheHandler类的对象,因此最后调用的是JdkCacheHandler的invoke方法*/return (String)super.h.invoke(this, m3, new Object[]{var1});} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);}}...static {...m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));m3 = Class.forName("proxy.jdk.UserService").getMethod("getUserByName", Class.forName("java.lang.String"));m2 = Class.forName("java.lang.Object").getMethod("toString");m0 = Class.forName("java.lang.Object").getMethod("hashCode");...}
}
  1. 因此h.invok实际调用的正是JdkCacheHandler类的invoke方法
// JdkCacheHandler.java
public class JdkCacheHandler implements InvocationHandler {...@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("查找数据库前,在缓存中查找是否存在:" + args[0]);// 触发目标类方法Object result = method.invoke(target, args);System.out.printf("查找数据库后,将%s加入到缓存中\r\n", result);return result;}
}
  1. method.invoke(target, args)中的method = getUserByName,target = 构造函数传进来的UserServiceImpl对象,args = "bugpool"
// UserServiceImpl.java
public class UserServiceImpl implements UserService {@Overridepublic String getUserByName(String name) {System.out.println("从数据库中查询到:" + name);return name;}
}

三、源码分析

原理

了解完Jdk动态代理的调用机制,所有核心问题都落在了$Proxy4类的对象proxy是如何生成的上面?即下面这句代码上,这里先给出概述,有利于宏观上看源码。在开始之前先复习一下java的运行机制:1. 所有.java文件经过编译生成.class文件 2. 通过类加载器classLoad将.class中的字节码加载到JVM中 3. 运行

// 创建JDK代理
public Object createJDKProxy() {Class clazz = target.getClass();// 创建JDK代理需要3个参数// 目标类加载器:用于加载生成的字节码// 目标类接口:用于生成字节码,也就是说$Proxy的生产仅仅需要接口数组就可以完成// 代理类对象(即本身):用于回调invoke方法,实现方法的增强return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
}
  1. 通过clazz.getInterfaces()获取到所有接口,通过接口可以生成类似以下字节码(注意以下给出的是代码),细细观察会发现其实各个接口方法生成的代码都是一样的,只有(String)super.h.invoke(this, m3, new Object[]{var1}的m3和参数有可能是不同的。所以其实想生成$Proxy字节码,只需要接口数组就已经完全足够了
public final String getUserByName(String var1) throws  {try {return (String)super.h.invoke(this, m3, new Object[]{var1});} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);}}
  1. 此时已经获取到$Proxy4.class的字节码,但是此处的字节码还未加载到JVM中,因此需要调用clazz.getClassLoader()传进来的类加载器进行加载,并得到对应的class,也就是$Proxy类
  2. 获取$Proxy类的构造函数,该构造函数有一个重要的参数h
  3. 通过反射调用$Proxy类的构造函数,cons.newInstance(new Object[]{h});构造函数的h正是传入的this,也就是JdkCacheHandler类的对象
  4. 将反射获取到的$Proxy对象放回

源码分析

  1. Proxy.newProxyInstance开始跟踪代码(注:①代表上方概述的步骤1)
// JdkCacheHandler.java
// 创建JDK代理
public Object createJDKProxy() {Class clazz = target.getClass();// 创建JDK代理需要3个参数,目标类加载器、目标类接口、代理类对象(即本身)return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
}
  1. 跟踪代码newProxyInstance,这里需要注意在①②过程结束后,③④过程调用前,即Class<?> cl = getProxyClass0(loader, intfs);结束后,cl变量一直都只是class,即$Proxy4类,并未生成对应的对象,这里不要混淆类和对象
// Proxy.java
// loader类加载器,interfaces目标类实现的所有接口,h即InvocationHandler类的对象
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException{// 校验InvocationHandler是否为空Objects.requireNonNull(h);// 该目标类实现的接口数组final Class<?>[] intfs = interfaces.clone();// 安全检查final SecurityManager sm = System.getSecurityManager();if (sm != null) {checkProxyAccess(Reflection.getCallerClass(), loader, intfs);}/** Look up or generate the designated proxy class.*/// 当缓存中存在代理类则直接获取,否则生成代理类// ①②步骤,核心代码,即生成代理类字节码以及加载都在这里进行Class<?> cl = getProxyClass0(loader, intfs);/** Invoke its constructor with the designated invocation handler.*/try {if (sm != null) {checkNewProxyPermission(Reflection.getCallerClass(), cl);}// ③ 从生成的代理类中获取构造函数// constructorParams = { InvocationHandler.class };final Constructor<?> cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;if (!Modifier.isPublic(cl.getModifiers())) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {cons.setAccessible(true);return null;}});}// ④ 调用构造函数,将InvocationHandler作为参数实例化代理对象return cons.newInstance(new Object[]{h});} catch (IllegalAccessException|InstantiationException e) {throw new InternalError(e.toString(), e);} catch (InvocationTargetException e) {Throwable t = e.getCause();if (t instanceof RuntimeException) {throw (RuntimeException) t;} else {throw new InternalError(t.toString(), t);}} catch (NoSuchMethodException e) {throw new InternalError(e.toString(), e);}}
  1. 跟踪Class<?> cl = getProxyClass0(loader, intfs);
// Proxy.java
private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {if (interfaces.length > 65535) {throw new IllegalArgumentException("interface limit exceeded");}// If the proxy class defined by the given loader implementing// the given interfaces exists, this will simply return the cached copy;// otherwise, it will create the proxy class via the ProxyClassFactory// 如果在类加载器中已经存在实现了对应接口的代理类,则直接返回缓存中的代理类// 否则,通过ProxyClassFactory新建代理类return proxyClassCache.get(loader, interfaces);
}
  1. 跟踪proxyClassCache.get(loader, interfaces);(注:Jdk动态代理对已经生成加载过的代理类进行了缓存以提高性能,缓存的相关代码不是我们关心的重点,可以跳过相关代码)本段代码我们主要关心V value = supplier.get();其中supplier本质是factory,通过new Factory(key, parameter, subKey, valuesMap)创建
// WeakCache.java
//K和P就是WeakCache定义中的泛型,key是类加载器,parameter是接口类数组
public V get(K key, P parameter) {// 检查接口数组是否为空Objects.requireNonNull(parameter);expungeStaleEntries();Object cacheKey = CacheKey.valueOf(key, refQueue);// lazily install the 2nd level valuesMap for the particular cacheKeyConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);if (valuesMap == null) {ConcurrentMap<Object, Supplier<V>> oldValuesMap= map.putIfAbsent(cacheKey,valuesMap = new ConcurrentHashMap<>());if (oldValuesMap != null) {valuesMap = oldValuesMap;}}// create subKey and retrieve the possible Supplier<V> stored by that// subKey from valuesMapObject subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));//通过sub-key得到supplier,实质就是factorySupplier<V> supplier = valuesMap.get(subKey);Factory factory = null;while (true) {if (supplier != null) {// supplier might be a Factory or a CacheValue<V> instance// ①②步骤都在这里,如果supplier不为空,则直接调用get方法返回代理类V value = supplier.get();if (value != null) {return value;}}// else no supplier in cache// or a supplier that returned null (could be a cleared CacheValue// or a Factory that wasn't successful in installing the CacheValue)// lazily construct a Factoryif (factory == null) {// 创建对应factory,此段代码在死循环中,下一次supplier.get()将会获取到代理类并退出循环factory = new Factory(key, parameter, subKey, valuesMap);}if (supplier == null) {supplier = valuesMap.putIfAbsent(subKey, factory);if (supplier == null) {// successfully installed Factory// 赋值给suppliersupplier = factory;}// else retry with winning supplier} else {if (valuesMap.replace(subKey, supplier, factory)) {// successfully replaced// cleared CacheEntry / unsuccessful Factory// with our Factorysupplier = factory;} else {// retry with current suppliersupplier = valuesMap.get(subKey);}}}}
  1. 跟踪V value = supplier.get();即Factory类的get方法,这里大部分的工作还是在做校验和缓存,我们只关心核心逻辑valueFactory.apply(key, parameter);其中valueFactory是上一个步骤传入的ProxyClassFactory
// Factory.java
public synchronized V get() { // serialize access// re-check// 再次检查,supplier是否是当前对象Supplier<V> supplier = valuesMap.get(subKey);if (supplier != this) {// something changed while we were waiting:// might be that we were replaced by a CacheValue// or were removed because of failure ->// return null to signal WeakCache.get() to retry// the loopreturn null;}// else still us (supplier == this)// create new valueV value = null;try {// valueFactory 是前序传进来的 new ProxyClassFactory()// ①②步骤,核心逻辑,调用valueFactory.apply生成对应代理类并加载value = Objects.requireNonNull(valueFactory.apply(key, parameter));} finally {if (value == null) { // remove us on failurevaluesMap.remove(subKey, this);}}// the only path to reach here is with non-null valueassert value != null;// wrap value with CacheValue (WeakReference)CacheValue<V> cacheValue = new CacheValue<>(value);// put into reverseMapreverseMap.put(cacheValue, Boolean.TRUE);// try replacing us with CacheValue (this should always succeed)if (!valuesMap.replace(subKey, this, cacheValue)) {throw new AssertionError("Should not reach here");}// successfully replaced us with new CacheValue -> return the value// wrapped by itreturn value;}
  1. 跟踪核心逻辑

    • Jdk动态代理通过拼凑的方式拼凑出$Proxy的全类名:com.sun.proxy.$proxy0.class
    • ③生产字节码byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags);可以看出Jdk动态代理需要interfaces接口数组进行生成字节码,这也是文章开头提出为什么必须实现接口的原因。同时从参数也可以看出需要生成字节码其实只需要接口数组,不需要其他信息。其实实现原理大概也可以猜出,Jdk动态代理通过遍历所有接口方法,为方法生成对应的return (String)super.h.invoke(this, m0~n, new Object[]{var1});代码
    • ④加载字节码:在③中获取到了字节码的字节数组,接下去就是调用classLoader将所有的字节码读入到JVM中
// ProxyClassFactory.java
private static final class ProxyClassFactoryimplements BiFunction<ClassLoader, Class<?>[], Class<?>>{// prefix for all proxy class names// 代理类名称前缀private static final String proxyClassNamePrefix = "$Proxy";// next number to use for generation of unique proxy class names// 代理类计数器private static final AtomicLong nextUniqueNumber = new AtomicLong();@Overridepublic Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);// 校验代理类接口for (Class<?> intf : interfaces) {/** Verify that the class loader resolves the name of this* interface to the same Class object.*/Class<?> interfaceClass = null;try {interfaceClass = Class.forName(intf.getName(), false, loader);} catch (ClassNotFoundException e) {}if (interfaceClass != intf) {throw new IllegalArgumentException(intf + " is not visible from class loader");}/** Verify that the Class object actually represents an* interface.*/if (!interfaceClass.isInterface()) {throw new IllegalArgumentException(interfaceClass.getName() + " is not an interface");}/** Verify that this interface is not a duplicate.*/if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {throw new IllegalArgumentException("repeated interface: " + interfaceClass.getName());}}// 代理类包名String proxyPkg = null;     // package to define proxy class inint accessFlags = Modifier.PUBLIC | Modifier.FINAL;/** Record the package of a non-public proxy interface so that the* proxy class will be defined in the same package.  Verify that* all non-public proxy interfaces are in the same package.*/// 当接口修饰符是public,则所有包都可以使用// 当接口是非public,则生成的代理类必须和接口在与非public接口同一个包下// 如果非public的接口均在同一个包下,则生成的代理类放在非public接口同一个包下// 而如果非public的接口存在多个,且在不同包下,则抛出异常for (Class<?> intf : interfaces) {int flags = intf.getModifiers();if (!Modifier.isPublic(flags)) {accessFlags = Modifier.FINAL;String name = intf.getName();int n = name.lastIndexOf('.');String pkg = ((n == -1) ? "" : name.substring(0, n + 1));if (proxyPkg == null) {proxyPkg = pkg;} else if (!pkg.equals(proxyPkg)) {throw new IllegalArgumentException("non-public interfaces from different packages");}}}if (proxyPkg == null) {// if no non-public proxy interfaces, use com.sun.proxy package// 如果都是公有的接口,则代理类默认放在com.sun.proxy packageproxyPkg = ReflectUtil.PROXY_PACKAGE + ".";}/** Choose a name for the proxy class to generate.*/// 生成计数器,例如$proxy0~nlong num = nextUniqueNumber.getAndIncrement();// 代理类名,com.sun.proxy.$proxy0.classString proxyName = proxyPkg + proxyClassNamePrefix + num;/** Generate the specified proxy class.*/// ③生成代理类字节码byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);try {// ④使用传进来的classLoader将代理类字节码加载到JVM中return defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);} catch (ClassFormatError e) {/** A ClassFormatError here means that (barring bugs in the* proxy class generation code) there was some other* invalid aspect of the arguments supplied to the proxy* class creation (such as virtual machine limitations* exceeded).*/throw new IllegalArgumentException(e.toString());}}}

本文首发于java黑洞网,csdn同步更新

Jdk动态代理 底层源码分析相关推荐

  1. JDK动态代理底层源码剖析

    1. 动态代理相关概念 目标类:程序员自己写的.普通的业务类,是需要被代理的类: 目标方法:目标类中的实现业务的具体方法,为了精简,只写核心业务代码,因此需要代理类来增强功能: 增强器:是给目标方法增 ...

  2. JDK 动态代理之源码解析

    过几天我会分享spring AOP 的相关的代理的源码,为了让大家学好springAOP ,今天先分析jdk 的动态代理 1.首先创建一个接口和一个被代理的对象: package com.nandao ...

  3. 动态代理原理源码分析

    看了这篇文章非常不错转载:https://www.jianshu.com/p/4e14dd223897 Java设计模式(14)----------动态代理原理源码分析 上篇文章<Java设计模 ...

  4. 【mybatis源码】 mybatis底层源码分析

    [mybatis源码] mybatis底层源码分析 1.测试用例 2.开撸源码 2.1 SqlSessionFactory对象的创建与获取 2.2 获取SqlSession对象 2.3 获取接口的代理 ...

  5. Thinkphp底层源码分析

    Thinkphp底层源码分析第一章 首先我们学习此章节的内容大家可能需要有一些基础才行,接着对PHP内置字符窜数组相关函数会用的比较多一点,当然不太熟悉的朋友,其实可以借助手册看下去.下面我们开始正题 ...

  6. ArrayList底层源码分析

    声明:本文为作者原创,请勿装载,如过转载,请注明转载地址 文章目录 ArrayList底层源码分析 1. 继承Serializable接口 2. 继承Cloneable接口 2.1 浅拷贝 2.2 深 ...

  7. idea 线程内存_Java线程池系列之-Java线程池底层源码分析系列(一)

    课程简介: 课程目标:通过本课程学习,深入理解Java线程池,提升自身技术能力与价值. 适用人群:具有Java多线程基础的人群,希望深入理解线程池底层原理的人群. 课程概述:多线程的异步执行方式,虽然 ...

  8. idea 线程内存_Java线程池系列之-Java线程池底层源码分析系列(二)

    课程简介: 课程目标:通过本课程学习,深入理解Java线程池,提升自身技术能力与价值. 适用人群:具有Java多线程基础的人群,希望深入理解线程池底层原理的人群. 课程概述:多线程的异步执行方式,虽然 ...

  9. 集合底层源码分析之HashMap《上》(三)

    集合底层源码分析之HashMap<上>(三) 前言 源码分析 HashMap主要属性及构造方法分析 tableSizeFor()方法源码分析 Node类源码分析 TreeNode类源码分析 ...

最新文章

  1. 域名管理系统 二级域名_域名系统简介
  2. 生命的礼赞,请记住我的名字,我叫科比-布莱恩特
  3. 解决centos6.5出现-bash: mysql: command not found的方法
  4. Ch4302-IntervalGCD【线段树,树状数组,GCD】
  5. Python——配置环境的导出与导入
  6. 整数、区间与区间端点(三)
  7. 驱动大师显示无法连接服务器,教你win10系统无法连接到nvidia服务器的解决教程...
  8. Django官方文档
  9. 蓝桥杯,基础练习 Fibonacci数列(斐波那契数列) C++
  10. 玩转#ChatGPT之“用Chat GPT 做出行攻略”
  11. 饿了么-T技术沙龙活动感悟。
  12. 数据库电话号码查询显示中间四位用****代替的SQL语句
  13. 使用 电报机器人 tele bot 远程执行服务器上的命令
  14. 新版本微信如何解绑手机号?
  15. 马毅教授讲座——反思深度学习:回归计算机视觉的挑战
  16. js html 渐变透明度,JavaScript动画之透明度渐变
  17. matlab粒子群加约束条件_matlab粒子群编程,等式约束如何加入
  18. WinRAR 使用说明
  19. 数据库审计系统-数据库安全审计工具
  20. qq空间微博等更多社交平台分享

热门文章

  1. SetTimer OnTimer WM_TIMER
  2. java 蓝桥杯算法提高 身份证号码升级(题解)
  3. java 标识符_java标识符是什么
  4. mysql高并发不用事务_Mysql高并发加锁事务处理
  5. 城乡投票源码php_响应式投票系统(支持微信、手机) php版 v3.2
  6. la是什么牌子_La Prairie
  7. php+mysql个人博客系统_推荐几个开源的个人独立博客系统
  8. (18)ISE14.7调试核名称与顶层名称不一致导致生成bit报error(FPGA不积跬步101)
  9. (190)FPGA变量初始化方法initial
  10. java printf与println_浅析Java中print、printf、println的区别