0%

Mybatis Executor

1
日常工作中,mybatis框架已经使用了很多年,然而终究只是停留在最表面。早就听闻其源码优雅,最近忍不住再一次捡起来看看,然观之仍不得要领,仅记之。

Executor 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface Executor {

ResultHandler NO_RESULT_HANDLER = null;
// 更新方法
int update(MappedStatement ms, Object parameter) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

List<BatchResult> flushStatements() throws SQLException;

void commit(boolean required) throws SQLException;

void rollback(boolean required) throws SQLException;

CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

boolean isCached(MappedStatement ms, CacheKey key);

void clearLocalCache();

void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

Transaction getTransaction();

void close(boolean forceRollback);

boolean isClosed();

void setExecutorWrapper(Executor executor);

}

继承关系

image-20220319115805569

Executor 方法摘要

image-20220319120704115

BaseExecutor 方法摘要

红框中四个抽象方法,由 BaseExecutor 子类实现,这样可以在 BaseExecutor 其中进行统一的处理,如一级缓存相关。

image-20220319120822251

以 BaseExecutor 中的 query 分析执行 select 语句的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 是否清除本地缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 缓存中获取数据
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 执行查询处理
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
......
return list;
}

queryFromDatabase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 调用具体的实现类中的方法
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 缓存处理
localCache.putObject(key, list);
// 存储过程调用返回值处理
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

对比 doQuery 方法

SimpleExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}

ReuseExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  @Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
}

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// statement 重用
if (hasStatementFor(sql)) {
stmt = getStatement(sql);
applyTransactionTimeout(stmt);
} else {
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
// 添加缓存
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}

CacheExecutor

通过 Debug SqlSession sqlSession = sqlSessionFactory.openSession() ,查看到 CachingExecutor 的由来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Configuration {
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 二级缓存 mybatis-config.xml 中配置,默认为true
// <settings>
// <setting name="cacheEnabled" value="true"/>
// </settings>
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 责任链
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InterceptorChain {

private final List<Interceptor> interceptors = new ArrayList<>();

public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}

public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}

}

Spring Resource 抽象

  • ResourceLoader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ApplicationContext 实际上也是一个ResourceLoader

@Component
public class ResourceLoaderContext implements ResourceLoaderAware {
private ResourceLoader resourceLoader;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
}

@Autowired
private ResourceLoaderContext resourceLoaderContext;

@Test
public void resourceLoaderTest() throws IOException {
// 这里会自动根据路径选择不同的资源加载器
final Resource resource = resourceLoaderContext.getResourceLoader().getResource("file:/Users/zhenglin/learn-space/RuoYun/framework/src/main/java/zlin/site/framework/Foo.java");
try (final InputStream stream = resource.getInputStream()) {
final String content = IOUtils.toString(stream);
log.info(content);
}
}
  • ResourcePatternResolver
  • Resource 注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 单个资源文件注入
@Component
@Getter
public class ResourcePathBean {
private final Resource template;
public ResourcePathBean(@Value("${template.path}") Resource template) {
this.template = template;
}
}

// 多个资源文件注入
@ConfigurationProperties(prefix = "templates")
@Component
@Getter
public class MultipleResourcePathBean {
private final List<Resource> templates;
public MultipleResourcePathBean(@Value("${path}") List<Resource> templates) {
this.templates = templates;
}
}

Spring 事件

1
Event handling in the ApplicationContext is provided through the ApplicationEvent class and the ApplicationListener interface. If a bean that implements the ApplicationListener interface is deployed into the context, every time an ApplicationEvent gets published to the ApplicationContext, that bean is notified. Essentially, this is the standard Observer design pattern.

定义事件

1
2
3
4
5
6
7
8
@Getter
public class PaymentEvent extends ApplicationEvent {
private final String account;
public PaymentEvent(Object source, String account) {
super(source);
this.account = account;
}
}

定义监听

  • 实现 ApplicationListener 接口
