Mybatis日志系统 – 非xml模式的配置和源码探究

前言

Mybatis大家用的都很多,但是百度上搜一下大多数还都是基于xml的配置居多,有些博主更是为了添加一个日志收集,配置好了以后硬套了一个xml去解析,不觉得不值当的吗。更有甚者说xxx.xml是必备的,简直是误人子弟。笔者写这篇文章就是就为了给大家提供一个新的Mybatis的配置思路。以及从源码的角度解释下为什么能这么做。更多Spring内容进入【Spring解读系列目录】

准备Spring和Mybatis依赖

首先要整合项目必定少不了各种依赖,笔者这里提供的是最小依赖,有了这些就可以完成一个项目了,因为要演示日志系统,所以也加入了Log4J,Mybatis内部日志系统基本上都一样弄懂一个弄懂全部。

<!--mysql-->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.21</version>
</dependency>
<!--spring core-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.2.8.RELEASE</version>
</dependency>
<!--spring jdbc 提供数据库连接池-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
  <version>5.2.8.RELEASE</version>
</dependency>
<!--mybatis core-->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.5.5</version>
</dependency>
<!--mybatis spring-->
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>2.0.5</version>
</dependency>
<!--log4j-->
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

Spring和Mybatis基于Java模式的整合

整合Mybatis有两个比较常用的组件数据源DataSourceSqlSessionFatory。网上的博客充斥着xml的配置,比如:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>
<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>

我不说这种错吧,毕竟官网上也这么写的,但是官网上还写了Java-based的开发模式怎么就不写了呢。所以这篇博客里要做一个不配置xml文件但是一样完成sql执行和打印log的整合

AppConfig配置类

@Configuration
@ComponentScan("com.demo")
@MapperScan("com.demo.mapper")
public class AppConfig {
    
    
    //配置数据源
    @Bean
    public DataSource dataSource(){
    
    
        DriverManagerDataSource driverManagerDataSource=new DriverManagerDataSource();
        driverManagerDataSource.setPassword("111111");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/new_schema");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return driverManagerDataSource;
    }
    //配置SqlSessionFactory
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
    
    
        //构造SqlSessionFactory
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        //构造一个ibatis的Configuration对象,用来设置log实现类
        org.apache.ibatis.session.Configuration configuration=new org.apache.ibatis.session.Configuration();
        //设置log实现类为log4j
        configuration.setLogImpl(Log4jImpl.class);
        //添加更新的configuration到SqlSessionFactoryBean中
        factoryBean.setConfiguration(configuration);
        //设置数据源
        factoryBean.setDataSource(dataSource());
        return factoryBean.getObject();
    }
}

Sql执行接口CityDao

这个接口需要在com.demo.mapper包下,因为配置类中扫描的就是这个包。

public interface CityDao {
    
    
    @Select("select * from city")
    public List<Map<String,Object>> list();
}

Service执行类

这个类也需要在com.demo.mapper包下。

@Service
public class CityService {
    
    
    //这种注入的写法已经不规范了,实际项目中还是用set方法注入吧
    @Autowired
    CityDao cityDao;
    public List<Map<String,Object>> find(){
    
    
        return cityDao.list();
    }
}

Log4J的配置

log4j.rootLogger=DEBUG, stdout
# 配置要应用LOG的包..方法
log4j.logger.com.demo.mapper.CityDao.list=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

测试类

这个是调用方法的测试类,完成上述配置以后,直接运行就是见证奇迹的时刻,一个xml没写一样完成了Mybatis的各种打印:

public class MybatisTest {
    
    
    public static void main(String[] args) {
    
    
        AnnotationConfigApplicationContext anno=new AnnotationConfigApplicationContext(AppConfig.class);
        System.out.println(anno.getBean(CityService.class).find());
    }
}

