文章目录
- 一,序
- 二,需求
- 三,代码实现
- 1. 代码结构
- 2. 完整代码备份
- 四,bug1 详情
- 1. 运行准备
- 1. )将 application.yml 文件active设置为test
- 2.)修改jdbc-mysql.properties 数据库参数设为实际值
- 3.)注释 RefreshLogPoolManager 40-45行代码
- 2. bug现象
- 1.)数据表记录超过100后,执行清理操作时,后台报错
- 2.)mysql 执行`SHOW PROCESSLIST` 发现数据库连接处于 Waiting for table metadata lock 状态
- 3. 疑问
- 五,bug2 详情
- 1. 运行准备
- 2. bug现象
- 3. 疑问
一,序
俗话说:但行好事,莫问前程,心之所向,无问西东
编程亦然,coding多了,就会遇到各种各样奇怪的问题,真是让人欢喜让人忧啊!
这不,小C最近实现了一个使用mysql数据库来保存日志的功能,不幸的是,遇到两个难解的问题,现拿出来,希望各位见多识广的大佬能帮忙分析,小可不胜感激!
二,需求
- 系统需要保存一定周期内的日志
- 系统自动清理超过设定周期的过期日志文件
- 日志数据表与业务系统是同一数据库
- 配置文件可能采用参数外部文件注入(spring.config.location指定)、启动时指定环境(spring.profiles.active指定)或docker-compose环境变量注入(environment)
三,代码实现
1. 代码结构
2. 完整代码备份
如何使用下面的备份文件恢复成原始的如上图的项目代码,请移步查阅:神奇代码恢复工具
因工具不能备份非文本文件,请大家在src\main\resources
下新建/data/pic/
目录并放入不少于4张jpg图片。
//goto docker\docker-compose.yml
version: '3'
networks:
default:
name: devops
driver: bridge
ipam:
config:
- subnet: 172.88.88.0/24
services:
hello:
image: registry.cn-shanghai.aliyuncs.com/00fly/springboot-log:1.0.0
container_name: hello
deploy:
resources:
limits:
cpus: '1'
memory: 300M
reservations:
memory: 200M
ports:
- 8080:8081
environment:
JAVA_OPTS: -server -Xms200m -Xmx200m -Djava.security.egd=file:/dev/./urandom
SPRING_DATASOURCE_USERNAME: test
SPRING_DATASOURCE_PASSWORD: test123
SPRING_DATASOURCE_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
SPRING_DATASOURCE_URL: jdbc:mysql://172.88.88.11:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
restart: on-failure
logging:
driver: json-file
options:
max-size: 5m
max-file: '1'
entrypoint: 'sh wait-for.sh 172.88.88.11:3306 -- java -jar /app.jar'
hello-simple:
image: registry.cn-shanghai.aliyuncs.com/00fly/springboot-log:1.0.0
container_name: hello-simple
deploy:
resources:
limits:
cpus: '1'
memory: 300M
reservations:
memory: 200M
environment:
JAVA_OPTS: -server -Xms200m -Xmx200m -Djava.security.egd=file:/dev/./urandom
SPRING_DATASOURCE_USERNAME: test
SPRING_DATASOURCE_PASSWORD: test123
SPRING_DATASOURCE_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
SPRING_DATASOURCE_URL: jdbc:mysql://172.88.88.11:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
restart: on-failure
logging:
driver: json-file
options:
max-size: 5m
max-file: '1'
entrypoint: 'sh wait-for.sh 172.88.88.11:3306 -- java -jar /app.jar --noweb'
mysql8:
image: mysql:8.0
container_name: mysql-8
deploy:
resources:
limits:
cpus: '2'
memory: 500M
reservations:
memory: 300M
volumes:
- ./mysql8:/var/lib/mysql/
ports:
- 3307:3306
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: root123
MYSQL_USER: test
MYSQL_PASSWORD: test123
MYSQL_DATABASE: test
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
--explicit_defaults_for_timestamp=true
--lower_case_table_names=1
--sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
restart: on-failure
logging:
driver: 'json-file'
options:
max-size: '5m'
max-file: '1'
networks:
default:
ipv4_address: 172.88.88.11
//goto docker\restart-simple.sh
#!/bin/bash
docker-compose down && docker-compose up -d hello-simple -d mysql8 && docker stats
//goto docker\restart-web.sh
#!/bin/bash
docker-compose down && docker-compose up -d hello -d mysql8 && docker stats
//goto docker\restart.sh
#!/bin/bash
docker-compose down && docker system prune -f && docker-compose up -d && docker stats
//goto docker\stop.sh
#!/bin/bash
docker-compose down
//goto docker\wait-for.sh
#!/bin/sh
TIMEOUT=15
QUIET=0
echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}
usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet Do not output any status messages
-t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit "$exitcode"
}
wait_for() {
for i in `seq $TIMEOUT` ; do
nc -z "$HOST" "$PORT" > /dev/null 2>&1
result=$?
if [ $result -eq 0 ] ; then
if [ $# -gt 0 ] ; then
exec "$@"
fi
exit 0
fi
sleep 1
done
echo "Operation timed out" >&2
exit 1
}
while [ $# -gt 0 ]
do
case "$1" in
*:* )
HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
shift 1
;;
-q | --quiet)
QUIET=1
shift 1
;;
-t)
TIMEOUT="$2"
if [ "$TIMEOUT" = "" ]; then break; fi
shift 2
;;
--timeout=*)
TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
break
;;
--help)
usage 0
;;
*)
echoerr "Unknown argument: $1"
usage 1
;;
esac
done
if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi
wait_for "$@"
//goto Dockerfile
#基础镜像
FROM openjdk:8-jre-alpine
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
COPY docker/wait-for.sh /
RUN chmod +x /wait-for.sh
#拷贝发布包
COPY target/*.jar /app.jar
EXPOSE 8080
CMD ["--server.port=8080"]
#启动脚本
ENTRYPOINT ["java","-Xmx128m","-Xms128m","-jar","/app.jar"]
//goto pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fly</groupId>
<artifactId>springboot-log</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.build.timestamp.format>yyyyMMdd-HH</maven.build.timestamp.format>
<docker.hub>registry.cn-shanghai.aliyuncs.com</docker.hub>
<java.version>1.8</java.version>
<skipTests>true</skipTests>
</properties>
<dependencies>
<!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!-- 异步日志,需要加入disruptor依赖 -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
<!-- Provided -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>-->
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 阿里云maven仓库 -->
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 添加docker-maven插件 -->
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.41.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
<goal>remove</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 连接到带docker环境的linux服务器编译image -->
<!--<dockerHost>http://192.168.182.10:2375</dockerHost>-->
<!-- Docker 推送镜像仓库地址 -->
<pushRegistry>${docker.hub}</pushRegistry>
<images>
<image>
<!--推送到私有镜像仓库,镜像名需要添加仓库地址 -->
<name>
${docker.hub}/00fly/${project.artifactId}:${project.version}-UTC-${maven.build.timestamp}</name>
<!--定义镜像构建行为 -->
<build>
<dockerFileDir>${project.basedir}</dockerFileDir>
</build>
</image>
<image>
<name>
${docker.hub}/00fly/${project.artifactId}:${project.version}</name>
<build>
<dockerFileDir>${project.basedir}</dockerFileDir>
</build>
</image>
</images>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/**</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
//goto src\main\java\com\fly\core\aop\RunTimeAspect.java
package com.fly.core.aop;
import java.lang.reflect.Method;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import lombok.extern.slf4j.Slf4j;
/**
*
* aop 打印运行时间
*
* @author 00fly
* @version [版本号, 2018年12月2日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Slf4j
@Aspect
@Component
public class RunTimeAspect
{
/**
* 构造方法
*/
public RunTimeAspect()
{
super();
}
/**
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("within(com.fly.hello.web..*||com.fly.hello.service..*)")
public Object around(ProceedingJoinPoint joinPoint)
throws Throwable
{
String className = joinPoint.getTarget().getClass().getSimpleName();
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = new StringBuffer(className).append('.').append(method.getName()).toString();
final StopWatch clock = new StopWatch();
clock.start(methodName);
Object object = joinPoint.proceed();
clock.stop();
log.info("running {} ms, method = {}", clock.getTotalTimeMillis(), clock.getLastTaskName());
return object;
}
}
//goto src\main\java\com\fly\core\config\AsyncThreadPoolConfig.java
package com.fly.core.config;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import lombok.extern.slf4j.Slf4j;
/**
*
* 异步线程池配置
*
* @author 00fly
* @version [版本号, 2023年10月22日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Slf4j
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig implements AsyncConfigurer
{
@Bean
ThreadPoolTaskExecutor taskExecutor()
{
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Math.max(processors, 5));
executor.setMaxPoolSize(Math.max(processors, 5) * 2);
executor.setQueueCapacity(10000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("asyncTask-");
// ThreadPoolExecutor类有几个内部实现类来处理这类情况:
// AbortPolicy 丢弃任务,抛运行时异常
// CallerRunsPolicy 执行任务
// DiscardPolicy 忽视,什么都不会发生
// DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
return executor;
}
@Override
public Executor getAsyncExecutor()
{
return taskExecutor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler()
{
return (ex, method, params) -> {
log.info("Exception message - {}", ex.getMessage());
log.info("Method name - {}", method.getName());
for (Object param : params)
{
log.info("Parameter value - {}", param);
}
};
}
}
//goto src\main\java\com\fly\core\config\DataSourceConfig.java
package com.fly.core.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;
@Configuration
@Profile("dev")
@PropertySource("classpath:jdbc-h2.properties")
class H2Config
{
}
@Configuration
@Profile({"test", "prod"})
@PropertySource("classpath:jdbc-mysql.properties")
class MysqlConfig
{
}
//goto src\main\java\com\fly\core\config\Knife4jConfig.java
package com.fly.core.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* knife4j
*
* @author jack
*/
@Configuration
@EnableKnife4j
@EnableSwagger2
@ConditionalOnWebApplication
public class Knife4jConfig
{
@Value("${knife4j.enable:false}")
private boolean enable;
@Bean
Docket defaultApi2()
{
return new Docket(DocumentationType.SWAGGER_2).enable(enable)
.apiInfo(apiInfo())
.groupName("非异步接口组")
.select()
.apis(RequestHandlerSelectors.basePackage("com.fly.hello.web"))
.paths(PathSelectors.regex("/(demo|knife|show)/.*")) // 针对RequestMapping
// .paths(PathSelectors.regex("(?!.*async).*")) //url中不包含async
.build();
}
private ApiInfo apiInfo()
{
return new ApiInfoBuilder().title("数据接口API").description("接口文档").termsOfServiceUrl("http://00fly.online/").version("1.0.0").build();
}
}
//goto src\main\java\com\fly\core\config\ScheduleThreadPoolConfig.java
package com.fly.core.config;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
*
* Schedule线程池配置
*
* @author 00fly
* @version [版本号, 2023年10月22日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Configuration
public class ScheduleThreadPoolConfig implements SchedulingConfigurer
{
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar)
{
ScheduledExecutorService service= new ScheduledThreadPoolExecutor(8, new CustomizableThreadFactory("schedule-pool-"));
taskRegistrar.setScheduler(service);
}
}
//goto src\main\java\com\fly\core\config\WebMvcConfig.java
package com.fly.core.config;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import lombok.extern.slf4j.Slf4j;
/**
*
* mvc配置
*
* @author 00fly
* @version [版本号, 2021年4月23日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Slf4j
@Configuration
@ConditionalOnWebApplication
public class WebMvcConfig implements WebMvcConfigurer
{
@Override
public void configureMessageConverters(final List<HttpMessageConverter<?>> converters)
{
converters.add(this.stringHttpMessageConverter());
converters.add(this.mappingJackson2HttpMessageConverter());
}
@Override
public void configureContentNegotiation(final ContentNegotiationConfigurer configurer)
{
// 是否通过请求Url的扩展名来决定mediatype
// TODO:放开,状态码406,待解决
// configurer.favorPathExtension(false).favorParameter(false).ignoreUnknownPathExtensions(false).defaultContentType(MediaType.APPLICATION_JSON);
final Map<String, MediaType> mediaTypes = new ConcurrentHashMap<>(3);
mediaTypes.put("atom", MediaType.APPLICATION_ATOM_XML);
mediaTypes.put("html", MediaType.TEXT_HTML);
mediaTypes.put("json", MediaType.APPLICATION_JSON);
configurer.mediaTypes(mediaTypes);
}
@Bean
public StringHttpMessageConverter stringHttpMessageConverter()
{
return new StringHttpMessageConverter();
}
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter()
{
final MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
final List<MediaType> list = new ArrayList<>();
list.add(MediaType.APPLICATION_JSON);
list.add(MediaType.APPLICATION_XML);
list.add(MediaType.TEXT_PLAIN);
list.add(MediaType.TEXT_HTML);
list.add(MediaType.TEXT_XML);
messageConverter.setSupportedMediaTypes(list);
return messageConverter;
}
/**
* 等价于mvc中<mvc:view-controller path="/" view-name="redirect:doc.html" /><br>
* 等价于mvc中<mvc:view-controller path="/index" view-name="index" />
*
* @param registry
*/
@Override
public void addViewControllers(final ViewControllerRegistry registry)
{
registry.addViewController("/").setViewName("redirect:doc.html");
registry.addViewController("/index").setViewName("index.html");
registry.addViewController("/in").setViewName("auto.html");
}
/**
* 加载静态资源文件或文件映射
*/
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry)
{
try
{
registry.addResourceHandler("/upload/**").addResourceLocations("file:" + new File("./upload/").getCanonicalPath() + "/");
}
catch (IOException e)
{
log.error(e.getMessage(), e);
}
}
}
//goto src\main\java\com\fly\core\JsonResult.java
package com.fly.core;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
*
* 结果对象
*
* @author 00fly
* @version [版本号, 2021年5月2日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Data
@ApiModel(description = "Json格式消息体")
public class JsonResult<T>
{
@ApiModelProperty(value = "数据对象")
private T data;
@ApiModelProperty(value = "是否成功", required = true, example = "true")
private boolean success;
@ApiModelProperty(value = "错误码")
private String errorCode;
@ApiModelProperty(value = "提示信息")
private String message;
public JsonResult()
{
super();
}
public static <T> JsonResult<T> success(T data)
{
JsonResult<T> r = new JsonResult<>();
r.setData(data);
r.setSuccess(true);
return r;
}
public static JsonResult<?> success()
{
JsonResult<Object> r = new JsonResult<>();
r.setSuccess(true);
return r;
}
public static JsonResult<Object> error(String code, String msg)
{
JsonResult<Object> r = new JsonResult<>();
r.setSuccess(false);
r.setErrorCode(code);
r.setMessage(msg);
return r;
}
public static JsonResult<Object> error(String msg)
{
return error("500", msg);
}
}
//goto src\main\java\com\fly\core\log\CleanLogJob.java
package com.fly.core.log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.fly.core.utils.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
/**
*
* CleanLogJob
*
* @author 00fly
* @version [版本号, 2022年11月30日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Slf4j
@Component
public class CleanLogJob
{
private String welcome = "Hello 00fly in ScheduleJob, profile: " + SpringContextUtils.getActiveProfile();
@Autowired
private JdbcTemplate jdbcTemplate;
@Scheduled(fixedRate = 10000L)
public void run()
{
final long count = jdbcTemplate.queryForObject("select count(*) from boot_log", Long.class);
log.info("------------------- boot_log count: {} --------------------", count);
if (count > 100)
{
log.info("###### clean table boot_log ######");
// truncate 执行后将重新水平线和索引(id从零开始)
// MySQL5.5版本开始引入了MDL锁(metadata lock),来保护表的元数据信息,用于解决或者保证DDL操作与DML操作之间的一致性
// 如果表上有活动事务(未提交或回滚),执行truncate table,请求写入的会话会等待在Metadata lock wait
// 故尽量不要使用truncate table
jdbcTemplate.execute("truncate table boot_log");
// jdbcTemplate.execute("delete from boot_log");
}
}
/**
* 测试日志打印
*/
@Scheduled(fixedRate = 500)
public void run3()
{
log.trace("★★★★★★★★ {}", welcome);
log.debug("★★★★★★★★ {}", welcome);
log.info("★★★★★★★★ {}", welcome);
log.warn("★★★★★★★★ {}", welcome);
}
}
//goto src\main\java\com\fly\core\log\Log4j2Configuration.java
package com.fly.core.log;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* 日志数据源注入,注意不能依赖web事件
*/
@Component
public class Log4j2Configuration implements ApplicationListener<ContextRefreshedEvent>
{
@Autowired
DataSource dataSource;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent)
{
// 初始化日志数据源
Assert.notNull(dataSource, "dataSource can not be null");
RefreshLogPoolManager.init(dataSource);
}
}
//goto src\main\java\com\fly\core\log\RefreshLogPoolManager.java
package com.fly.core.log;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.fly.core.utils.SpringContextUtils;
/**
*
* 可刷新日志数据库数据源
*
* @author 00fly
* @version [版本号, 2023年3月27日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public final class RefreshLogPoolManager
{
private static DataSource dataSource;
public static void init(DataSource ds)
{
if (dataSource == null)
{
System.out.println("------------------- dataSource get in init -------------------" + ds);
dataSource = ds;
}
}
/**
* getConnection
*
* @return
* @throws SQLException
* @see [类、类#方法、类#成员]
*/
public static Connection getConnection()
throws SQLException
{
if (dataSource == null)
{
dataSource = SpringContextUtils.getBean(DataSource.class);
System.out.println("------------------- dataSource get by springContextUtils -------------------" + dataSource);
return dataSource.getConnection();
}
System.out.println(dataSource);
return dataSource.getConnection();
}
}
//goto src\main\java\com\fly\core\utils\SpringContextUtils.java
package com.fly.core.utils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.servlet.ServletContext;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import lombok.extern.slf4j.Slf4j;
/**
* Spring Context 工具类
*
* @author 00fly
*
*/
@Slf4j
@Component
public class SpringContextUtils implements ApplicationContextAware
{
private static ApplicationContext applicationContext;
/**
* web服务器基准URL
*/
private static String SERVER_BASE_URL = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException
{
log.info("###### execute setApplicationContext ######");
SpringContextUtils.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext()
{
return applicationContext;
}
public static <T> T getBean(Class<T> clazz)
{
Assert.notNull(applicationContext, "applicationContext is null");
return applicationContext.getBean(clazz);
}
/**
* execute @PostConstruct May be SpringContextUtils not inited, throw NullPointerException
*
* @return
*/
public static String getActiveProfile()
{
Assert.notNull(applicationContext, "applicationContext is null");
String[] profiles = applicationContext.getEnvironment().getActiveProfiles();
return StringUtils.join(profiles, ",");
}
/**
* can use in @PostConstruct
*
* @param context
* @return
*/
public static String getActiveProfile(ApplicationContext context)
{
Assert.notNull(context, "context is null");
String[] profiles = context.getEnvironment().getActiveProfiles();
return StringUtils.join(profiles, ",");
}
/**
* get web服务基准地址,一般为 http://${ip}:${port}/${contentPath}
*
* @return
* @throws UnknownHostException
* @see [类、类#方法、类#成员]
*/
public static String getServerBaseURL()
throws UnknownHostException
{
if (SERVER_BASE_URL == null)
{
ServletContext servletContext = getBean(ServletContext.class);
Assert.notNull(servletContext, "servletContext is null");
String ip = InetAddress.getLocalHost().getHostAddress();
SERVER_BASE_URL = "http://" + ip + ":" + getProperty("server.port") + servletContext.getContextPath();
}
return SERVER_BASE_URL;
}
/**
* getProperty
*
* @param key eg:server.port
* @return
* @see [类、类#方法、类#成员]
*/
public static String getProperty(String key)
{
return applicationContext.getEnvironment().getProperty(key, "");
}
}
//goto src\main\java\com\fly\hello\runner\WebStartedRunner.java
package com.fly.hello.runner;
import java.io.IOException;
import javax.servlet.ServletContext;
import org.apache.commons.lang3.SystemUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import com.fly.core.utils.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@Configuration
@ConditionalOnWebApplication
public class WebStartedRunner implements ApplicationListener<WebServerInitializedEvent>
{
@Value("${server.port}")
Integer port;
@Autowired
ServletContext servletContext;
@Override
public void onApplicationEvent(WebServerInitializedEvent event)
{
int port = event.getWebServer().getPort();
log.info("#### server.port: {} ####", port);
}
@Bean
CommandLineRunner run()
{
return args -> {
openBrowser();
};
}
private void openBrowser()
throws IOException
{
String url;
switch (SpringContextUtils.getActiveProfile())
{
case "prod":
log.info("请修改hosts: 127.0.0.1 test.00fly.online");
url = "https://test.00fly.online:" + port + servletContext.getContextPath();
break;
default:
url = SpringContextUtils.getServerBaseURL();
break;
}
if (SystemUtils.IS_OS_WINDOWS && port > 0)
{
log.info("★★★★★★★★ now open Browser ★★★★★★★★ ");
Runtime.getRuntime().exec("cmd /c start /min " + url);
Runtime.getRuntime().exec("cmd /c start /min " + url + "/index");
if (SpringContextUtils.getActiveProfile().contains("dev"))
{
Runtime.getRuntime().exec("cmd /c start /min " + url + "/h2-console");
}
}
}
}
//goto src\main\java\com\fly\hello\web\DemoController.java
package com.fly.hello.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fly.core.JsonResult;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
@Api(tags = "演示排序接口")
@RestController
@RequestMapping("/demo")
public class DemoController
{
@GetMapping("/first")
@ApiOperationSupport(order = 100)
@ApiOperation("001 @ApiImplicitParams演示")
@ApiImplicitParams({@ApiImplicitParam(name = "key", value = "键", required = true), @ApiImplicitParam(name = "value", value = "值", required = true)})
public JsonResult<?> first(String key, String value)
{
return JsonResult.success();
}
@GetMapping("/second")
@ApiOperationSupport(order = 80)
@ApiOperation("002 @ApiImplicitParam演示")
@ApiImplicitParam(name = "key", value = "键", required = true)
public JsonResult<?> second(String key)
{
return JsonResult.success();
}
@GetMapping("/third")
@ApiOperationSupport(order = 60)
@ApiOperation("003 @Parameter演示")
@ApiImplicitParam(name = "key", value = "键,字符串", required = true)
public JsonResult<?> third(String key)
{
return JsonResult.success();
}
@GetMapping("/fourth")
@ApiOperationSupport(order = 40)
@ApiOperation("004 @Parameter演示")
@ApiImplicitParams({@ApiImplicitParam(name = "key", value = "键", required = true), @ApiImplicitParam(name = "value", value = "值", required = true)})
public JsonResult<?> fourth(String key, String value)
{
return JsonResult.success();
}
}
//goto src\main\java\com\fly\hello\web\PicDataController.java
package com.fly.hello.web;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.http.MediaType;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@Api(tags = "图片接口")
@Slf4j
@RestController
@RequestMapping("/show")
public class PicDataController
{
Resource[] resources;
List<Resource> resourceList = new ArrayList<>();
/**
* FIFO
*/
private Queue<Integer> quque = new ConcurrentLinkedQueue<>();
@PostConstruct
private void init()
{
try
{
resources = new PathMatchingResourcePatternResolver().getResources(ResourceUtils.CLASSPATH_URL_PREFIX + "data/pic/**/*.jpg");
Arrays.stream(resources).forEach(image -> {
resourceList.add(image);
log.info("add pic: {}", image.getFilename());
});
}
catch (IOException e)
{
log.error(e.getMessage(), e);
}
}
@ApiOperation("图片")
@GetMapping(value = {"/girl", "/pic"}, produces = MediaType.IMAGE_JPEG_VALUE)
public byte[] showPic1()
throws IOException
{
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(createImage(), "jpg", os);
return os.toByteArray();
}
/**
* createImage 生成图片
*
* @return
* @throws IOException
* @see [类、类#方法、类#成员]
*/
private synchronized BufferedImage createImage()
throws IOException
{
if (resources.length < 4)
{
log.info("############### 请在[resources/data/pic/]目录放入不少于4张jpg图片 ###############");
return new BufferedImage(400, 400, BufferedImage.TYPE_BYTE_GRAY);
}
// 保留3条记录
while (quque.size() > 3)
{
quque.poll();
}
// 新生成无重复数据
int index;
do
{
index = RandomUtils.nextInt(0, resources.length);
} while (quque.contains(index));
quque.add(index);
log.info("add: {}, quque:{}", index, quque);
// 取图片
return ImageIO.read(resources[index].getInputStream());
}
}
//goto src\main\java\com\fly\HelloApplication.java
package com.fly;
import javax.annotation.PreDestroy;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import com.fly.core.utils.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@EnableAsync
@EnableScheduling
@ServletComponentScan
@SpringBootApplication
public class HelloApplication
{
@Autowired
SpringContextUtils springContextUtils;
public static void main(String[] args)
{
// args = new String[] {"--noweb"};
boolean web = !ArrayUtils.contains(args, "--noweb");
log.info("############### with Web Configuration: {} #############", web);
if (RandomUtils.nextBoolean())
{
new SpringApplicationBuilder(HelloApplication.class).web(web ? WebApplicationType.SERVLET : WebApplicationType.NONE).run(args);
}
else
{
SpringApplication application = new SpringApplication(HelloApplication.class);
application.setWebApplicationType(web ? WebApplicationType.SERVLET : WebApplicationType.NONE);
application.run(args);
}
}
@PreDestroy
public void destroy()
{
log.info("###### destroy ######");
}
}
//goto src\main\resources\application-dev.yml
spring:
h2:
console:
enabled: true
path: /h2-console
settings:
web-allow-others: true
//goto src\main\resources\application.yml
server:
port: 8081
servlet:
context-path: /
session:
timeout: 1800
spring:
datasource:
url: ${druid.url}
username: ${druid.username}
password: ${druid.password}
driverClassName: ${druid.driverClassName}
type: com.alibaba.druid.pool.DruidDataSource
sqlScriptEncoding: utf-8
# initialization-mode: embedded
# initialization-mode: never
initialization-mode: always
schema: classpath:schema.sql
continue-on-error: true
druid:
initial-size: 5 # 初始化大小
min-idle: 10 # 最小连接数
max-active: 20 # 最大连接数
max-wait: 60000 # 获取连接时的最大等待时间
min-evictable-idle-time-millis: 300000 # 一个连接在池中最小生存的时间,单位是毫秒
time-between-eviction-runs-millis: 60000 # 多久才进行一次检测需要关闭的空闲连接,单位是毫秒
validation-query: SELECT 1 # 检测连接是否有效的 SQL语句,为空时以下三个配置均无效
test-on-borrow: true # 申请连接时执行validationQuery检测连接是否有效,默认true,开启后会降低性能
test-on-return: true # 归还连接时执行validationQuery检测连接是否有效,默认false,开启后会降低性能
test-while-idle: true # 申请连接时如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效,默认false,建议开启,不影响性能
devtools:
restart:
exclude: jdbc-log.properties
profiles:
active:
- test
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
knife4j:
enable: true
logging:
level:
root: info
org.springframework.web: info
//goto src\main\resources\jdbc-h2.properties
druid.username=sa
druid.password=
druid.url=jdbc:h2:mem:hello;database_to_upper=false
druid.driverClassName=org.h2.Driver
//goto src\main\resources\jdbc-mysql.properties
druid.url=jdbc:mysql://127.0.0.1:23306/hello?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
druid.driverClassName=com.mysql.cj.jdbc.Driver
druid.username=root
druid.password=root123
druid.filters=stat
druid.initialSize=2
druid.maxActive=20
druid.maxWait=60000
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000
druid.validationQuery=SELECT 1
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
druid.poolPreparedStatements=false
druid.maxPoolPreparedStatementPerConnectionSize=200
//goto src\main\resources\log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 该xml配置中,xml元素大小写不敏感 -->
<!-- status="off",log4j2把自身事件记录到控制台的配置,off表示不记录,日志级别以及优先级排序: off > fatal > error > warn > info > debug > trace > all -->
<!-- monitorInterval表示检测更改配置的时间,单位是秒,最小间隔为5秒,0或负数表示不检测 -->
<configuration status="off" monitorInterval="0">
<!-- 常量引用 -->
<properties>
<property name="LOG_HOME">../logs</property>
<property name="PROJECT">boot-hello</property>
<property name="FORMAT">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</property>
</properties>
<!-- appender用于接收各种日志 -->
<appenders>
<!-- 常见的输出到console,常用于开发环境中,默认是system_err,还有一个system_out -->
<console name="Console" target="system_out">
<!-- appender级别的日志过滤 -->
<!-- <thresholdFilter level="info" onMatch="accept" onMismatch="deny"/> -->
<patternLayout pattern="${FORMAT}" />
</console>
<!-- bufferSize 没起作用,待排查 -->
<JDBC name="databaseAppender" bufferSize="20" tableName="boot_log">
<ConnectionFactory class="com.fly.core.log.RefreshLogPoolManager" method="getConnection" />
<Column name="event_id" pattern="%X{id}" />
<Column name="event_date" isEventTimestamp="true" />
<Column name="thread" pattern="%t %x" />
<Column name="class" pattern="%C" />
<Column name="`function`" pattern="%M" />
<Column name="message" pattern="%m" />
<Column name="exception" pattern="%ex{full}" />
<Column name="level" pattern="%level" />
<Column name="time" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
</JDBC>
<RollingRandomAccessFile name="RollingFileInfo" fileName="${LOG_HOME}/${PROJECT}/info.log" filePattern="${LOG_HOME}/${PROJECT}/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<ThresholdFilter level="error" onMatch="deny" onMismatch="neutral" />
<ThresholdFilter level="warn" onMatch="deny" onMismatch="neutral" />
<ThresholdFilter level="info" onMatch="accept" onMismatch="deny" />
</Filters>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n" />
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1" />
<SizeBasedTriggeringPolicy size="20 MB" />
</Policies>
<!-- DefaultRolloverStrategy不设置的话,max默认值为7,删除符合条件7天之前的日志 -->
<!-- info-%d{yyyy-MM-dd}-%i.log.gz保存20个日志文件,删除60天之外的日志 -->
<DefaultRolloverStrategy max="20">
<Delete basePath="${LOG_HOME}/${PROJECT}/" maxDepth="2">
<IfFileName glob="*/info-*.log.gz" />
<IfLastModified age="60d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
<RollingRandomAccessFile name="RollingFileWarn" fileName="${LOG_HOME}/${PROJECT}/warn.log" filePattern="${LOG_HOME}/${PROJECT}/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<ThresholdFilter level="error" onMatch="deny" onMismatch="neutral" />
<ThresholdFilter level="warn" onMatch="accept" onMismatch="deny" />
</Filters>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n" />
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1" />
<SizeBasedTriggeringPolicy size="20 MB" />
</Policies>
<DefaultRolloverStrategy max="20">
<Delete basePath="${LOG_HOME}/${PROJECT}/" maxDepth="2">
<IfFileName glob="*/warn-*.log.gz" />
<IfLastModified age="60d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
<RollingRandomAccessFile name="RollingFileError" fileName="${LOG_HOME}/${PROJECT}/error.log" filePattern="${LOG_HOME}/${PROJECT}/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log.gz">
<ThresholdFilter level="error" onMatch="accept" onMismatch="deny" />
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n" />
<Policies>
<TimeBasedTriggeringPolicy modulate="true" interval="1" />
<SizeBasedTriggeringPolicy size="20 MB" />
</Policies>
<DefaultRolloverStrategy max="20">
<Delete basePath="${LOG_HOME}/${PROJECT}/" maxDepth="2">
<IfFileName glob="*/error-*.log.gz" />
<IfLastModified age="60d" />
</Delete>
</DefaultRolloverStrategy>
</RollingRandomAccessFile>
</appenders>
<!-- 接收appender -->
<loggers>
<!--过滤掉spring的一些无用的debug信息 -->
<logger name="org.springframework" level="info" />
<!-- 定制包级别日志 -->
<!-- 自定义 logger 对象 includeLocation="false" 关闭日志记录的行号信息,开启的话会严重影响异步输出的性能 additivity="false" 不再继承 rootlogger对象 -->
<AsyncLogger name="com.fly.common" level="info" includeLocation="false" additivity="false">
<AppenderRef ref="Console" />
<AppenderRef ref="RollingFileInfo" />
<AppenderRef ref="RollingFileWarn" />
<AppenderRef ref="RollingFileError" />
</AsyncLogger>
<AsyncLogger name="com.fly.core.log.job" level="info" includeLocation="true" additivity="false">
<AppenderRef ref="Console" />
</AsyncLogger>
<!-- root logger,一般用于放置所有的appender -->
<root level="info">
<appender-ref ref="Console" />
<AppenderRef ref="databaseAppender" />
<appender-ref ref="RollingFileInfo" />
<appender-ref ref="RollingFileWarn" />
<appender-ref ref="RollingFileError" />
</root>
</loggers>
<!-- 最终实现:混合异步打印日志,日志文件中,只打印本级别日志,控制台打印全部日志, -->
</configuration>
//goto src\main\resources\schema.sql
DROP TABLE IF EXISTS `boot_log`;
CREATE TABLE IF NOT EXISTS boot_log (
`id` bigint NOT NULL AUTO_INCREMENT ,
`event_id` varchar(50) ,
`event_date` datetime ,
`thread` varchar(255) ,
`class` varchar(255) ,
`function` varchar(255) ,
`message` varchar(255) ,
`exception` text,
`level` varchar(255) ,
`time` datetime,
PRIMARY KEY (id)
);
//goto src\main\resources\static\index.html
<html>
<title>Hello World!</title>
<body>
<h2 align="center">
<a href="index">reload</a>
</h2>
<img src="show/girl" width="600" height="600" />
<img src="show/pic" width="600" height="600" />
</body>
</html>
//goto src\test\java\com\fly\hello\ApplicationTest.java
package com.fly.hello;
import java.util.Scanner;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit4.SpringRunner;
import com.fly.HelloApplication;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = HelloApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationTest
{
/**
* 注意:测试定时任务时,请勿使用dev环境,因为每个应用访问的是自己的内存数据库
*/
@Test
public void test()
{
try (Scanner sc = new Scanner(System.in))
{
do
{
log.info("------------输入x退出,回车换行继续------------");
} while (!"x".equalsIgnoreCase(sc.nextLine()));
log.info("------------成功退出------------");
}
}
}
//goto src\test\java\com\fly\hello\simple\ClassPathResourceTest.java
package com.fly.hello.simple;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.ResourceUtils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ClassPathResourceTest
{
@Test
public void test()
throws IOException
{
if (ResourceUtils.isFileURL(ResourceUtils.getURL("classpath:")))
{
File src = new ClassPathResource("application.properties").getFile();
String path = ResourceUtils.getURL("classpath:").getPath() + "log-jdbc.properties";
File dest = new File(path);
log.info("{} ==> {}", src.getCanonicalPath(), dest.getCanonicalPath());
FileUtils.copyFile(src, dest);
}
}
@Test
public void test2()
throws Exception
{
// 通过properties文件传递数据源配置
if (ResourceUtils.isFileURL(ResourceUtils.getURL("classpath:")))
{
String path = ResourceUtils.getURL("classpath:").getPath() + "jdbc-log.properties";
Collection<String> lines = new ArrayList<>();
lines.add("druid.url=");
lines.add("druid.username=");
lines.add("druid.password=");
lines.add("druid.driverClassName=");
try (OutputStream outputStream = new FileOutputStream(new File(path)))
{
IOUtils.writeLines(lines, null, outputStream, StandardCharsets.UTF_8);
}
// 读取
Resource resource = new ClassPathResource("jdbc-log.properties");
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
log.info("properties: {}", properties);
log.info("dataSource: {}", dataSource);
}
}
}
四,bug1 详情
1. 运行准备
1. )将 application.yml 文件active设置为test
profiles:
active:
- test
2.)修改jdbc-mysql.properties 数据库参数设为实际值
druid.url=jdbc:mysql://127.0.0.1:23306/hello?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
druid.driverClassName=com.mysql.cj.jdbc.Driver
druid.username=root
druid.password=root123
druid.filters=stat
druid.initialSize=2
druid.maxActive=20
druid.maxWait=60000
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000
druid.validationQuery=SELECT 1
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
druid.poolPreparedStatements=false
druid.maxPoolPreparedStatementPerConnectionSize=200
3.)注释 RefreshLogPoolManager 40-45行代码
2. bug现象
1.)数据表记录超过100后,执行清理操作时,后台报错
2.)mysql 执行SHOW PROCESSLIST
发现数据库连接处于 Waiting for table metadata lock 状态
3. 疑问
假如不注释 RefreshLogPoolManager 40-45行代码,运行程序则不会发生这个错误。
现在我想知道的是: 发生这个错误的根本原因是什么?
五,bug2 详情
1. 运行准备
- 不注释 RefreshLogPoolManager 40-45行代码
- 将pom文件 108-112行注释打开,启用spring-boot-devtools
2. bug现象
运行程序,发现日志保存根本不被执行
3. 疑问
现在我想知道的是: 启用spring-boot-devtools 为什么会影响到日志的保存?
如有高手帮忙释疑不胜感激,谢谢!