1
2
3
4
5
6
7
8
@Component
public class CustomerEventListenerV2 implements ApplicationListener<EmailEvent> {

@Override
public void onApplicationEvent(EmailEvent event) {
// 事件处理
}
}
  • 注解@EventListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class CustomerEventListener {

@EventListener(classes = PaymentEvent.class)
public void processPaymentEvent(PaymentEvent event) {
// 事件处理
}

// 官方文档描述当监听多个事件时,不允许定义任何参数 但是实际测试可以定义一个参数
// The event classes that this listener handles.
// <p>If this attribute is specified with a single value, the
// annotated method may optionally accept a single parameter.
// However, if this attribute is specified with multiple values,
// the annotated method must <em>not</em> declare any parameters.
@EventListener(classes = {PaymentEvent.class, EmailEvent.class})
public void processBlockedListEvent(ApplicationEvent event) {
if (event instanceof PaymentEvent) {
System.out.println("接收到事件:" + ((PaymentEvent) event).getAccount());
}

if (event instanceof EmailEvent) {
System.out.println("接收到事件:" + ((EmailEvent) event).getAddress());
}
}

}

事件的处理后返回下一个事件

1
2
3
4
5
6
7
8
9
10
@Component
@Slf4j
public class CustomerEventListenerV2 {
@EventListener(classes = SendGoodEvent.class)
public PaymentEvent onApplicationEvent(SendGoodEvent event) {
// 事件处理
log.info("订单发货:{}", event.getName());
return new PaymentEvent(event, "5000");
}
}

异步监听事件处理

实现事件异步监听只需要在监听方法上增加注解**@Async**即可.

  • If an asynchronous event listener throws an Exception, it is not propagated to the caller. See AsyncUncaughtExceptionHandler for more details.
  • 不支持事件传播
  • Asynchronous event listener methods cannot publish a subsequent event by returning a value. If you need to publish another event as the result of the processing, inject an ApplicationEventPublisher to publish the event manually.
1
2
3
4
5
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {

}

事件优先级

1
2
3
4
5
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}

Event SpEL

这个暂时不看

新特性

As of Spring 4.2, the event infrastructure has been significantly improved and offers an annotation-based model as well as the ability to publish any arbitrary event (that is, an object that does not necessarily extend from ApplicationEvent). When such an object is published, we wrap it in an event for you.

Spring4.2 之后,可以不继承 ApplicationEvent,直接以 model 形式实现事件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 定义事件
@Getter
public class SendGoodEvent{
private final String name;
private final long count;
public SendGoodEvent(String name, long count) {
this.name = name;
this.count = count;
}
}

// 事件监听器
@Component
@Slf4j
public class CustomerEventListenerV2 {
@EventListener(classes = SendGoodEvent.class)
public void onApplicationEvent(SendGoodEvent event) {
// 事件处理
log.info("订单发货:{}", event.getName());
}
}

// 源码
@FunctionalInterface
public interface ApplicationEventPublisher {

default void publishEvent(ApplicationEvent event) {
publishEvent((Object) event);
}
// Spring 4.2之后新增的事件发布方法
void publishEvent(Object event);

}

Spring Boot Json 浅析

1
2
3
在Spring中使用`@ResponseBody`注解可以将方法返回的对象序列化成JSON.

Spring Boot使用Jackson来完成JSON的序列化和反序列化操作.

简介

  • @JsonIgnore 使用@JsonIgnore 注解的属性不会被序列化
  • @JsonFormat(pattern = “yyyy-MM-dd”) 指定序列化格式
  • @JsonNaming 用于指定一个命名策略,作用于类或者属性上。Jackson 自带了多种命名策略,你可以实现自己的命名策略,比如输出的 key 由 Java 命名方式转为下面线命名方法 —— userName 转化为 user-name。
  • @JsonView

自定义 ObjectMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.text.SimpleDateFormat;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper getObjectMapper(){
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
return mapper;
}
}

定义对象