打印结果:
2020-09-22 17:46:02,037 DEBUG [org.apache.ibatis.logging.LogFactory] - Logging initialized using 'class org.apache.ibatis.logging.log4j.Log4jImpl' adapter.
2020-09-22 17:46:02,438 DEBUG [org.mybatis.spring.SqlSessionUtils] - Creating a new SqlSession
2020-09-22 17:46:02,452 DEBUG [org.mybatis.spring.SqlSessionUtils] - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@770d3326] was not registered for synchronization because synchronization is not active
2020-09-22 17:46:07,104 DEBUG [org.mybatis.spring.transaction.SpringManagedTransaction] - JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@71329995] will not be managed by Spring
我们配置的sql语句
2020-09-22 17:46:07,114 DEBUG [com.demo.mapper.CityDao.list] - ==>  Preparing: select * from city  
参数,举个例子没有配置所以为空
2020-09-22 17:46:07,184 DEBUG [com.demo.mapper.CityDao.list] - ==> Parameters: 	
总的条目
2020-09-22 17:46:07,252 DEBUG [com.demo.mapper.CityDao.list] - <==      Total: 1	
2020-09-22 17:46:07,270 DEBUG [org.mybatis.spring.SqlSessionUtils] - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@770d3326]
打印数据
[{
    
    country_code=11, district=11, name=awd, id=1, population=11}] 

Mybatis官网

为什么我们能够这样写,是因为Mybatis官网已经说的很清楚了。比如SqlSessionFactoryBean这个类,官网除了使用xml外还贴了这样的内容。

@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
    
    
  SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
  factoryBean.setDataSource(dataSource());
  return factoryBean.getObject();
}

笔者就是直接贴来用的,但是网上的帖子还是充斥着xml的配置,真的没有人多往下看一行吗?令人费解。再比如配置log的内容,随便搜一个就是xml配置。

<configuration>
  <settings>
    ...
    <setting name="logImpl" value="LOG4J"/>
    ...
  </settings>
</configuration>

这也是官网上的内容不错,但凡往下看一行就能发现基于Java的写法,以下就是摘自官网,就在上面的xml的正下方:

Valid values are SLF4J, LOG4J, LOG4J2, JDK_LOGGING, COMMONS_LOGGING, STDOUT_LOGGING, NO_LOGGING or a full qualified class name that implements org.apache.ibatis.logging.Log and gets an string as a constructor parameter.

You can also select the implementation by calling one of the following methods:
org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useLog4J2Logging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();

If you choose to call one of these methods, you should do so before calling any other MyBatis method. Also, these methods will only switch to the requested log implementation if that implementation is available on the runtime classpath. For example, if you try to select Log4J logging and Log4J is not available at runtime, then MyBatis will ignore the request to use Log4J and will use it’s normal algorithm for discovering logging implementations.

然后官网还贴心的告诉用户如果想要用Log4j,下面也是摘自官网。

Step 1: Add the Log4J JAR file.
Step 2: Configure Log4J
package org.mybatis.example;
public interface BlogMapper {
    
    
  @Select("SELECT * FROM blog WHERE id = #{id}")
  Blog selectBlog(int id);
}
Create a file called log4j.properties as shown below and place it in your classpath:
# Global logging configuration
log4j.rootLogger=ERROR, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

官网当然也给了SLF4J等等的详细示例,这里就不多说了。但是有这样一句话值得注意

If you choose to call one of these methods, you should do so before calling any other MyBatis method.

也就是说想要使用useSlf4jLogging()这种方法,必须要在用之前添加上。显然我们不可能给每一个调用sql注解的方法里加上这个方法,这样耦合度就太高了。那我们去源码里看下Mybatis的日志实现是怎么加载的。

Mybatis源码

既然Mybatis要我们使用org.apache.ibatis.logging.LogFactory.useSlf4jLogging();那就去这个方法里看看到底写了啥,只要进入这个类就能发现这样一块静态代码块。

