mybatis独立使用及源码分析
2021/6/18 20:57:59
本文主要是介绍mybatis独立使用及源码分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
mybatis独立使用及源码分析
不使用mybatis使用jdbc
JdbcExample
public class JdbcExample { public static void main(String[] args) throws Exception{ Class.forName("org.postgresql.Driver"); String url = "jdbc:postgresql://10.25.76.198:5432/artifact"; String username = "dev_root"; String password = "dev_root"; Connection connection = DriverManager.getConnection(url, username, password); Statement stmt = connection.createStatement(); PreparedStatement preparedStatement = connection.prepareStatement("select * from clean_policy where id = ?"); preparedStatement.setLong(1,292L); //prepare sql进行预处理 ResultSet resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { String name = resultSet.getString("name"); System.out.println("name:" + name); } //statement 不预处理直接替换 String sql = "select * from clean_policy where id = {0}"; sql = MessageFormat.format(sql, 292L); ResultSet resultSet1 = stmt.executeQuery(sql); while (resultSet1.next()) { String name = resultSet1.getString("name"); System.out.println("name1:" + name); } } }
我们直接使用jdbc查询语句有两种方式:
1. 预处理
2. 直接替换
两种方式的本质区别:预处理可以避免sql注入;直接替换无法避免预处理
SPI
Class.forName
//加载pg的驱动,这里加载类会触发类的静态代码块 Class.forName("org.postgresql.Driver"); org.postgresql.Driver class Driver{ //org.postgresql.Driver类的静态代码块,再类加载的时候就执行 static { try { register(); } catch (SQLException var1) { throw new ExceptionInInitializerError(var1); } } public static void register() throws SQLException { if (isRegistered()) { throw new IllegalStateException("Driver is already registered. It can only be registered once."); } else { Driver registeredDriver = new Driver(); // 这里把驱动注册到驱动管理器 DriverManager的registeredDrivers集合中 DriverManager.registerDriver(registeredDriver); Driver.registeredDriver = registeredDriver; } } }
获取到数据库连接从已加载的驱动中获取
DriverManager.getConnection(url, username, password); private static Connection getConnection(){ for(DriverInfo aDriver : registeredDrivers) { Connection con = aDriver.driver.connect(url, info); return con } }
SPI的机制
上面JdbcExample中main方法我们去掉Class.forName方法,发现还是可以正常获取数据连接,这说明数据库驱动是被
正常加载的,这种机制是如何实现的呢? 这种就是我们接下来要了解的SPI机制(service provider interface)
public class DriverManager { static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { while(driversIterator.hasNext()) { driversIterator.next(); } } } // 通过下面的源码我们可以看到driversIterator == lookupIterator class ServiceLoader{ public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); } public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); } } //driversIterator.hasNext() private static final String PREFIX = "META-INF/services/"; private boolean hasNextService() { if (configs == null) { try { // service.getName() == java.sql.Driver String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } //读取到jar包org.postgresql的META-INF下的services的java.sql.Driver的内容org.postgresql.Driver pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } // driversIterator.next(); private S nextService() { // 这里nextName就是从hasNextService方法中读到的org.postgresql.Driver, String cn = nextName; //和加载org.postgresql.Driver类,执行静态代码块,把自己注册到DriverManager的registeredDrivers集合中 c = Class.forName(cn, false, loader); }
总结:通过约定jar包具体目录存放接口的实现类,来动态可插拔加载类的方式
这里通过: 接口 + 配置文件 通过策略模式来实现spi机制,
- springboot中stater中就是通过这种方式来实现自动加载注入配置的。
- java中领域参数校验Validation也有采用这种机制
不依赖spring使用mybaits简单实例
创建Maven项目
MybatisUse
import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.InputStream; /** * @author xuelongjiang109 * @description **/ public class MybatisUse { public static void main(String[] args) throws Exception{ String mybatisXml = "mybatis.xml"; InputStream inputStream = Resources.getResourceAsStream(mybatisXml); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); CleanPolicyMapper cleanPolicyMapper = sqlSession.getMapper(CleanPolicyMapper.class); CleanPolicy cleanPolicy = cleanPolicyMapper.selectById(292L); System.out.println("name:" + cleanPolicy.getName()); sqlSession.commit(); sqlSession.flushStatements(); sqlSession.close(); } }
mybaits.xml
位于resources目录下
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!-- configuration 核心配置文件 --> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="org.postgresql.Driver"/> <property name="url" value="jdbc:postgresql://10.25.76.198:5432/artifact"/> <property name="username" value="dev_root"/> <property name="password" value="dev_root"/> </dataSource> </environment> </environments> <!-- 每一个 Mapper.XML 都需要在 Mybatis 核心配置文件中注册!!--> <mappers> <mapper class="com.xuelongjiang.testanyone.mybatis.CleanPolicyMapper"/> <!--<mapper class="com.song.dao.UserMapper"/>--> <!--<package name="com.song.dao"/>--> </mappers> </configuration>
CleanPolicyMapper
import org.apache.ibatis.annotations.Select; public interface CleanPolicyMapper { @Select("select * from clean_policy where id = #{id} ") CleanPolicy selectById(Long id); }
CleanPolicy实体
/** * @author xuelongjiang109 * @description **/ public class CleanPolicy { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
源码分析
下面源码会省略部分代码,我们只要焦距在主流程就行。
生成sql会话工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //sqlSessionFactory是sqlSession的工厂,sqlSession是对数据库一次会话的抽象 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // parser.parse() 返回org.apache.ibatis.session.Configuration return build(parser.parse()); } //返回默认的会话工厂 DefaultSqlSessionFactory public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
parser.parse()
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; // 读取到xml的根节点configuration parseConfiguration(parser.evalNode("/configuration")); return configuration; } // 解析xml中的节点及其内容 private void parseConfiguration(XNode root) { environmentsElement(root.evalNode("environments")); mapperElement(root.evalNode("mappers")); // ....省略了解析其他节点如:properties,plugins,plugins }
解析数据库配置
// 解析读取数据库配置 结果存放到configuration的environment属性 private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { //得到指定数据源的id 对用我们配置的 development environment = context.getStringAttribute("default"); } // 读取environment节点的内容 for (XNode child : context.getChildren()) { String id = child.getStringAttribute("id"); //如果当前环境的id等于指定的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()); } } } }
解析mapper
依次读取resource、url、class 注意这里如果mapper中的这个三个属性只能配置一个
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { //如果配置的是package则加载其下的所有类 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { // 如果是mapper 依次读取resource、url、class 注意这里如果mapper中的这个三个属性只能配置一个 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); 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("A mapper element may only specify a url, resource or class, but not more than one."); } } } } } // 继续解析mappers中的sql mapperParser.parse(); // 解析xml中的select,iseret,update,delete语句 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); // 解析sql语句的各种配置以及Sql内容构造出MappedStatement来存储这些信息 statementParser.parseStatementNode(); //注意:MappedStatement中的id是namespace.id MappedStatement //MappedStatement接下来被放到configuration.mappedStatements中(这一个map StrictMap是mybatis继承HashMap重写了部分方法) configuration.addMappedStatement(statement); public V put(String key, V value) { //如果已经put过sql了直接报异常,如mapper接口有注解sql和xml也有sql,则直接报错 if (containsKey(key)) { throw new IllegalArgumentException(name + " already contains value for " + key); } // ... 省略 }
CleanPolicyMapper cleanPolicyMapper = sqlSession.getMapper(CleanPolicyMapper.class);如何执行的
CleanPolicyMapper是一个接口,我们并没有实现这个接口,那么getMapper返回的是什么呢?
通过下面源码,我们看到getMapper返回的一个动态代理对象
bindMapperForNamespace: 解析mapper对应的namespace,创建CleanPolicyMapper的代理类()
mapperParser.parse() ----> bindMapperForNamespace() ----> configuration.addMapper() —> knownMappers.put(type, new MapperProxyFactory(type));
sqlSession.getMapper(CleanPolicyMapper.class) —> mapperProxyFactory = knownMappers.get(type); —> mapperProxyFactory.newInstance(sqlSession); --> Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); --> MapperProxy.invoke() ----> mapperMethod.execute(sqlSession, args); —> sqlSession.selectOne(); --> DefaultSqlSession.selectList() -->
MappedStatement ms = configuration.getMappedStatement(); --> executor.query()
// mapperParser.parse(); public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } //解析namespace对应的类,加入到MapperProxyFactory代理工厂 private void bindMapperForNamespace() { String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource configuration.addLoadedResource("namespace:" + namespace); // cleanPolicyMapper创建MapperProxyFactory加入到mapper中 configuration.addMapper(boundType); } } } } 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); } } } } public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } @Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); } public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } // 最终执行 protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } // 实现jdk的InvocationHandler接口实现动态代理 public class MapperProxy<T> implements InvocationHandler, Serializable { }
加载:
- MappedStatement构造出
- 接口构造出MapperProxy动态代理
mybatis如何处理有无@param
selectById(@param(id)long id)
selectById(long id)
如果有@param则下方代码中names为
key=1,name= id
如果没有@param则
key=1,name=arg0
Object param = method.convertArgsToSqlCommandParam(args); paramNameResolver.getNamedParams(args); public Object getNamedParams(Object[] args) { final int paramCount = names.size(); //如果没有参数直接直接返回null if (args == null || paramCount == 0) { return null; } //如果只有一个参数直接返回传入的值 else if (!hasParamAnnotation && paramCount == 1) { return args[names.firstKey()]; } //如果有多个参数返回map结构 else { final Map<String, Object> param = new ParamMap<Object>(); int i = 0; for (Map.Entry<Integer, String> entry : names.entrySet()) { param.put(entry.getValue(), args[entry.getKey()]); // add generic param names (param1, param2, ...) final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); // ensure not to overwrite parameter named with @Param if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }
这里我们重点看下多个参数的情况
selectById(@param(id)long id, @param(“name”) String name)
selectById(long id, String name)
id =292
name= zhang
1.有@param返回的map
key=id, value = 292
key=name,value=zhang
key=param1,value=292
key=param2,value=zhang
2.没有@param返回的map
key=arg0, value = 292
key=arg1,value=zhang
key=param1,value=292
key=param2,value=zhang
param1,param2的作用是为了第三方框架方便和mybatis继承。
mybatis处理sql注入
mybaits处理sql注入的原理是利用jdbc的PreparedStatement的set值而不是简单的替换。
#{}: 使用PreparedStatement来进行set值
${}: 简单替换
handler.parameterize(stmt); public void parameterize(Statement statement) throws SQLException { parameterHandler.setParameters((PreparedStatement) statement); } void setParameters(PreparedStatement ps) { typeHandler.setParameter(ps, i + 1, value, jdbcType); }
处理带有${}的动态sql
可以看到这里是简单替换有sql注入风险
executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); BoundSql boundSql = ms.getBoundSql(parameterObject); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); public BoundSql getBoundSql(Object parameterObject) { rootSqlNode.apply(context); } // TextSqlNode public boolean apply(DynamicContext context) { GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); context.appendSql(parser.parse(text)); return true; } //解析替换${} TextSqlNode.parse()
上述源码中的常见对象
Configuration: 存储解析的xml的属性,sql,自定义插件
SqlSessionFactory:sql会话工厂
SqlSession:sql会话
XMLConfigBuilder:解析mybatis的xml配置文件以及mapper对应的xml文件
MappedStatement mapper对应xml中sql的解析封装
StrictMap: mybaits继承hashmap重写put方法,key不能重复加入
MapperProxyFactory: mapper接口的代理对象
MapperProxy: 实现jdk动态代理InvocationHandler接口,实际执行增删改查调用它的invoke方法
MapperMethod: 内部封装sql类型,以及id可以找到MappedStatement,以及方法的签名信息,解析方法参数
executor: 方法执行器这个过程会涉及到mybatis的核心组件ParameterHanlder、ResultSetHanler、StatementHanlder、以及自身Executor
总结点
- JDBC加载驱动器有两种方式Class.forName()和SPI机制来加载驱动
- springboot中stater中就是通过这种方式来实现自动加载注入配置的。
- configuration存放了myabtis的整个配置文件的信息以及抽象出sql的MappedStatemen
- MappedStatement: 解析sql语句的各种配置以及Sql内容构造出MappedStatement来存储这些信息
- 中依次读取resource、url、class 注意这里如果mapper中的这个三个属性只能配置一个
- 解析sql语句的各种配置以及Sql内容构造出MappedStatement来存储这些信息
- MappedStatement中的id是namespace.id,也就是用nameSpace和id来定位唯一sql
- StrictMap的Put实现,如果put过sql了直接报异常(如mapper接口有注解sql和xml也有sql,则直接报错)
- interface中的方法与xml中sql的关联通过id(inteface全路径 + 方法名)
- mapper接口最终会被jdk动态代理为MapperProxy
- param1,param2的作用是为了第三方框架方便和mybatis继承。
- ${}有sql注入风险,#{}没有sql注入风险使用了jdbc的PreparedStatement
这篇关于mybatis独立使用及源码分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27消息中间件底层原理资料详解
- 2024-11-27RocketMQ底层原理资料详解:新手入门教程
- 2024-11-27MQ底层原理资料详解:新手入门教程
- 2024-11-27MQ项目开发资料入门教程
- 2024-11-27RocketMQ源码资料详解:新手入门教程
- 2024-11-27本地多文件上传简易教程
- 2024-11-26消息中间件源码剖析教程
- 2024-11-26JAVA语音识别项目资料的收集与应用
- 2024-11-26Java语音识别项目资料:入门级教程与实战指南
- 2024-11-26SpringAI:Java 开发的智能新利器