1
2
3
4
5
6
7
8
9
10
11
public static class Car {
private String name;

@JsonIgnore
private String band;

@JsonFormat(pattern = "yyyy-MM-dd")
private Date birth;

private BigDecimal price;
}

对象序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void writeJson() throws JsonProcessingException {

ObjectMapper mapper = new ObjectMapper();
Car car = new Car();
car.setName("A8L");
car.setBand("Audi");
car.setBirth(new Date());
final String value = mapper.writeValueAsString(car);

logger.info(value);
}
// 10:37:37.557 [Test worker] INFO zlin.site.framework.JackjsonTest - {"name":"A8L","birth":"2021-07-24"}

对象反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void readJsonAsClassInstance() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();

String json = "{\"name\":\"A8L\",\"band\":\"Audi\",\"birth\":1627101288204,\"price\":900000}";
final Car car = mapper.readValue(json, Car.class);
logger.info(car.band);

final JsonNode readTree = mapper.readTree(json);
final String name = readTree.get("name").asText();
logger.info("name: {}", name);

// 凡序列化一个对象的属性为对象这种情况
String jsonHasObjectProperties = "{\"name\":\"A8L\",\"birth\":\"2021-07-24\",\"price\":900000,\"color\":{\"name\":\"read\"}}";
final JsonNode jsonNode = mapper.readTree(jsonHasObjectProperties);
final String color = jsonNode.get("color").get("name").asText();
logger.info("color={}",color);
}

集合对象反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 在controller方法中会自动转为指定对象
@RequestMapping("updatecar")
@ResponseBody
public int updateUser(@RequestBody List<Car> list){
return list.size();
}

// 这种方式不能将对象反序列化为指定类型对象
@Test
public void readJsonAsClassInstanceList() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();

String jsonArray = "[{\"name\":\"A8L\",\"band\":\"Audi\",\"birth\":1627101288204,\"price\":900000},{\"name\":\"A8L\",\"band\":\"Audi\",\"birth\":1627101288204,\"price\":900000}]";
List<Car> list = mapper.readValue(jsonArray, List.class);
for (Car car : list) {
logger.info("name:{}", car.getBand());
}
}
// java.util.LinkedHashMap cannot be cast to zlin.site.framework.JackjsonTest$Car
// java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to zlin.site.framework.JackjsonTest$Car

// 下面这种方式OK
@Test
public void readJsonAsClassInstanceList2() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();

String jsonArray = "[{\"name\":\"A8L\",\"band\":\"Audi\",\"birth\":1627101288204,\"price\":900000},{\"name\":\"A8L\",\"band\":\"Audi\",\"birth\":1627101288204,\"price\":900000}]";
JavaType type = mapper.getTypeFactory().constructParametricType(List.class, Car.class);

List<Car> list = mapper.readValue(jsonArray, type);
for (Car car : list) {
logger.info("name:{}", car.getName());
}
}

自定义对象序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 自定义序列化
public static class PersonSerializer extends JsonSerializer<Person> {
@Override
public void serialize(Person person, JsonGenerator generator, SerializerProvider serializerProvider) throws IOException {
generator.writeStartObject();
generator.writeStringField("user-name", person.getUsername());
generator.writeNumberField("age", person.getAge() + 20);
generator.writeEndObject();
}
}

// 使用指定的序列化方式
@JsonSerialize(using = PersonSerializer.class)
public static class Person implements Serializable {
private String username;
private String gender;
private int age;
}

@Test
public void customizeSerializer() throws JsonProcessingException {
Person person = new Person();
person.setUsername("小汪");
person.setGender("女");
person.setAge(19);
ObjectMapper mapper = new ObjectMapper();

final String value = mapper.writeValueAsString(person);
logger.info("Person={}", value);
}
// 13:17:56.356 [Test worker] INFO zlin.site.framework.JackjsonTest - Person={"user-name":"小汪","age":39}

