【1】MyBatis是一个持久层的ORM框架【Object Relational Mapping,对象关系映射】,使用简单,学习成本较低。可以执行自己手写的SQL语句,比较灵活。但是MyBatis的自动化程度不高,移植性也不高,有时从一个数据库迁移到另外一个数据库的时候需要自己修改配置,所以称只为半自动ORM框架。
【1】简单使用
@Test public void test() throws SQLException {Connection conn=null;PreparedStatement pstmt=null;try {// 1.加载驱动,其实这一步可以不加因为DriverManager里面会有自动加载驱动的一步Class.forName("com.mysql.jdbc.Driver");// 2.创建连接conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis_example", "root", "123456");//开启事务conn.setAutoCommit(false);// SQL语句String sql="select id,user_name,create_time from t_user where id=? ";// 获得sql执行者pstmt=conn.prepareStatement(sql);pstmt.setInt(1,1);// 执行查询//ResultSet rs= pstmt.executeQuery();pstmt.execute();ResultSet rs= pstmt.getResultSet();rs.next();User user =new User();user.setId(rs.getLong("id"));user.setUserName(rs.getString("user_name"));user.setCreateTime(rs.getDate("create_time"));System.out.println(user.toString());} catch (Exception e) {e.printStackTrace();}finally{// 关闭资源try {if(conn!=null){conn.close();}if(pstmt!=null){pstmt.close();}} catch (SQLException e) {e.printStackTrace();}} }
【1.1】DriverManager如何自动加载驱动【利用静态代码块调用初始化方法,至于为什么要使用SPI机制,主要是为了实现解耦和可插拔,因为驱动有多种】
注:驱动展示【位于mysql-connector-java-5.1.22.jar/META-INF/services/java.sql.Driver 内容为:com.mysql.jdbc.Driver】
static {// 启动类加载器加载DriverManager类,触发静态方法执行loadInitialDrivers();println("JDBC DriverManager initialized"); }private static void loadInitialDrivers() {String drivers;try {drivers = AccessController.doPrivileged(new PrivilegedAction() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// 加载java.sql.Driver驱动的实现AccessController.doPrivileged(new PrivilegedAction () {public Void run() {// 1、创建一个 ServiceLoader对象,【这里就将上下文类加载器设置到ServiceLoader对象的变量上了】ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);// 2、创建一个迭代器对象Iterator driversIterator = loadedDrivers.iterator();try{// 3、这里调用driversIterator.hasNext()的时候,触发将 META-INF/services 下的配置文件中的数据读取进来,方便下面的next方法使用while(driversIterator.hasNext()) {// 4、【关键】:触发上面创建的迭代器对象的方法调用。这里才是具体加载的实现逻辑,非常不好找driversIterator.next();}} catch(Throwable t) {}return null;}});//判断有没有加载到if (drivers == null || drivers.equals("")) {return;}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());} catch (Exception ex) {...}} }//ServiceLoader类#hasNext方法 public boolean hasNext() {if (acc == null) {return hasNextService();} else {PrivilegedAction action = new PrivilegedAction () {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc);} }private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {// 1、拼凑要读取的文件的全名// final String PREFIX = "META-INF/services/";String fullName = PREFIX + service.getName();// 2、根据 fullName 去到META-INF/services/目录下寻找配置文件// 如果类加载器为空,则使用系统类加载器,如果不为空则使用指定的类加载器if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}// 3、使用parse方法解析配置文件中的每一行数据pending = parse(service, configs.nextElement());}nextName = pending.next();return true; }
【2】当然正常情况一般是封装成类来使用的,如
//数据访问基类 public class BaseDao {// 驱动private static String DRIVER = null;// 链接字符串private static String URL = null;// 用户名private static String USERNAME = null;// 密码private static String PASSWORD = null;//初始化static { init(); }// 初始化private static void init() {try {// 使用Properties对象读取资源文件属性Properties pro = new Properties();// 获得资源文件输入流InputStream inStream = BaseDao.class.getClassLoader().getResourceAsStream("db.properties");// 加载输入流pro.load(inStream);DRIVER = pro.getProperty("mysql.driverClass");URL = pro.getProperty("mysql.jdbcUrl");USERNAME = pro.getProperty("mysql.user");PASSWORD = pro.getProperty("mysql.password");Class.forName(DRIVER);} catch (Exception e) {e.printStackTrace();}}//获取数据库连接对象protected Connection getConnection() {Connection conn = null;try {conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}return conn;}/*** 关闭所有链接* @param conn* @param stmt* @param rs*/protected void CloseAll(Connection conn, Statement stmt, ResultSet rs) {try {if (conn != null) {conn.close();}if (stmt != null) {stmt.close();}if (rs != null) {rs.close();}} catch (SQLException e) {// TODO Auto-generated catch blocke.printStackTrace();}}/*** 执行 增、删、 改的公共方法* @param sql SQL语句* @param prams 参数* @return 受影响的行数*/protected int executeUpdate(String sql, Object... prams) {// 获得数据库链接对象Connection conn = getConnection();// 声明SQL执行者PreparedStatement pstmt = null;try {// 获得SQL执行者pstmt = conn.prepareStatement(sql);// 循环加载参数for (int i = 0; i < prams.length; i++) {pstmt.setObject(i + 1, prams[i]);}// 执行executeUpdate 返回受影响行数return pstmt.executeUpdate();} catch (SQLException e) {e.printStackTrace();} finally {// 关闭所有需要关闭的对象CloseAll(conn, pstmt, null);}return 0;}/*** 执行查询 返回单个值* @param sql SQL语句* @param prams 参数* @return OBJECT*/protected Object executeScaler(String sql, Object... prams) {// 获得数据库链接对象Connection conn = getConnection();// 声明SQL执行者PreparedStatement pstmt = null;// 声明查询结果集ResultSet rs = null;// 接收单个值Object value = null;try {// 获得SQL执行者pstmt = conn.prepareStatement(sql);// 循环加载参数for (int i = 0; i < prams.length; i++) {pstmt.setObject(i + 1, prams[i]);}// 执行executeUpdate 返回受影响行数rs = pstmt.executeQuery();if (rs.next()) {value = rs.getObject(1);}} catch (SQLException e) {e.printStackTrace();} finally {CloseAll(conn, pstmt, rs);}return value;}/*** 执行查询返回list* * @param sql SQL语句* @param clazz 类的类型* @return List*/publicList executeList(String sql, Class clazz, Object... prams) {// 数据集合List list = new ArrayList ();// 获得数据库连接Connection conn = getConnection();// 声明SQL执行者PreparedStatement pstmt = null;// 声明查询结果集ResultSet rs = null;try {// 3. 通过链接创建一个SQL执行者pstmt = conn.prepareStatement(sql);// 循环加载参数for (int i = 0; i < prams.length; i++) {//内部会通过instance of判断这个参数到底是哪个类型的具体对象pstmt.setObject(i + 1, prams[i]);}// 4 执行查询SQL 返回查询结果rs = pstmt.executeQuery();// 获得结果集的列信息对象ResultSetMetaData rsmd = rs.getMetaData();while (rs.next()) {// 通过类反射实例化T obj = clazz.newInstance();// 循环所有的列for (int i = 1; i <= rsmd.getColumnCount(); i++) {/* 通过属性名称使用反射给泛型实例赋值 Begin */// 获得每一列的列名String cloName = rsmd.getColumnName(i);// 根据列名反射到类的字段Field filed = clazz.getDeclaredField(cloName);// 设置私有属性的访问权限filed.setAccessible(true);// 给泛型实例的某一个属性赋值filed.set(obj, rs.getObject(cloName));/* 通过属性名称使用反射给泛型实例赋值 End */}// 将泛型实例添加到 泛型集合中list.add(obj);}} catch (Exception e) {e.printStackTrace();}return list;}/*** 执行查询返回JavaBean* * @param sql SQL语句* @param clazz 类的类型* @return JavaBean*/public T executeJavaBean(String sql, Class clazz, Object... prams) {// 声明数据对象T obj=null;// 获得数据库连接Connection conn = getConnection();// 声明SQL执行者PreparedStatement pstmt = null;// 声明查询结果集ResultSet rs = null;try {// 3. 通过链接创建一个SQL执行者pstmt = conn.prepareStatement(sql);// 循环加载参数for (int i = 0; i < prams.length; i++) {pstmt.setObject(i + 1, prams[i]);}// 4 执行查询SQL 返回查询结果rs = pstmt.executeQuery();// 获得结果集的列信息对象ResultSetMetaData rsmd = rs.getMetaData();if (rs.next()) {// 通过类反射实例化obj = clazz.newInstance();// 循环所有的列for (int i = 1; i <= rsmd.getColumnCount(); i++) {/* 通过属性名称使用反射给泛型实例赋值 Begin */// 获得每一列的列名String cloName = rsmd.getColumnName(i);// 根据列名反射到类的字段Field filed = clazz.getDeclaredField(cloName);// 设置私有属性的访问权限filed.setAccessible(true);// 给泛型实例的某一个属性赋值filed.set(obj, rs.getObject(cloName));/* 通过属性名称使用反射给泛型实例赋值 End */} }} catch (Exception e) {e.printStackTrace();}return obj;} }
【3】总结JDBC的四大核心对象
1)DriverManager(驱动管理对象):获取数据库连接; 2)Connection(数据库连接对象):获取执行sql对象;管理事务; 3)Statement(执行sql对象):executeUpdate执行DML语句(增删改)DDL语句;executeQuery 执行DQL语句; 4)ResultSet(结果集对象):
【4】传统JDBC的问题
1)数据库连接创建,释放频繁造成西戎资源的浪费,从而影响系统性能,使用数据库连接池可以解决问题。 2)sql语句在代码中硬编码,造成代码的不已维护,实际应用中sql的变化可能较大,sql代码和java代码没有分离开来维护不方便。 3)使用preparedStatement向有占位符传递参数存在硬编码问题因为sql中的where子句的条件不确定,同样是修改不方便。 4)对结果集中解析存在硬编码问题,sql的变化导致解析代码的变化,系统维护不方便。
【5】针对问题的优化
1、数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。 优化部分,如mybatis:在SqlMapConfig.xml中配置数据连接池,使用连接池管理数据库链接。2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。 优化部分,如mybatis:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 优化部分,如mybatis:自动将java对象映射至sql语句,通过statement中的parameterType定义输入参数的类型。4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。 优化部分,如mybatis:自动将sql执行结果映射至java对象,通过statement中的resultType定义输出结果的类型。
【1】图示
【2】分析
把Mybatis的功能架构分为三层: 1)基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。 对应示例中的部分为: SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);2)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。 对应示例中的部分为: SqlSession session = sqlMapper.openSession();3)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。 对应示例中的部分为: User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1); 或者 UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.selectById(1L);
【3】简单示例
public static void main(String[] args) {String resource = "mybatis-config.xml";Reader reader;try {//将XML配置文件构建为Configuration配置类reader = Resources.getResourceAsReader(resource);// 通过加载配置文件流构建一个SqlSessionFactory DefaultSqlSessionFactorySqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);// 数据源 执行器 DefaultSqlSessionSqlSession session = sqlMapper.openSession();try {// 执行查询 底层执行jdbc//User user = (User)session.selectOne("com.mapper.UserMapper.selectById", 1);UserMapper mapper = session.getMapper(UserMapper.class);User user = mapper.selectById(1L);session.commit();System.out.println(user.getUserName());} catch (Exception e) {e.printStackTrace();}finally {session.close();}} catch (IOException e) {e.printStackTrace();} }
【1】插件主要作用于四⼤组件对象
1)执⾏器 Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 等⽅法) 2)参数处理器 ParameterHandler (getParameterObject, setParameters 等⽅法) 3)结果集处理器 ResultSetHandler (handleResultSets, handleOutputParameters 等⽅法) 4)SQL语法构建处理器 StatementHandler (prepare, parameterize, batch, update, query 等⽅法)
【1】分析怎么通过加载配置文件流构建一个SqlSessionFactory
public SqlSessionFactory build(Reader reader) {return build(reader, null, null);}public SqlSessionFactory build(Reader reader, String environment, Properties properties) {try {XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);return build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);} finally {ErrorContext.instance().reset();try {reader.close();} catch (IOException e) {// Intentionally ignore. Prefer previous error.}} }// 到这里配置文件已经解析成了Configuration public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config); }
【1.1】分析XMLConfigBuilder类怎么将xml的资源文件流解析成Configuration:
【1.1.1】新建XMLConfigBuilder过程【所以说XMLConfigBuilder并不负责解析,解析的是它里面的XPathParser类,】
/** * 创建一个用于解析xml配置的构建器对象 * @param inputStream 传入进来的xml的配置 * @param environment 我们的环境变量 * @param props:用于保存我们从xml中解析出来的属性 */ public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {/*** 该方法做了二个事情* 第一件事情:创建XPathParser 解析器对象,在这里会把我们的* 把我们的mybatis-config.xml解析出一个Document对象* 第二节事情:调用重写的构造函数来构建我XMLConfigBuilder*/this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); }private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {//调用父类的BaseBuilder的构造方法:给configuration赋值,typeAliasRegistry别名注册器赋值,TypeHandlerRegistry赋值super(new Configuration());ErrorContext.instance().resource("SQL Mapper Configuration");//把props绑定到configuration的props属性上this.configuration.setVariables(props);this.parsed = false;this.environment = environment;this.parser = parser; }//又因为class XMLConfigBuilder extends BaseBuilder,所以看一下他的父类 public abstract class BaseBuilder {// mybatis的全局配置文件protected final Configuration configuration;// 用于保存我们的Entity的别名protected final TypeAliasRegistry typeAliasRegistry;// 用户保存我们java类型和jdbc数据库类型的protected final TypeHandlerRegistry typeHandlerRegistry;public BaseBuilder(Configuration configuration) {this.configuration = configuration;this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();}.... }
【1.1.2】parser.parse()解析过程【本质上就是从根结点开始解析】
public Configuration parse() {//若已经解析过了 就抛出异常if (parsed) {throw new BuilderException("Each XMLConfigBuilder can only be used once.");}//设置解析标志位,保证只解析一次parsed = true;/*** 解析我们的mybatis-config.xml的节点**/parseConfiguration(parser.evalNode("/configuration"));return configuration; }//方法实现说明:解析我们mybatis-config.xml的 configuration节点 private void parseConfiguration(XNode root) {try {// 解析 properties节点// 如: propertiesElement(root.evalNode("properties"));// 解析我们的mybatis-config.xml中的settings节点// 如: Properties settings = settingsAsProperties(root.evalNode("settings"));// 基本没有用过该属性// VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。// Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序loadCustomVfs(settings);// 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。// SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING// 解析到org.apache.ibatis.session.Configuration#logImplloadCustomLogImpl(settings);// 解析别名typeAliases节点typeAliasesElement(root.evalNode("typeAliases"));// 解析插件节点(比如分页插件),解析到 interceptorChain.interceptorspluginElement(root.evalNode("plugins"));objectFactoryElement(root.evalNode("objectFactory"));objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));reflectorFactoryElement(root.evalNode("reflectorFactory"));// 设置settings 到 configuration 里面settingsElement(settings);// 解析mybatis环境,解析到:org.apache.ibatis.session.Configuration#environment// 在集成spring情况下由 spring-mybatis提供数据源 和事务工厂environmentsElement(root.evalNode("environments"));// 解析数据库厂商databaseIdProviderElement(root.evalNode("databaseIdProvider"));// 解析类型处理器节点typeHandlerElement(root.evalNode("typeHandlers"));// 解析mapper节点(这个是最重要的)mapperElement(root.evalNode("mappers"));} catch (Exception e) {throw new BuilderException(...);} }
【1.1.2.1】展示xml配置文件与Mapper文件
【1.1.2.2】部分内容分析:
【1.1.2.2.1】解析 properties节点
// 本质上这个分离方式便是为了解耦 // 大体上看来加载properties有两种,一种是本地文件,一种是远程文件 // 而且加载出来的数据其实是会放置于configuration属性里面的,也就是XMLConfigBuilder类里面 private void propertiesElement(XNode context) throws Exception {if (context != null) {Properties defaults = context.getChildrenAsProperties();String resource = context.getStringAttribute("resource");String url = context.getStringAttribute("url");if (resource != null && url != null) {throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");}if (resource != null) {defaults.putAll(Resources.getResourceAsProperties(resource));} else if (url != null) {defaults.putAll(Resources.getUrlAsProperties(url));}Properties vars = configuration.getVariables();if (vars != null) {defaults.putAll(vars);}parser.setVariables(defaults);configuration.setVariables(defaults);} }//下面便展示一下db.properties的常用内容 mysql.driverClass=com.mysql.jdbc.Driver mysql.jdbcUrl=jdbc:mysql://localhost:3306/mybatis_example?characterEncoding=utf8 mysql.user= root mysql.password= 123456
【1.1.2.2.2】解析我们的mybatis-config.xml中的settings节点
private Properties settingsAsProperties(XNode context) {if (context == null) {return new Properties();}//分析配置的节点Properties props = context.getChildrenAsProperties();// 其实就是去configuration类里面拿到所有setter方法,看看有没有当前的配置项MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);for (Object key : props.keySet()) {if (!metaConfig.hasSetter(String.valueOf(key))) {throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");}}return props; }
【1.1.2.2.3】解析别名typeAliases节点
// 别名设置,别名设置最常用的就是对于entity【也就是实体类对象】 // 常常作用域Mapper的resultMap属性,也就是查询结果的映射,映射到对于的实体类上 // 由下面分析可以看出来是具备单个或者对包类路径的扫描 private void typeAliasesElement(XNode parent) {if (parent != null) {for (XNode child : parent.getChildren()) {if ("package".equals(child.getName())) {String typeAliasPackage = child.getStringAttribute("name");configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);} else {String alias = child.getStringAttribute("alias");String type = child.getStringAttribute("type");try {Class> clazz = Resources.classForName(type);if (alias == null) {typeAliasRegistry.registerAlias(clazz);} else {typeAliasRegistry.registerAlias(alias, clazz);}} catch (ClassNotFoundException e) {throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);}}}} }
【1.1.2.2.4】解析插件节点(比较重要)
// 对于插件的解析,很明显的有用到装饰器模式的概念,其实都是封装成了interceptor // 然后统一存放于configuration里面 private void pluginElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {String interceptor = child.getStringAttribute("interceptor");Properties properties = child.getChildrenAsProperties();Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();interceptorInstance.setProperties(properties);configuration.addInterceptor(interceptorInstance);}} }//实际上是封装成了这个类,因为Interceptor是接口,是不具备实例化能力的 @Intercepts({}) public class ExamplePlugin implements Interceptor {private Properties properties;@Overridepublic Object intercept(Invocation invocation) throws Throwable {return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {this.properties = properties;}public Properties getProperties() {return properties;}}
【1.1.2.2.5】解析mybatis环境
//这里面主要的就是事务工厂和数据源的设置 private void environmentsElement(XNode context) throws Exception {if (context != null) {if (environment == null) {environment = context.getStringAttribute("default");}for (XNode child : context.getChildren()) {String id = child.getStringAttribute("id");if (isSpecifiedEnvironment(id)) {TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));DataSource dataSource = dsFactory.getDataSource();Environment.Builder environmentBuilder = new Environment.Builder(id).transactionFactory(txFactory).dataSource(dataSource);configuration.setEnvironment(environmentBuilder.build());}}} }//展示一下一般可以在xml里面怎么配置//其实里面的JDBC和POOLED是怎么来的呢,都是源于别名那部分的,在Configuration类构造函数里面,他就会为别名里面注册一部分常用数据 public Configuration() {typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);typeAliasRegistry.registerAlias("FIFO", FifoCache.class);typeAliasRegistry.registerAlias("LRU", LruCache.class);typeAliasRegistry.registerAlias("SOFT", SoftCache.class);typeAliasRegistry.registerAlias("WEAK", WeakCache.class);typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);languageRegistry.register(RawLanguageDriver.class); }
【1.1.2.2.6】类型处理器解析
private void typeHandlerElement(XNode parent) {if (parent != null) {for (XNode child : parent.getChildren()) {// 根据包类路径注册if ("package".equals(child.getName())) {String typeHandlerPackage = child.getStringAttribute("name");typeHandlerRegistry.register(typeHandlerPackage);} else {//根据单个进行注册,属性有 handler(处理器) ,jdbcType(对应数据库类型) ,javaType(对应java类型)String javaTypeName = child.getStringAttribute("javaType");String jdbcTypeName = child.getStringAttribute("jdbcType");String handlerTypeName = child.getStringAttribute("handler");Class> javaTypeClass = resolveClass(javaTypeName);JdbcType jdbcType = resolveJdbcType(jdbcTypeName);Class> typeHandlerClass = resolveClass(handlerTypeName);if (javaTypeClass != null) {if (jdbcType == null) {typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);} else {typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);}} else {typeHandlerRegistry.register(typeHandlerClass);}}}} }
【1.1.2.2.7】最重要的mapper解析,明显是有四种类型:
private void mapperElement(XNode parent) throws Exception {if (parent != null) {// 获取我们mappers节点下的一个一个的mapper节点for (XNode child : parent.getChildren()) {// 判断我们mapper是不是通过批量注册的,如if ("package".equals(child.getName())) {String mapperPackage = child.getStringAttribute("name");configuration.addMappers(mapperPackage);} else {// 判断从classpath下读取我们的mapper,如 String resource = child.getStringAttribute("resource");// 判断是不是从我们的网络资源读取(或者本地磁盘得),如 String url = child.getStringAttribute("url");// 解析这种类型(要求接口和xml在同一个包下),如 String mapperClass = child.getStringAttribute("class");// 拿 这种进行分析if (resource != null && url == null && mapperClass == null) {ErrorContext.instance().resource(resource);// 把我们的文件读取出一个流InputStream inputStream = Resources.getResourceAsStream(resource);// 创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());// 真正的解析我们的mapper.xml配置文件(说白了就是来解析我们的sql) mapperParser.parse();} else if (resource == null && url != null && mapperClass == null) {// 这种从网络资源读取,与上面的大体一致就不解释了ErrorContext.instance().resource(url);InputStream inputStream = Resources.getUrlAsStream(url);XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());mapperParser.parse();} else if (resource == null && url == null && mapperClass != null) {Class> mapperInterface = Resources.classForName(mapperClass);configuration.addMapper(mapperInterface);} else {// 都不是则抛出异常throw new BuilderException(...);}}}} }
【1.1.2.2.7.1】分析addMapper方法最后是怎么解析的
// MapperRegistry类的两个属性值 // private final Configuration config; // private final Map, MapperProxyFactory>> knownMappers = new HashMap<>();//Configuration类#addMapper方法 // 传入包路径的方法 public void addMappers(String packageName) {mapperRegistry.addMappers(packageName); }public void addMappers(String packageName) {addMappers(packageName, Object.class); }//说白了也就是从包路径下面拿到所有的类,然后调用传入类的方法 public void addMappers(String packageName, Class> superType) {ResolverUtil > resolverUtil = new ResolverUtil<>();resolverUtil.find(new ResolverUtil.IsA(superType), packageName);Set >> mapperSet = resolverUtil.getClasses();for (Class> mapperClass : mapperSet) {// 调用的也是核心方法1addMapper(mapperClass);} }// 传入类的方法 public void addMapper(Class type) {mapperRegistry.addMapper(type); }// 核心方法1 // 方法实现说明:把我们的Mapper class保存到我们的knownMappers map 中 public void addMapper(Class type) {// 判断我们传入进来的type类型是不是接口if (type.isInterface()) {// 判断我们的缓存中有没有该类型if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {// 创建一个MapperProxyFactory 把我们的Mapper接口保存到工厂类中knownMappers.put(type, new MapperProxyFactory<>(type));// mapper注解构造器MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);// 进行解析parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}} }
【1.1.2.2.7.2】分析parse()方法怎么解析,其实有两种方式,一种是 XMLMapperBuilder类#parse方法,另一种是 MapperAnnotationBuilder类#parse方法
【1.1.2.2.7.2.1】分析XMLMapperBuilder类#parse方法
// XMLMapperBuilder类#parse方法 public void parse() {// 判断当前的Mapper是否被加载过if (!configuration.isResourceLoaded(resource)) {// 真正的解析我们的configurationElement(parser.evalNode("/mapper"));// 把资源保存到我们Configuration中configuration.addLoadedResource(resource);bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements(); }// 方法实现说明:解析我们的 节点 private void configurationElement(XNode context) {try {// 解析我们的namespace属性 String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.equals("")) {throw new BuilderException("Mapper's namespace cannot be empty");}// 保存我们当前的namespace 并且判断接口完全类名==namespacebuilderAssistant.setCurrentNamespace(namespace);// 解析缓存引用// 示例说明:说明当前的缓存引用和DeptMapper的缓存引用一致, // 解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>// 异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefscacheRefElement(context.evalNode("cache-ref"));// 解析cache节点,如 // 解析到:org.apache.ibatis.session.Configuration#caches// org.apache.ibatis.builder.MapperBuilderAssistant#currentCachecacheElement(context.evalNode("cache"));// 解析paramterMap节点(该节点mybaits3.5貌似不推荐使用了)parameterMapElement(context.evalNodes("/mapper/parameterMap"));// 解析resultMap节点// 解析到:org.apache.ibatis.session.Configuration#resultMaps// 异常 org.apache.ibatis.session.Configuration#incompleteResultMapsresultMapElements(context.evalNodes("/mapper/resultMap"));// 解析sql节点// 解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments,其实等于 org.apache.ibatis.session.Configuration#sqlFragments,因为他们是同一引用,在构建XMLMapperBuilder 时把Configuration.getSqlFragments传进去了sqlElement(context.evalNodes("/mapper/sql"));// 解析我们的select | insert |update |delete节点,解析到org.apache.ibatis.session.Configuration#mappedStatements// 实际上便是将每个语句都构建成mappedStatement对象buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} catch (Exception e) {throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);} }
【1.1.2.2.7.2.1.1】MappedStatement类结构展示
public final class MappedStatement {private String resource;//mapper配置文件名,如:UserMapper.xmlprivate Configuration configuration;//全局配置private String id;//节点的id属性加命名空间,如:com.lucky.mybatis.dao.UserMapper.selectByExampleprivate Integer fetchSize;private Integer timeout;//超时时间private StatementType statementType;//操作SQL的对象的类型private ResultSetType resultSetType;//结果类型private SqlSource sqlSource;//sql语句private Cache cache;//缓存private ParameterMap parameterMap;private ListresultMaps;private boolean flushCacheRequired;private boolean useCache;//是否使用缓存,默认为trueprivate boolean resultOrdered;//结果是否排序private SqlCommandType sqlCommandType;//sql语句的类型,如select、update、delete、insertprivate KeyGenerator keyGenerator;private String[] keyProperties;private String[] keyColumns;private boolean hasNestedResultMaps;private String databaseId;//数据库IDprivate Log statementLog;private LanguageDriver lang;private String[] resultSets; }
【1.1.2.2.7.2.1.2】解析cache节点部分cacheElement方法解析
//这里面便是二级缓存的产生 private void cacheElement(XNode context) {if (context != null) {//解析cache节点的type属性String type = context.getStringAttribute("type", "PERPETUAL");//根据type的String获取class类型Class extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);// 获取缓存过期策略:默认是LRU,// LRU - 最近最少使用:移除最长时间不被使用的对象(默认)// FIFO - 先进先出:按对象进入缓存的顺序来移除他们// SOFT - 软引用:基于垃圾回收器状态和软引用规则移除对象// WEAK - 弱引用:更积极的基于垃圾收集器状态和弱引用规则移除对象String eviction = context.getStringAttribute("eviction", "LRU");Class extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);//flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。Long flushInterval = context.getLongAttribute("flushInterval");//size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。Integer size = context.getIntAttribute("size");//只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 falseboolean readWrite = !context.getBooleanAttribute("readOnly", false);boolean blocking = context.getBooleanAttribute("blocking", false);Properties props = context.getChildrenAsProperties();//把缓存节点加入到Configuration中builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);} }
【1.1.2.2.7.2.1.2.0】分析缓存是如何封装的
public Cache useNewCache(Class extends Cache> typeClass, Class extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) {Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build();configuration.addCache(cache);currentCache = cache;return cache; }public Cache build() {setDefaultImplementations();//这里便是设置了PerpetualCache为最基础层的cacheCache cache = newBaseCacheInstance(implementation, id);setCacheProperties(cache);//在这里利用decorators作为包装器进行包装if (PerpetualCache.class.equals(cache.getClass())) {//便会存在LruCache【 delegate属性-》PerpetualCache】,利用了反射for (Class extends Cache> decorator : decorators) {cache = newCacheDecoratorInstance(decorator, cache);setCacheProperties(cache);}//这里又会包装别的cache = setStandardDecorators(cache);} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {cache = new LoggingCache(cache);}return cache; }private Cache setStandardDecorators(Cache cache) {try {MetaObject metaCache = SystemMetaObject.forObject(cache);if (size != null && metaCache.hasSetter("size")) {metaCache.setValue("size", size);}if (clearInterval != null) {cache = new ScheduledCache(cache);//ScheduledCache:调度缓存,负责定时清空缓存((ScheduledCache) cache).setClearInterval(clearInterval);}if (readWrite) { // 将LRU 装饰到Serializedcache = new SerializedCache(cache); //SerializedCache:缓存序列化和反序列化存储}cache = new LoggingCache(cache); //日志记录层包装cache = new SynchronizedCache(cache); //线程同步层包装if (blocking) { //判断是否有进行防穿透设置cache = new BlockingCache(cache);}return cache;} catch (Exception e) {throw new CacheException("Error building standard cache decorators. Cause: " + e, e);} }
【1.1.2.2.7.2.1.2.1】对cache的分析:
//Cache本身只是一个接口,用于定义缓存 public interface Cache {String getId();void putObject(Object key, Object value);Object getObject(Object key);Object removeObject(Object key);void clear();int getSize();default ReadWriteLock getReadWriteLock() { return null; } }//重点在于它的实现类 //四种具备淘汰机制的cache LruCache:(最近最少使用)防溢出缓存区 FifoCache:(先进先出) WeakCache:基于弱引用实现的缓存管理策略 SoftCache:基于软引用实现的缓存管理策略//底层的cache PerpetualCache:真正存储缓存的地方//额外辅助功能的cache ScheduledCache:过期清理缓存区 SynchronizedCache:线程同步缓存区 LoggingCache:统计命中率以及打印日志 SerializedCache:缓存序列化和反序列化存储 BlockingCache:防穿透
【1.1.2.2.7.2.1.2.1.1】对cache的最常用的两种淘汰策略分析:
【1.1.2.2.7.2.1.2.1.1.1】对LRU分析:【这个本质上就是利用LinkedHashMap】
public class LruCache implements Cache {private final Cache delegate;private Map
【1.1.2.2.7.2.1.2.1.1.2】对FIFO分析:【这个本质上就是利用LinkedList】
public class FifoCache implements Cache {private final Cache delegate;private final DequekeyList;private int size;public FifoCache(Cache delegate) {this.delegate = delegate;this.keyList = new LinkedList<>();this.size = 1024;}@Overridepublic String getId() {return delegate.getId();}@Overridepublic int getSize() {return delegate.getSize();}public void setSize(int size) {this.size = size;}//可以看出都是移除头部节点,将新的塞入尾结点@Overridepublic void putObject(Object key, Object value) {cycleKeyList(key);delegate.putObject(key, value);}@Overridepublic Object getObject(Object key) {return delegate.getObject(key);}@Overridepublic Object removeObject(Object key) {return delegate.removeObject(key);}@Overridepublic void clear() {delegate.clear();keyList.clear();}//加到最后,移除最前private void cycleKeyList(Object key) {keyList.addLast(key);if (keyList.size() > size) {Object oldestKey = keyList.removeFirst();delegate.removeObject(oldestKey);}}}
【1.1.2.2.7.2.1.3】解析sql节点部分sqlElement方法解析:
private void buildStatementFromContext(Listlist) {// 判断有没有配置数据库厂商IDif (configuration.getDatabaseId() != null) {buildStatementFromContext(list, configuration.getDatabaseId());}buildStatementFromContext(list, null); }/** * 方法实现说明:解析select|update|delte|insert节点然后创建mapperStatment对象 * @param list:所有的select|update|delte|insert节点 * @param requiredDatabaseId:判断有没有数据库厂商Id */ private void buildStatementFromContext(List list, String requiredDatabaseId) {// 循环select|delte|insert|update节点for (XNode context : list) {// 创建一个xmlStatement的构建器对象final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);try {statementParser.parseStatementNode();} catch (IncompleteElementException e) {configuration.addIncompleteStatement(statementParser);}} }
【1.1.2.2.7.2.1.3.1】深入分析是怎么解析的:
public void parseStatementNode() {// insert|delte|update|select 语句的sqlIdString id = context.getStringAttribute("id");// 判断insert|delte|update|select 节点是否配置了数据库厂商标注String databaseId = context.getStringAttribute("databaseId");// 匹配当前的数据库厂商id是否匹配当前数据源的厂商idif (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 获得节点名称:select|insert|update|deleteString nodeName = context.getNode().getNodeName();// 根据nodeName 获得 SqlCommandType枚举SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));// 判断是不是select语句节点boolean isSelect = sqlCommandType == SqlCommandType.SELECT;// 获取flushCache属性,默认值为isSelect的反值:查询:默认flushCache=false,增删改:默认flushCache=trueboolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);// 获取useCache属性,默认值为isSelect:查询:默认useCache=true,增删改:默认useCache=falseboolean useCache = context.getBooleanAttribute("useCache", isSelect);// resultOrdered: 是否需要处理嵌套查询结果 group by (使用极少),可以将比如 30条数据的三组数据 组成一个嵌套的查询结果boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);// 解析sql公用片段,如////将解析成sql语句 放在
【1.1.2.2.7.2.1.3.1.1】深入分析SqlNode的生成:
@Override //方法实现说明:创建sqlSource对象 public SqlSource createSqlSource(Configuration configuration, XNode script, Class> parameterType) {XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);return builder.parseScriptNode(); }public SqlSource parseScriptNode() {MixedSqlNode rootSqlNode = parseDynamicTags(context);SqlSource sqlSource;if (isDynamic) {// 动态Sql源(需要参数才能确定的sql语句)// 如:select id,user_name,create_time from t_user where id=${param1} ,这种是拼接的sqlSource = new DynamicSqlSource(configuration, rootSqlNode);} else {// 静态Sql源(不需要通过参数就能确定的sql语句),它会在这里解析// 如:select id,user_name,create_time from t_user where id=#{param1} ,因为是会把 #{param1} 这部分用?替换sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);}return sqlSource; }// 解析${} 和 动态节点 /*** 递归解析 selectById这个sql元素会解析成* 1层 MixedSqlNode
【1.1.2.2.7.2.2】分析MapperAnnotationBuilder类#parse方法
// MapperAnnotationBuilder类#parse方法 public void parse() {String resource = type.toString();// 是否已经解析mapper接口对应的xmlif (!configuration.isResourceLoaded(resource)) {// 根据mapper接口名获取 xml文件并解析,解析里面所有东西放到configurationloadXmlResource();// 添加已解析的标记configuration.addLoadedResource(resource);assistant.setCurrentNamespace(type.getName());parseCache();parseCacheRef();// 获取所有方法 看是不是用了注解Method[] methods = type.getMethods();for (Method method : methods) {try {if (!method.isBridge()) {// 是不是用了注解 用了注解会将注解解析成MappedStatementparseStatement(method);}} catch (IncompleteElementException e) {configuration.addIncompleteMethod(new MethodResolver(this, method));}}}parsePendingMethods(); }
上一篇:PR-视频加介绍背景