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