一文搞懂Java开发中混乱的日志体系

一、日志框架

市面上的日志框架有JUL、JCL、Jboss-logging、logback、Log4j、log4j2、slf4j....

日志门面(日志的抽象层) 日志实现
jcl(Jakarta Commons Logging)、SLF4J(Simple Logging facade for Java)、jboss-loggin(使用场景少) Log4j、JUL(java.utils.logging)、Log4j2、Logback

左边选一个门面(抽象层)、右边来选一个实现。其中jcl最后更新时间为2014年后面就没再更新了,jboss-loggin使用场景很少。Log4j由于有性能问题,原作者写了个新的框架Logback,考虑到以后可能会有更多的框架,于是作者写了一个日志门面SLF4J。使用上日志门面使用 SLF4J,日志实现的话由于Log4j有了更好的替代者Logback,所以不考虑使用Log4j。JUL是Java为了抢占市场份额写的一个日志框架,不考虑。log4j2是借log4j之名由Apache重新写的日志框架,但是很多框架都还没适配起来,不考虑。所以选择使用Logback。
spring框架默认使用JCL(抽象层,具体实现不同版本不一样)。而SpringBoot由于底层是spring框架,springboot对日志进行了一次包装,选用SLF4j和logback

二、日志的使用
2.1 Log4j的使用

添加依赖:

<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

添加一个配置文件:

log4j.rootLogger=info,stdout
#输出到控制台
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{yyyy-MM-dd HH:mm:ss} %p [%t] %C.%M(%L) | %m%n

测试:

import org.apache.log4j.Logger;
public class Log4JTest {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger("log4j");
        logger.info("log4j");
    }
}
2.2 JUL的使用

这是java自带的日志框架,无需添加依赖即可直接使用:

import java.util.logging.Logger;
public class JULTest {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger("jul");
        logger.info("jul");
    }
}
2.3 JCL的使用

jcl是一个抽象层的框架,不直接记录日志,它是使用第三方实现(Log4j、JUL等等)记录日志,框架中有谁就用谁,都有的时候会按照默认顺序选择(下文会讲到),添加JCL依赖:

<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.2</version>
</dependency>

测试:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class JCLTest {
    public static void main(String[] args) {
        Log log = LogFactory.getLog("jcl");
        log.info("jcl");
    }
}

可以看到此时JCL打印结果格式和JUL一样。通过断点我们看到log的类型,Jdk14Logger是JCL用来操作JUL的实现类:

如果加上Log4j的依赖,输出格式变成了和Log4j一样:

断点看下log类型,其中Log4JLogger是JCL提供的用来操作Log4j日志的实现类:

JCL框架不直接记录日志,提供了记录日志的抽象方法即接口(info、debug、error等),底层通过一个数组存放具体的日志框架的类名,然后循环数组依次去匹配这些类名是否在app中被依赖了,如果找到被依赖的则直接使用,所以他有先后顺序:

底层会循环该数组,通过class.forname的形式如果能找到某个类就会跳出循环去创建这个类。否则遍历完这个数组。可以理解为jcl不提供日志实现,只是提供了一组记录日志的抽象方法即接口,好处在于我们的上层调用代码是JCL提供的,是固定的,不需要随着日志的实现而修改这部分代码,很好的解除了日志框架切换的耦合性,我们只需要选择具体的日志版本实现:增加或删除依赖即可实现日志框架切换。但是jcl已经不更新,并且通过源码我们知道这个框架对Logback、Log4j2是不支持的。我们有更好的替代者:slf4j

2.4 SLF4J的使用

项目里面导入门面slf4j的依赖:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}

此时运行项目就会出现以下提示信息:

这并不是错误信息,并不是日志框架出错,只是这样不会有任何日志输出,如果需要输出日志我们需要绑定一个日志具体的实现框架,具体如下:

所有的应用程序应该面向SLF4J编程,导入具体的日志实现,最终SLF4J会调用具体的日志实现去记录日志。
空实现:如上图,如果项目中只导入了SLF4J那么不会有任何日志输出。
使用logback:logback就是对slf4j的实现,只需要导入logback的jar(有两个),底层调用logbackAPI去记录日志。
log4j:由于log4j出现比较早,当时还没有SLF4J,因此log4j在设计的时候没考虑到SLF4J,因此要使用就需要一个适配层(slf4j-log412:包含了log4j):向上实现了SLF4J的具体方法,向下真正需要记录日志的时候会去调用具体的日志实现的API
jul:同log4j
simple:slf4j默认的简单的日志实现
no-operation:没有什么操作的实现包
每一个日志的实现框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件。
slf4j+logback的使用只需要在pom文件中添加logback的两个依赖即可:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.26</version>
</dependency>
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-core</artifactId>
  <version>1.2.3</version>