自定义反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static class PersonDeserializer extends JsonDeserializer<Person> {
@Override
public Person deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
String userName = node.get("user-name").asText();
final int age = node.get("age").asInt();
Person p = new Person();
p.setUsername(userName);
p.setGender("未知");
p.setAge(age - 20);
return p;
}
}
// 在类上添加反序列化注解 @JsonDeserialize(using = PersonDeserializer.class)

@Test
public void customizeDeserializer() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String personJson = "{\"user-name\":\"小汪\",\"age\":39}";

final Person person = mapper.readValue(personJson, Person.class);

logger.info("person name = {}", person.getUsername());

}
// 13:29:32.478 [Test worker] INFO zlin.site.framework.JackjsonTest - person name = 小汪

Logback 使用

官方文档

http://logback.qos.ch/manual/appenders.html

依赖

默认 Springboot 自带依赖

1
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-logging'

配置

encoder 节点

<encoder>表示对日志进行编码:

  • %d{HH: mm:ss.SSS}——日志输出时间。
  • %thread——输出日志的进程名字,这在 Web 应用以及异步任务处理中很有用。
  • %-5level——日志级别,并且使用 5 个字符靠左对齐。
  • %logger{36}——日志输出者的名字。
  • %msg——日志消息。
  • %n——平台的换行符。

Root 子节点

root 节点是必选节点,用来指定最基础的日志输出级别,只有一个 level 属性,用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,不能设置为 INHERITED 或者同义词 NULL。

默认是 DEBUG。可以包含零个或多个元素,标识这个 appender 将会添加到这个 logger。

1
2
3
4
<root level="debug">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>

logger 子节点

<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender><logger>仅有一个 name 属性,一个可选的 level 和一个可选的 addtivity 属性。

  • name:用来指定受此 logger 约束的某一个包或者具体的某一个类。
  • level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,还有一个特俗值 INHERITED 或者同义词 NULL,代表强制执行上级的级别。如果未设置此属性,那么当前 logger 将会继承上级的级别。
  • addtivity:是否向上级 logger 传递打印信息。默认是 true。
1
2
3
4
5
6
7
8
9
10
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>

<logger name="zlin.site.framework.generator.controller.StudentController" level="WARN"/>

<!-- additivity=false 则当前logger配置只会输出到指定appender中 -->
<logger name="zlin.site.framework.generator.controller.StudentController" level="INFO" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引入profiles配置文件中的变量 -->
<springProperty scope="context" name="LOG_PATH" source="log.path" defaultValue="logAll"/>
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="app"/>

<!-- 输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<!-- 输出到文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 打包策略配置 -->
<fileNamePattern>${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
<!-- 单个日志文件大小限制 -->
<maxFileSize>100MB</maxFileSize>
<!-- 保留历史文件数 -->
<maxHistory>60</maxHistory>
<!-- 日志文件最大数 -->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>

<encoder>
<pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
</encoder>
</appender>

<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.error.log</file>
<!-- 日志过滤器配置 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>

<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 打包策略配置 -->
<fileNamePattern>${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.error.log.%i.gz</fileNamePattern>
<!-- 单个日志文件大小限制 -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>

<encoder>
<pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
</encoder>
</appender>

<appender name="STUDENT" class="ch.qos.logback.core.FileAppender">
<file>${LOG_PATH}/student.log</file>
<encoder>
<pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
</encoder>
</appender>

<!-- 分环境输出不同日志 name的值是profile文件名 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>

<logger name="zlin.site.framework" level="INFO">
<appender-ref ref="FILE"/>
</logger>

<!-- additivity=false 则当前logger配置只会输出到指定appender中 -->
<logger name="zlin.site.framework.generator.controller.StudentController" level="INFO" additivity="false">
<appender-ref ref="STUDENT"/>
</logger>
</springProfile>

<!-- 分环境输出不同日志 name的值是profile文件名 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>

<logger name="zlin.site.framework" level="INFO">
<appender-ref ref="FILE"/>
</logger>

<logger name="zlin.site.framework.generator.controller.StudentController" level="INFO" additivity="false">
<appender-ref ref="STUDENT"/>
</logger>
</springProfile>

</configuration>