网站首页 > 开源技术 正文
原文作者: 码匠笔记
前言
因为 MyBatis 的易上手性和可控性,使得它成为了 ORM框架中的首选。近日新起了一个项目,所以重新搭建了一下 Spring-mybatis, 下面是搭建笔记和从 Spring-mybatis源码分析其如何使用 Java动态代理,希望对大家有帮助。
Spring 集成 Mybatis
Spring 集成 Mybatis的方式有很多种,大家耳熟能详的 xml配置方式或者本文的采用的方式:
首先需要添加 MyBatis的和 MyBatis-Spring的依赖,本文使用的 Spring-mybatis版本是1.3.1。在 mvnrepository里面我们可以找到当前 Spring-mybatis依赖的 spring和 mybatis版本,最好是选择匹配的版本以避免处理不必要的兼容性问题。因为 MyBatis-Spring中对 mybatis的依赖选择了 provided模式,所以我们不得不额外添加 mybatis依赖,依赖配置如下。
<dependency> <groupId> org.mybatis </groupId> <artifactId> mybatis-spring </artifactId> <version> 1.3.1 </version> </dependency> <dependency> <groupId> org.mybatis </groupId> <artifactId> mybatis </artifactId> <version> 3.4.1 </version> </dependency>
接下来会我们要创建工厂bean,放置下面的代码在 Spring 的 XML 配置文件中:
<bean id = "sqlSessionFactory" class = "org.mybatis.spring.SqlSessionFactoryBean" > <property name = "dataSource" ref = "dataSource" /> </bean>
这个工厂需要一个 DataSource,就是我们熟知的数据源了。这里我们选择了阿里的 Druid,同样我们需要引入两个配置
<dependency> <groupId> mysql </groupId> <artifactId> mysql-connector-java </artifactId> <version> 5.1.41 </version> </dependency> <dependency> <groupId> com.alibaba </groupId> <artifactId> druid </artifactId> <version> 1.1.2 </version> </dependency>
添加 Spring配置如下
<bean id = "dataSource" class = "com.alibaba.druid.pool.DruidDataSource" init-method = "init" destroy-method = "close" > <!-- 基本属性 url、user、password --> <property name = "url" > <value> <![CDATA[${db.url}]]> </value> </property> <property name = "username" value = "${db.username}" /> <property name = "password" value = "${db.password}" /> <!-- 省略其他配置 --> </bean>
接下来我们要编写数据库访问对象,大多数人会把它叫做 DAO或者 Repository,在这里其被称为
Mapper,也是因为它的实现方式所决定。要注意的是所指定的映射器类必须是一个接口,而不是具体的实现类。这便因为 Mybatis的内部实现使用的是 Java动态代理,而 Java动态代理只支持接口,关于 动态代理我们下文有更详细的描述。
public interface UserMapper { @Select ( "SELECT * FROM users WHERE id = #{userId}" ) User getUser ( @Param ( "userId" ) String userId ); }
接下来可以使用 MapperFactoryBean,像下面这样来把接口加入到 Spring 中,这样就把 UserMapper 和 SessionFactory关联到一起了,原来使用 xml配置的时候还需要Dao继承 SqlSessionDaoSupport才能注入 SessionFactory,这种方式直接通过 Java动态代理把 SqlSessionFactory代理给了 UserMapper,使得我们直接使用 UserMapper即可。配置如下。
<bean id = "userMapper" class = "org.mybatis.spring.mapper.MapperFactoryBean" > <property name = "mapperInterface" value = "org.mybatis.spring.sample.mapper.UserMapper" /> <property name = "sqlSessionFactory" ref = "sqlSessionFactory" /> </bean>
这样我们已经完成了90%,就差调用了,前提是你 Spring环境是OK的。调用 MyBatis数据方法现在只需一行代码:
public class FooServiceImpl implements FooService { private UserMapper userMapper ; public void setUserMapper ( UserMapper userMapper ) { this . userMapper = userMapper ; } public User doSomeBusinessStuff ( String userId ) { return this . userMapper . getUser ( userId ); }
那么问题又来了,每次写一个DAO都需要为其写一个 Bean配置,那不是累死?于是我们又寻找另一种方案,代替手动声明 *Mapper。 MapperScannerConfigurer的出现解决了这个问题, 它会根据你配置的包路径自动的扫描类文件并自动将它们创建成 MapperFactoryBean,可以在 Spring 的配置中添加如下代码:
<bean class = "org.mybatis.spring.mapper.MapperScannerConfigurer" > <property name = "basePackage" value = "com.github.codedrinker.mapper" /> </bean>
basePackage属性是让你为映射器接口文件设置基本的包路径。你可以使用分号或逗号作为分隔符设置多于一个的包路径。这个时候如果想自定义 sqlSessionFactory可以添加如下配置:
<property name = "sqlSessionFactoryBeanName" value = "sqlSessionFactory" />
这样以后还有一点点小瑕疵,如果我们数据的 column名字是 _连接的,那么它不会那么聪明自动转换为驼峰的变量,所以我们需要对 SqlSessionFactoryBean做如下配置,但是在1.3.0以后才可以通过xml配置,如果用早起版本的需要注意了。
<bean id = "sqlSessionFactory" class = "org.mybatis.spring.SqlSessionFactoryBean" > <property name = "dataSource" ref = "dataSource" /> <property name = "configuration" > <bean class = "org.apache.ibatis.session.Configuration" > <property name = "mapUnderscoreToCamelCase" value = "true" /> </bean> </property> </bean>
至此关于 SpringMyBatis的配置已经全部结束,后面我们会简单说下 SpringMyBatis中的动态代理。
浅析 Java 动态代理
JDK自带的动态代理需要了解InvocationHandler接口和Proxy类,他们都是在java.lang.reflect包下。
InvocationHandler是代理实例的调用处理程序实现的接口。每个代理实例都具有一个关联的 InvocationHandler。对代理实例调用方法时,这个方法会调用 InvocationHandler的 invoke方法。 Proxy提供静态方法用于创建动态代理类和实例,同时后面自动生成的代理类都是 Proxy对象。下面我们直接通过代码来分析 Java动态代理: InvocationInterceptor实现 InvocationHandler接口,用于处理具体的代理逻辑。
/** * Created by codedrinker on 12/10/2017. */ public class InvocationInterceptor implements InvocationHandler { private Object target ; public InvocationInterceptor ( Object target ) { this . target = target ; } @Override public Object invoke ( Object proxy , Method method , Object [] args ) throws Throwable { System . out . println ( "before user create" ); method . invoke ( target , args ); System . out . println ( "end user create" ); return null ; } }
User和 UserImpl是被代理对象的接口和类
/** * Created by codedrinker on 12/10/2017. */ public interface User { void create (); }
/** * Created by codedrinker on 12/10/2017. */ public class UserImpl implements User { @Override public void create () { System . out . println ( "create user" ); } }
DynamicProxyTest是测试类,用于创建 InvocationInterceptor和 Proxy类以便测试。
/** * Created by codedrinker on 12/10/2017. */ public class DynamicProxyTest { public static void main ( String [] args ) { User target = new UserImpl (); InvocationInterceptor invocationInterceptor = new InvocationInterceptor ( target ); User proxyInstance = ( User ) Proxy . newProxyInstance ( UserImpl . class . getClassLoader (), UserImpl . class . getInterfaces (), invocationInterceptor ); proxyInstance . create (); } }
输入结果如下:
before user create create user end user create
很明显,我们通过proxyInstance这个代理类进行方法调用的时候,会在方法调用前后进行输出打印,这样就简单的实现了一个 Java动态代理例子。动态代理不仅仅是打印输出这么简单,我们可以通过它打印日志,打开关闭事务, 权限检查了等等。当然它更是许多框架的钟爱,就如下文我们要说的 MyBatis中 Java动态代理的实现。再多说一句 Spring的 AOP也是使用动态代理实现的,当然它同时使用了 Java动态代理和 CGLib两种方式。不过 CGLIB不是本文要讨论的范围。
注意观察的同学看到上面代码的时候可能发现 invoke方法的 proxy参数并没有被使用,笔者查阅了一些相关文档也没有找到合理的说法,只能在源码中看看究竟喽,笔者当前的JDK版本是1.8。我们从入口开始, Proxy.newProxyInstance:
/* * Look up or generate the designated proxy class. */ @CallerSensitive public static Object newProxyInstance ( ClassLoader loader , Class <?>[] interfaces , InvocationHandler h ) throws IllegalArgumentException { Class <?> cl = getProxyClass0 ( loader , intfs ); }
如上代码由此可见,它调用了 getProxyClass0来获取 ProxyClass,那我们继续往下看。
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 return proxyClassCache . get ( loader , interfaces ); }
其实上面写的已经很简单了,如果存在就在 proxyClassCache里面获取到,如果不存在就使用 ProxyClassFactory创建一个。当然我们如果看一下 proxyClassCache变量的话其也是 ProxyClassFactory对象。
private static final WeakCache < ClassLoader , Class <?>[], Class <?>> proxyClassCache = new WeakCache <>( new KeyFactory (), new ProxyClassFactory ());
那么我们直接就去查看 ProxyClassFactory的实现问题不就解决了吗?
private static final class ProxyClassFactory implements 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 (); @Override public Class <?> apply ( ClassLoader loader , Class <?>[] interfaces ) { String proxyName = proxyPkg + proxyClassNamePrefix + num ; /* * Generate the specified proxy class. */ byte [] proxyClassFile = ProxyGenerator . generateProxyClass ( proxyName , interfaces , accessFlags ); } }
由上代码便一目了然了,为什么我们 Debug的时候 Proxy对象是 $Proxy0,是因为他通过 $Proxy和 AtomicLong拼起来的类名,其实这不是重点。重点是 ProxyGenerator.generateProxyClass(proxyName,interfaces,accessFlags)。这就是生成 class的地方,它把所有的条件组合好,生成 class文件,然后再加载到内存里面以供使用。有兴趣的同学可以继续往深处查看。而我们需要做的是获取到他生成的字节码,看一下里面到底是什么?当 saveGeneratedFiles为 true的时候会保存 class文件,所以我们在 DynamicProxyTest的 main函数添加一行即可:
System . setProperty ( "sun.misc.ProxyGenerator.saveGeneratedFiles" , "true" );
通过 Debug我们可以发现,它存储 class文件的路径是 com/sun/proxy/$Proxy0.class,所以直接在我们项目的目录下面就能找到它,然后通过 Idea打开便得到如下代码:
public final class $Proxy0 extends Proxy implements User { private static Method m1 ; private static Method m2 ; private static Method m3 ; private static Method m0 ; public $Proxy0 ( InvocationHandler var1 ) throws { super ( var1 ); } public final boolean equals ( Object var1 ) throws { try { return (( Boolean ) super . h . invoke ( this , m1 , new Object []{ var1 })). booleanValue (); } 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 void create () throws { try { super . h . invoke ( this , m3 , ( 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 )). intValue (); } catch ( RuntimeException | Error var2 ) { throw var2 ; } catch ( Throwable var3 ) { throw new UndeclaredThrowableException ( var3 ); } } static { try { m1 = Class . forName ( "java.lang.Object" ). getMethod ( "equals" , new Class []{ Class . forName ( "java.lang.Object" )}); m2 = Class . forName ( "java.lang.Object" ). getMethod ( "toString" , new Class [ 0 ]); m3 = Class . forName ( "local.dynimicproxy.User" ). getMethod ( "create" , new Class [ 0 ]); m0 = Class . forName ( "java.lang.Object" ). getMethod ( "hashCode" , new Class [ 0 ]); } catch ( NoSuchMethodException var2 ) { throw new NoSuchMethodError ( var2 . getMessage ()); } catch ( ClassNotFoundException var3 ) { throw new NoClassDefFoundError ( var3 . getMessage ()); } } }
这样好多问题就迎刃而解。
为什么 Java动态代理必须是接口,因为生成的类要去实现这个接口。
invoke方法的 proxy是干嘛的,通过 super.h.invoke(this,m3,(Object[])null);我们可以发现传递给 invoke方法的就是 Proxy本身。
同时 Proxy类也通过反射实现了 toString, equals,和 hashcode等方法。
自此关于 Java动态代理的讲解已经告段落,下面让我们简单看一下 Spring-mybatis中关于 Java动态代理的使用。
Java动态代理在Spring-mybatis中的实现
关于 Spring-mybatis的实现我们得从 MapperScannerConfigurer说起,首先 MapperScannerConfigurer实现了 BeanDefinitionRegistryPostProcessor接口。而 BeanDefinitionRegistryPostProcessor依赖于 Spring框架,简单的说 BeanDefinitionRegistryPostProcessor使得我们可以将 BeanDefinition添加到 BeanDefinitionRegistry中,而 BeanDefinition描述了一个Bean实例所拥有的实例、结构参数和参数值,简单点说拥有它就可以实例化 Bean了。 BeanDefinitionRegistryPostProcessor的 postProcessBeanDefinitionRegistry方法在 Bean被定义但还没被创建的时候执行,所以 Spring-mybatis也是借助了这一点。需要想需要更深入的了解可以查看 Spring的生命周期。
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor , InitializingBean , ApplicationContextAware , BeanNameAware { /** * {@inheritDoc} * * @since 1.0.2 */ @Override public void postProcessBeanDefinitionRegistry ( BeanDefinitionRegistry registry ) { ClassPathMapperScanner scanner = new ClassPathMapperScanner ( registry ); scanner . scan ( StringUtils . tokenizeToStringArray ( this . basePackage , ConfigurableApplicationContext . CONFIG_LOCATION_DELIMITERS )); }
由上代码我们可以看到在 postProcessBeanDefinitionRegistry里面得到 registry然后使用 ClassPathMapperScanner开始扫描包路径得到的 Bean并且注册到 registry里面。我们接着往里面看。
@Override public Set < BeanDefinitionHolder > doScan ( String ... basePackages ) { Set < BeanDefinitionHolder > beanDefinitions = super . doScan ( basePackages ); if ( beanDefinitions . isEmpty ()) { logger . warn ( "No MyBatis mapper was found in '" + Arrays . toString ( basePackages ) + "' package. Please check your configuration." ); } else { processBeanDefinitions ( beanDefinitions ); } return beanDefinitions ; }
ClassPathMapperScanner继承了 Spring的 ClassPathBeanDefinitionScanner所以调用父类的 doScan方法就可以加载 Bean然后再通过 processBeanDefinitions方法加工成 MyBatis需要的 Bean。
private void processBeanDefinitions ( Set < BeanDefinitionHolder > beanDefinitions ) { GenericBeanDefinition definition ; for ( BeanDefinitionHolder holder : beanDefinitions ) { definition = ( GenericBeanDefinition ) holder . getBeanDefinition (); definition . setBeanClass ( this . mapperFactoryBean . getClass ()); } }
如上代码循环了所有由 Spring容器解析出来的 beanDefinitions然后把他们的 BeanClass修改为 mapperFactoryBean,这就进入了行文的重点。我们翻看到 MapperFactoryBean:
@Override protected void checkDaoConfig () { super . checkDaoConfig (); notNull ( this . mapperInterface , "Property 'mapperInterface' is required" ); Configuration configuration = getSqlSession (). getConfiguration (); if ( this . addToConfig && ! configuration . hasMapper ( this . mapperInterface )) { try { configuration . addMapper ( this . mapperInterface ); } catch ( Exception e ) { logger . error ( "Error while adding the mapper '" + this . mapperInterface + "' to configuration." , e ); throw new IllegalArgumentException ( e ); } finally { ErrorContext . instance (). reset (); } } }
其调用了 Configuration的 addMapper方法,这样就把 Bean交给 MyBatis管理了。那么 checkDaoConfig是什么时候调用的呢?我们翻看其父类 DaoSupport可以看到:
public abstract class DaoSupport implements InitializingBean { @Override public final void afterPropertiesSet () throws IllegalArgumentException , BeanInitializationException { checkDaoConfig (); } }
因为 DaoSupport实现了 InitializingBean并重写 afterPropertiesSet方法,了解 Spring生命周期的同学知道 afterPropertiesSet方法会在资源加载完以后,初始化bean之前执行。我们继续查看 addMapper方法。
public < T > void addMapper ( Class < T > type ) { if ( type . isInterface ()) { if ( hasMapper ( type )) { throw new BindingException ( "Type " + type + " is already known to the MapperRegistry." ); } boolean loadCompleted = false ; try { knownMappers . put ( type , new MapperProxyFactory < T >( type )); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. MapperAnnotationBuilder parser = new MapperAnnotationBuilder ( config , type ); parser . parse (); loadCompleted = true ; } finally { if (! loadCompleted ) { knownMappers . remove ( type ); } } } }
addMapper方法最终创建了 MapperProxyFactory对象,在 MapperProxyFactory里面我们两眼泪汪汪地发现了似曾相识的代码:
protected T newInstance ( MapperProxy < T > mapperProxy ) { return ( T ) Proxy . newProxyInstance ( mapperInterface . getClassLoader (), new Class [] { mapperInterface }, mapperProxy ); } public T newInstance ( SqlSession sqlSession ) { final MapperProxy < T > mapperProxy = new MapperProxy < T >( sqlSession , mapperInterface , methodCache ); return newInstance ( mapperProxy ); }
而 MapperProxy实现了 InvocationHandler方法,最终实现对 Bean的代理,同时获取到上下文的 sqlSession以供使用。具体生成过程我们不再累述,直接通过其源码结束本篇文章:
public class MapperProxy < T > implements InvocationHandler , Serializable { private static final long serialVersionUID = - 6424540398559729838L ; private final SqlSession sqlSession ; private final Class < T > mapperInterface ; private final Map < Method , MapperMethod > methodCache ; public MapperProxy ( SqlSession sqlSession , Class < T > mapperInterface , Map < Method , MapperMethod > methodCache ) { this . sqlSession = sqlSession ; this . mapperInterface = mapperInterface ; this . methodCache = methodCache ; } @Override public Object invoke ( Object proxy , Method method , Object [] args ) throws Throwable { try { if ( Object . class . equals ( method . getDeclaringClass ())) { return method . invoke ( this , args ); } else if ( isDefaultMethod ( method )) { return invokeDefaultMethod ( proxy , method , args ); } } catch ( Throwable t ) { throw ExceptionUtil . unwrapThrowable ( t ); } final MapperMethod mapperMethod = cachedMapperMethod ( method ); return mapperMethod . execute ( sqlSession , args ); } }
最后,我自己是一名从事了多年开发的Java老程序员,辞职目前在做自己的Java私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的Java学习干货,可以送给每一位喜欢Java的小伙伴,想要获取的可以关注我的头条号并在后台私信我:01,即可免费获取。
- 上一篇: 在实际操作过程中如何避免出现SQL注入漏洞
- 下一篇: 腾讯Go安全指南
猜你喜欢
- 2024-11-24 腾讯Go安全指南
- 2024-11-24 在实际操作过程中如何避免出现SQL注入漏洞
- 2024-11-24 2024为什么前端都应该掌握Nextjs 或 Nuxtjs
- 2024-11-24 Python3环境安装Scrapy爬虫框架过程
- 2024-11-24 如何避免出现SQL注入漏洞
- 2024-11-24 开发安全Java应用程序的13条规则
- 2024-11-24 聊一聊影响网络安全的SQL注入
- 2024-11-24 72 个网络应用安全实操要点,全方位保护 Web 应用的安全
- 2024-11-24 Cursor使用指南:释放AI编程的无限潜能
- 2024-11-24 如何避免出现 SQL 注入漏洞
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)