</dependency>

项目开发中产生的日志遗留问题
比如有一个项目A开发的时候使用SLF4J+Logback,但是开发中需要使用的Spring(commons-logging)、Hibernate(jboss-logging)、MyBatis……不同的框架使用了不同的日志实现,如何实现统一日志记录,即使是别的框架也使用slf4j进行输出?

如上图所示,假设应用程序使用的Slf4j+logback,引入的其它框架包含了Commons-logging、log4j、juc等日志实现,如果要实现日志统一,我们需要使用对应的替换包(jcl-over-slf4j、log4j-over-slf4j、jul-to-slf4j)去取代原来的日志实现,这样把原来的实现排除掉了,但是替换包的功能和原来的具体实现包一样,原来的实现包中有什么类替换的包中也有,这样可以保证在排除了框架中依赖的日志类的时候就不会报错,但是此时调用替换的类去记录日志的时候,其实是这个类去调用slf4j,最后slf4j去调用我们应用程序中的具体实现logback。说白了就是替换包模拟原来个框架依赖的日志的各个类,框架最终使用的就是这些模拟的类,模拟的类就可以去操作我们应用程序的日志。
如果上面我们的应用程序需要使用Slf4j+log4j/jcl,其它框架的转换不变,但是应用程序在使用的时候由于在slf4j和log4j之间需要加一个适配层。
总结:如何让系统中所有的日志都统一到slf4j
1、将系统中其他日志框架先排除出去;
2、用中间包来替换原有的日志框架;
3、看情况导入slf4j其他的实现(logback)或适配层(比如slf4j-log412)

三、spring4与spring5日志的区别
1.spring4的日志

spring4默认使用了jcl(commons-logging)作为抽象层,使用方式同上面介绍过的jcl使用方式一样:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>4.3.14.RELEASE</version>
</dependency>

spring4的日志结构:

测试:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class SpringLogTest {
    public static void main(String[] args) {
        Log log = LogFactory.getLog("spring");
        log.info("test spring");
    }
}

此时没有添加其它日志实现框架,默认使用的是jul框架去打印日志:

如果加上log4j依赖就会使用log4j去打印日志:

2.spring5的日志

spring5对commons-logging做了修改(spring-jcl),默认使用jcl去绑定jul打印日志。如果要使用其他日志实现框架需要让对应的日志框架去绑定slf4j,即使用方式同slf4j一样。

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.1.8.RELEASE</version>
</dependency>

spring5的日志结构:

测试:

此时如果我们也加上log4j依赖:

可以看到此时不论我们的应用中是否添加了其它日志实现框架,打印日志的log类型始终是LogAdapter$JavaUtilsLog。我们看一下spring5中这个获取日志对象的源码:

public static Log createLog(String name) {
    switch (logApi) {
        case LOG4J:
            return LogAdapter.Log4jAdapter.createLog(name);
        case SLF4J_LAL:
            return LogAdapter.Slf4jAdapter.createLocationAwareLog(name);
        case SLF4J:
            return LogAdapter.Slf4jAdapter.createLog(name);
        default:
            // Defensively use lazy-initializing adapter class here as well since the
            // java.logging module is not present by default on JDK 9. We are requiring
            // its presence if neither Log4j nor SLF4J is available; however, in the
            // case of Log4j or SLF4J, we are trying to prevent early initialization
            // of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly
            // trying to parse the bytecode for all the cases of this switch clause.
            return LogAdapter.JavaUtilAdapter.createLog(name);
    }
}

其中logApi的值在类加载的时候就会初始化:

private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";
private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";
private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger";
private static final String SLF4J_API = "org.slf4j.Logger";
private static final LogAdapter.LogApi logApi;
static {
    if (isPresent(LOG4J_SPI)) {
        if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
            // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
            // however, we still prefer Log4j over the plain SLF4J API since
            // the latter does not have location awareness support.
            logApi = LogAdapter.LogApi.SLF4J_LAL;
        }
        else {
            // Use Log4j 2.x directly, including location awareness support
            logApi = LogAdapter.LogApi.LOG4J;
        }
    }
    else if (isPresent(SLF4J_SPI)) {
        // Full SLF4J SPI including location awareness support
        logApi = LogAdapter.LogApi.SLF4J_LAL;
    }
    else if (isPresent(SLF4J_API)) {
        // Minimal SLF4J API without location awareness support
        logApi = LogAdapter.LogApi.SLF4J;
    }
    else {
        // java.util.logging as default
        logApi = LogAdapter.LogApi.JUL;
    }
}

可以看到,静态代码块中针对使用不同的日志框架做了一系列的判断,定义的常量字符串代表的类都是各个日志实现框架中的类,会根据这个去查找是否引入了对应的依赖。例如这里的“org.slf4j.spi.LocationAwareLogger”是slf4j中的类,我们如果要使用log4j框架和slf4j使用log4j一样操作即可(在这个测试中我们不需要spring自带的日志,可以剔除,但是需要注意,如果使用了AnnotationConfigApplicationContext剔除以后需要添加jcl的转换包,否则会报错,使用在下面“4.2 MyBatis整合Spring时日志”介绍时pom.xml中会有说明):

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.1.8.RELEASE</version>
  <exclusions>
    <exclusion>
      <artifactId>org.springframework</artifactId>
      <groupId>spring-jcl</groupId>
    </exclusion>
  </exclusions>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.26</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.7.26</version>
</dependency>

此时“org.slf4j.spi.LocationAwareLogger”就会在项目中存在,在静态代码块的判断过程中logApi的值就会变成“SLF4J_LAL”,创建log对象的时候就会创建log4j的:

此时控制台输出的日志就是通过log4j的框架去实现的:

spring5日志使用总结:
1、默认使用jcl去依赖java自带的jul
2、如果要使用其它日志框架实现需要通过对应的日志实现去绑定slf4j(就是slf4j的使用方式)

四、MyBatis以及和Spring整合时的日志
4.1 MyBatis单独使用时的日志操作

MyBatis自带了日志抽象工厂,具体的实现由SLF4J、Apache Commons Logging、Log4j 2、Log4j、JDK logging完成。当项目中存在多个实现时会根据上面的几个框架从前到后这个顺序依次查找,找到了就会优先使用该日志实现。看一下MyBatis中日志源码即可知晓原因:


在org.apache.ibatis.logging.LogFactory的静态代码块中,会根据上面说过的顺序拿到指定的日志实现类的类字节码创建对象,如果失败则继续尝试,只要找到一个就不会再往下去创建。
在很多情况下项目中会自带日志框架,比如Tomcat中自带了Commons Logging,如果我们想要使用log4j作为日志框架,我们需要引入log4j的jar,并且还要指定使用的日志框架:
方式一:通过在 MyBatis 配置文件 mybatis-config.xml 里面添加一项 setting 来选择别的日志工具

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

其中logImpl 可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了接口 org.apache.ibatis.logging.Log 的,且构造方法是以字符串为参数的类的完全限定名
方式二:调用如下任一方法来使用日志工具
org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();
需要注意的是:要调用以上某个方法,必须在调用其它 MyBatis 方法之前调用它。另外,仅当运行时类路径中存在该日志工具时,调用与该日志工具对应的方法才会生效,否则 MyBatis 一概忽略。如你环境中并不存在 Log4J,你却调用了相应的方法,MyBatis 就会忽略这一调用,转而以默认的查找顺序查找日志工具
使用示例之Log4J打印日志
maven项目中添加以下依赖:

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>3.4.6</version>
</dependency>
<!--mysql jdbc驱动-->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.46</version>
</dependency>
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

创建Dao接口:

public interface UserMapper {
    List<Map<String, Object>> findAllUser();
    List<Map<String, Object>> findUserByCondition(String strId);
}

在resources目录下创建同包名同类名的xml文件:

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mobei.dao.UserMapper">
    <select id="findAllUser" resultType="map">
        select * from user
    </select>
    <select id="findUserByCondition" resultType="map">
        select * from user where id = #{strId}
    </select>
</mapper>

核心配置文件mybatis-config.xml:

<?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>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///test"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/mobei/dao/UserMapper.xml"/>
    </mappers>
</configuration>

log4j.properties:

log4j.rootLogger=debug,stdout
#这句控制输出SQL语句
log4j.logger.com.mobei.dao=TRACE
#输出到控制台
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{yyyy-MM-dd HH:mm:ss} %p [%t] %C.%M(%L) | %m%n

测试类:

public static void main(String[] args) throws Exception {
    InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    SqlSessionFactory sessionFactory = builder.build(is);
    SqlSession sqlSession = sessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    List<Map<String, Object>> allUser = mapper.findAllUser();
    System.out.println(allUser);
    List<Map<String, Object>> conditionUser = mapper.findUserByCondition("1");
    System.out.println(conditionUser);
    
    sqlSession.close();
    is.close();
}

根据前面的分析我们知道,由于此时类路径下包含了log4j日志和jdk自带的jul日志,由于log4j优先于jul所以肯定会使用log4j框架输出日志:


org.apache.ibatis.logging.LogFactory.useXXXLogging()测试
此时如果我们想要切换成STDOUT_LOGGING,则需要在使用之前(测试发现必须在SqlSessionFactory 产生之前)调用org.apache.ibatis.logging.LogFactory.useStdOutLogging():

public static void main(String[] args) throws Exception {
    InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

    org.apache.ibatis.logging.LogFactory.useStdOutLogging();

    SqlSessionFactory sessionFactory = builder.build(is);
    SqlSession sqlSession = sessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<Map<String, Object>> allUser = mapper.findAllUser();
    System.out.println(allUser);
    List<Map<String, Object>> conditionUser = mapper.findUserByCondition("1");
    System.out.println(conditionUser);
    sqlSession.close();
    is.close();
}

此时的日志:

使用JUL打印sql日志
我们将pom.xml中的log4j日志去除掉,此时类路径下就只有JDK自带的JUL日志记录包了,我们测试:

public static void main(String[] args) throws Exception {
    InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    SqlSessionFactory sessionFactory = builder.build(is);
    SqlSession sqlSession = sessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<Map<String, Object>> allUser = mapper.findAllUser();
    System.out.println(allUser);
    List<Map<String, Object>> conditionUser = mapper.findUserByCondition("1");
    System.out.println(conditionUser);
    sqlSession.close();
    is.close();
}

发现此时没有任何sql相关的日志打印,经过我们前面的分析,此时应该默认会使用了JUL去打印日志才对,为什么会没有呢?这其实跟JUL默认的日志级别有关,我们断点调试即可知道JUL默认的日志级别是FINE(500):

这里的log对象类型是Jdk14LoggingImpl,我们找到该方法:

可以看到JUL底层默认设置的日志级别为FINE(500),是小于INFO(800)的,所以不会输出日志,如果我们想要修改这个日志级别,可以进行扩展:
自定义一个实现Log的类,在类中声明JUL的logger字段(这个类在这里仅为了测试输出log的,其它级别的日志如果需要还要继续去完善):

import org.apache.ibatis.logging.Log;
import java.util.logging.Logger;
public class MyLog implements Log {
    private Logger logger;
    public MyLog(String clazz) {
        logger = Logger.getLogger("my log");
    }
    @Override
    public boolean isDebugEnabled() {
        return true;
    }
    @Override
    public boolean isTraceEnabled() {
        return false;
    }
    @Override
    public void error(String s, Throwable e) {
    }
    @Override
    public void error(String s) {
    }
    @Override
    public void debug(String s) {
        logger.info(s);
    }
    @Override
    public void trace(String s) {
    }
    @Override
    public void warn(String s) {
    }
}

将该日志注册到核心配置文件mybatis-config.xml中:

<settings>
    <setting name="logImpl" value="com.mobei.log.MyLog"/>
</settings>

此时测试即可看到sql日志打印:

log4j调整打印sql粒度:指定只打印某个方法对应的sql
修改配置文件,只打印findAllUser方法的sql:log4j.logger.com.mobei.dao.UserMapper.findAllUser=TRACE,同样可以指定某个类中所有方法和某个包下所有类中的所有方法,具体可以看官网

log4j.rootLogger=info,stdout
#输出到控制台
log4j.logger.com.mobei.dao.UserMapper.findAllUser=TRACE
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{yyyy-MM-dd HH:mm:ss} %p [%t] %C.%M(%L) | %m%n

输出结果(记得切换成使用log4j日志),可以看到此时findUserByCondition方法的sql并没有打印:

4.2 MyBatis整合Spring时日志