static {
    
    
  tryImplementation(LogFactory::useSlf4jLogging);
  tryImplementation(LogFactory::useCommonsLogging);
  tryImplementation(LogFactory::useLog4J2Logging);
  tryImplementation(LogFactory::useLog4JLogging);
  tryImplementation(LogFactory::useJdkLogging);
  tryImplementation(LogFactory::useNoLogging);
}

也就是说只要加载到LogFactory这个类这里的方法就会被解析。这里LogFactory使用了Runnable接口直接调用了useSlf4jLogging()方法,接着走。

public static synchronized void useSlf4jLogging() {
    
    
 setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}

这是个套壳方法,接着走。

private static void setImplementation(Class<? extends Log> implClass) {
    
    
  try {
    
    
    Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
    Log log = candidate.newInstance(LogFactory.class.getName());
    if (log.isDebugEnabled()) {
    
    
      log.debug("Logging initialized using '" + implClass + "' adapter.");
    }
    logConstructor = candidate;
  } catch (Throwable t) {
    
    
    throw new LogException("Error setting Log implementation.  Cause: " + t, t);
  }
}

发现这里只是做了给了一个logConstructor也就是一个构造方法对象供外部使用,说明这里也没干什么重要的事情。那就说明问题一定在Slf4jImpl构造方法里,因为setImplementation()这个方法就是在拿到传入的类的构造方法对象的,所以要去Slf4jImpl的构造方法里面看看有什么内容。

public Slf4jImpl(String clazz) {
    
    
  Logger logger = LoggerFactory.getLogger(clazz);

  if (logger instanceof LocationAwareLogger) {
    
    
    try {
    
    
      // check for slf4j >= 1.6 method signature
      logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class);
      log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger);
      return;
    } catch (SecurityException | NoSuchMethodException e) {
    
    
      // fail-back to Slf4jLoggerImpl
    }
  }

这里就发现Logger对象开始被反射,如果拿不到这个对象,就捕获异常退出去。而外面getLog()方法执行的时候也会因为是null而报错,打印一个exception结束,直到拿到一个有值的。

public static Log getLog(String logger) {
    
    
  try {
    
    
    return logConstructor.newInstance(logger); //反射拿到log对象
  } catch (Throwable t) {
    
    
    throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
  }
}

看到这里就可以大胆地猜测只要设置了一个Mybatis支持的Log实现类就可以直接使用了。于是就在Configuration里面翻出来了这个方法setLogImpl(Class<? extends Log> logImpl);,看看里面写的是啥,是不是就是LogFactory这个类。

public void setLogImpl(Class<? extends Log> logImpl) {
    
    
  if (logImpl != null) {
    
    
    this.logImpl = logImpl;
    LogFactory.useCustomLogging(this.logImpl);
  }
}

另一种配置

如果一定要是使用官网上org.apache.ibatis.logging.LogFactory.useLog4JLogging();的方法,则需要在Spring初始化之前做,加在下面这里就可以了。因为Spring一定会先于Mybatis初始化,在实例化CityDao的代理的时候,CityDao的代理里面需要有一个已经设定好的Logger对象,这个Logger对象需要由Spring去告诉Mybatis使用哪个实例类去实现。因此如果再加别处,Spring无法完成这个代理也就是说Logger对象为null,没有办法完成代理,也没有办法完成日志初始化。

public class MybatisTest {
    
    
    public static void main(String[] args) {
    
    
   		org.apache.ibatis.logging.LogFactory.useLog4JLogging();
        AnnotationConfigApplicationContext anno=new AnnotationConfigApplicationContext(AppConfig.class);
        System.out.println(anno.getBean(CityService.class).find());
    }
}

在这里插入图片描述

结语

到此Mybatis的日志系统其实基本上就说完了,看起来也没有很复杂。了解源码了解更多神器背后的故事,谢谢大家。

猜你喜欢

转载自blog.csdn.net/Smallc0de/article/details/108738079