【求教】老菜鸟遇到新问题,双bug欢迎有緣人答疑

23 篇文章 0 订阅
12 篇文章 0 订阅

一,序

俗话说:但行好事,莫问前程,心之所向,无问西东

编程亦然,coding多了,就会遇到各种各样奇怪的问题,真是让人欢喜让人忧啊!

这不,小C最近实现了一个使用mysql数据库来保存日志的功能,不幸的是,遇到两个难解的问题,现拿出来,希望各位见多识广的大佬能帮忙分析,小可不胜感激!

二,需求

  1. 系统需要保存一定周期内的日志
  2. 系统自动清理超过设定周期的过期日志文件
  3. 日志数据表与业务系统是同一数据库
  4. 配置文件可能采用参数外部文件注入(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. 运行准备

  1. 不注释 RefreshLogPoolManager 40-45行代码
  2. 将pom文件 108-112行注释打开,启用spring-boot-devtools

2. bug现象

运行程序,发现日志保存根本不被执行在这里插入图片描述
在这里插入图片描述

3. 疑问

现在我想知道的是: 启用spring-boot-devtools 为什么会影响到日志的保存?

六,参考文章

log4j2 日志保存至数据库


如有高手帮忙释疑不胜感激,谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android懒加载WebView可以通过Fragment的方式来实现,具体步骤如下: 1. 在布局文件中添加一个FrameLayout用于显示WebView。 2. 创建一个继承自Fragment的类,重写onCreateView()方法,在该方法中加载WebView,并使用setUserVisibleHint()方法来判断是否需要加载WebView。 3. 在Activity中,使用ViewPager来实现选项卡效果,每个选项卡对应一个WebViewFragment。 4. 在ViewPager的OnPageChangeListener中,通过调用WebViewFragment的setUserVisibleHint()方法来判断是否需要加载WebView。 下面是一些常见问题的解答: 1. 如何判断WebView是否需要重加载? 可以在WebViewFragment中保存一个boolean类型的变量,用来表示WebView是否已经加载过。在setUserVisibleHint()方法中判断该变量的值,如果为false,则加载WebView,否则不做任何操作。 2. 如何处理WebView的缓存? 可以在WebView的设置中设置WebViewClient,重写shouldInterceptRequest()方法,在该方法中判断是否需要使用缓存。例如,可以将静态资源缓存在本地,每次加载时先判断本地是否有缓存,如果有则直接加载本地缓存,否则重从网络上加载。 3. 如何处理WebView的内存泄漏? 可以在WebViewFragment中重写onDestroyView()方法,调用WebView的destroy()方法来销毁WebView,确保WebView的资源被释放。同时,在Activity中,可以通过调用System.gc()方法来触发垃圾回收,释放WebView占用的内存。 希望以上内容能够对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值