经过前面那么多的分析, MyBatis整合Spring时日志的使用就变得异常简单了。以Spring5为例,Spring5默认使用jcl+jul,这里我们指定为使用log4j打印日志(其他使用方式和Spring5中日志的使用一样,都是相通的):
pom.xml:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.8.RELEASE</version>
    <!--日志统一使用log4j,需要排除掉spring自带的jcl-->
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jcl</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--添加slf4j门面-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.26</version>
</dependency>
<!--由于剔除了spring5自带的jcl框架,需要使用转换包去替换,否则spring类中依赖的其它类就会报错-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.26</version>
</dependency>
<!--绑定到log4j-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.26</version>
</dependency>
<!--spring自带的jdbc提供了连接池-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.1.8.RELEASE</version>
</dependency>
<!--mybatis核心-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>
<!--mysql jdbc驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.46</version>
</dependency>
<!--mybatis整合spring-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.1</version>
</dependency>

dao接口:

public interface UserMapper {
    @Select("select * from user")
    List<Map<String, Object>> findAllUser();
}

service:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    public List<Map<String, Object>> findAllUser(){
        return userMapper.findAllUser();
    }
}

log4j.proprerties:

log4j.rootLogger=info,stdout
#这句控制输出SQL
log4j.logger.com.mobei.dao=TRACE
#输出到控制台
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{yyyy-MM-dd HH:mm:ss} %p [%t] %C.%M(%L) | %m%n

配置类:

@Configuration
@ComponentScan("com.mobei")
@MapperScan("com.mobei.dao")
public class AppConfig {
    @Bean
    public DataSource dataSource(){
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setUsername("root");
        ds.setPassword("123456");
        ds.setUrl("jdbc:mysql:///test");
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        return ds;
    }
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        return factoryBean;
    }
}

测试:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ac =
        new AnnotationConfigApplicationContext(AppConfig.class);
    UserService service = ac.getBean(UserService.class);
    List<Map<String, Object>> allUser = service.findAllUser();
    System.out.println(allUser);
}

验证log4j生效:

log4j.appender.stdout.target=System.err

PS: 默认情况下的JUL日志级别配置
Spring5默认使用JCL+JUL,如果不添加任何其它日志实现框架,就是使用JUL输出日志,前面我们介绍的自定义JUL日志对象以修改日志级别,我们是将自定义的类配置在配置文件的setting标签中,对于这种配置类我们可以使用如下方式配置:

@Bean
public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();

    org.apache.ibatis.session.Configuration cfg = 
        new org.apache.ibatis.session.Configuration();
    cfg.setLogImpl(MyLog.class);
    factoryBean.setConfiguration(cfg);

    factoryBean.setDataSource(dataSource());
    return factoryBean;
}
五、springboot中的日志
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.1.6.RELEASE</version>
</dependency>

spring-boot-starter使用的日志:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
      <version>2.1.6.RELEASE</version>
      <scope>compile</scope>
    </dependency>

总结:
1)、SpringBoot底层也是使用slf4j+logback的方式进行日志记录
2)、SpringBoot也把其他的日志都替换成了slf4j;
如果我们要引入其它框架,如果框架使用的日志不是logback或者版本不一致,一定要把这个框架的默认日志依赖移除掉,否则可能会出现jar包冲突。SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其它框架的时候把这个框架依赖的日志框架排除掉。
SpringBoot默认帮我们配置好了日志,启动应用就可以看到控制台的日志输出。我们可以根据自己的需要来使用日志。

5.1 日志级别(这里应该放到前面介绍的,懒得改了,凑合着看吧)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SbLog1ApplicationTests {
    Logger logger = LoggerFactory.getLogger(getClass());
    @Test
    public void contextLoads() {
        //日志的级别:
        //低->高 trace<debug<info<warn<error
        //可以调整输出的日志级别:日志只会在这个级别及以后的高级别生效
        logger.trace("这是trace日志……");
        logger.debug("这是debug日志……");
        //SpringBoot默认给我们使用的是info级别的
        logger.info("这是info日志……");
        logger.warn("这是warn日志……");
        logger.error("这是error日志……");
    }
}

由于SpringBoot默认使用info级别(root级别),所以只会有高于info级别的日志输出:

我们可以在properties文件中设置指定目录下(或者类)的日志输出级别,比如指定com.mobei.sb_log_1目录下日志输出级别为trace:

logging.level.com.mobei.sb_log_1=trace
5.2 SpringBoot修改日志的默认配置
logging.file logging.path Example Description
(none) (none) 只在控制台输出
指定文件名 (none) my.log 输出日志到my.log文件
(none) 指定目录 /var/log 输出到指定目录的 spring.log 文件中

logging.file
不指定路径在当前项目下生成xxx.log日志

logging.file=springboot.log

也可以指定完整的路径:

logging.file=G:/springboot.log


logging.path
和logging.file是冲突设置,两者任选其一即可,如果两者都指定也只有logging.file起作用。一般使用logging.path,用于指定日志文件生成后存放的目录,其中日志文件默认叫spring.log

# 在当前磁盘的根路径(当前项目所在目录的根目录,例如这里项目在G盘下,根目录就是G:\)
# 下创建spring文件夹和里面的log文件夹:使用 spring.log 作为默认文件
logging.path=/spring/log
5.3 日志的格式

默认格式:

%d表示日期时间,        
%thread表示线程名,        
%‐5level:级别从左显示5个字符宽度        
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。         
%msg:日志消息,        
%n是换行符        

logging.pattern.console:在控制台输出的日志的格式
修改成我们自己指定格式的:

logging.pattern.console=%d{yyyy‐MM‐dd} [%thread] %‐5level %logger{50} ‐ %msg%n

logging.pattern.file:指定文件中日志输出的格式

logging.pattern.file=%d{yyyy‐MM‐dd} === [%thread] === %-5level === %logger{50} ==== %msg%n
5.4 SpringBoot中日志的默认值所在位置
5.5 指定配置(参考官网说明)

给类路径下放上每个日志框架自己的配置文件即可,SpringBoot就不使用他默认配置的了:

官网说明:使用标准的文件名logback.xml在应用启动的时候被日志框架识别,绕过了SpringBoot,不能扩展高级功能,如果需要扩展,需要使用扩展名比如logback-spring.xml,这样日志框架无法识别,不会去加载,而是由SpringBoot去解析配置,可以使用到SpringBoot的高级Profile功能

比如我们指定某端配置只在某个环境下生效:

<layout class="ch.qos.logback.classic.PatternLayout">
    <springProfile name="dev">
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
    </springProfile>
    <springProfile name="!dev">
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
    </springProfile>
</layout>

非生产环境:

生产环境:
激活生产环境可以在配置文件中设置:

spring.profiles.active=dev

或者在启动的时候带上命令参数:

运行结果:

如果不使用扩展名,直接使用logback.xml就会报错:

附上logback-spring.xml完整内容:

<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
    <!-- 定义日志的根目录 -->
    <property name="LOG_HOME" value="/app/log" />
    <!-- 定义日志文件名称 -->
    <property name="appName" value="mobei-springboot"></property>
    <!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <!--
        日志输出格式:
            %d表示日期时间,
            %thread表示线程名,
            %-5level:级别从左显示5个字符宽度
            %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 
            %msg:日志消息,
            %n是换行符
        -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <springProfile name="dev">
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
            </springProfile>
            <springProfile name="!dev">
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
            </springProfile>
        </layout>
    </appender>

    <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->  
    <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 指定日志文件的名称 -->
        <file>${LOG_HOME}/${appName}.log</file>
        <!--
        当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
        TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
        -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--
            滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动 
            %i:当文件大小超过maxFileSize时,按照i进行文件滚动
            -->
            <fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!-- 
            可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
            且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
            那些为了归档而创建的目录也会被删除。
            -->
            <MaxHistory>365</MaxHistory>
            <!-- 
            当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
            -->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <!-- 日志输出格式: -->     
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
        </layout>
    </appender>

    <!-- 
        logger主要用于存放日志对象,也可以定义日志类型、级别
        name:表示匹配的logger类型前缀,也就是包的前半部分
        level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
        additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,
        false:表示只用当前logger的appender-ref,true:
        表示当前logger的appender-ref和rootLogger的appender-ref都有效
    -->
    <!-- hibernate logger -->
    <logger name="com.mobei" level="debug" />
    <!-- Spring framework logger -->
    <logger name="org.springframework" level="debug" additivity="false"></logger>

    <!-- 
    root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
    要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。 
    -->
    <root level="info">
        <appender-ref ref="stdout" />
        <appender-ref ref="appLogAppender" />
    </root>
</configuration> 
5.6 切换日志框架
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-logging</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343