Log4j2日志
1.加入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> <version>2.4.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.71</version> </dependency>
2.加入注解
3.加入 Log4j2-spring.xml文件
<?xml version="1.0" encoding="UTF-8"?> <!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出--> <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数--> <configuration status="INFO" monitorInterval="5"> <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--变量配置--> <Properties> <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符--> <!-- %logger{36} 表示 Logger 名字最长36个字符 --> <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/> <!-- 定义日志存储的路径 --> <property name="FILE_PATH" value="../log"/> <property name="FILE_NAME" value="frame.log"/> </Properties> <!--https://logging.apache.org/log4j/2.x/manual/appenders.html--> <appenders> <console name="Console" target="SYSTEM_OUT"> <!--输出日志的格式--> <PatternLayout pattern="${LOG_PATTERN}"/> <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> </console> <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用--> <File name="fileLog" fileName="${FILE_PATH}/temp.log" append="false"> <PatternLayout pattern="${LOG_PATTERN}"/> </File> <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="15"/> </RollingFile> <!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="15"/> </RollingFile> <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="15"/> </RollingFile> </appenders> <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。--> <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效--> <loggers> <!--过滤掉spring和mybatis的一些无用的DEBUG信息--> <logger name="org.mybatis" level="info" additivity="false"> <AppenderRef ref="Console"/> </logger> <!--监控系统信息--> <!--若是additivity设为false,则子Logger只会在自己的appender里输出,而不会在父Logger的appender里输出。--> <Logger name="org.springframework" level="info" additivity="false"> <AppenderRef ref="Console"/> </Logger> <root level="info"> <appender-ref ref="Console"/> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> <appender-ref ref="fileLog"/> </root> </loggers> </configuration>
4.application.yml中
logging: config: classpath:Log4j2-spring.xml
log4j2的经典错误jar包冲突 -> 删jar包
基于druid配置文件加密
1.utils包
package com.jingdianjichi.subject.infra.basic.util;/** * @Author:豆浆 * @name :DruidEncryptUtil * @Date:2024/6/5 18:49 */ import com.alibaba.druid.filter.config.ConfigTools; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; /** * 数据库加密 * @author 豆浆 * @date 2024/6/5 18:49 */ public class DruidEncryptUtil { private static String publicKey; //公钥 private static String privateKey; //私钥 static { try { String[] keyPair = ConfigTools.genKeyPair(512); privateKey= keyPair[0]; System.out.println("私钥"+privateKey); publicKey=keyPair[1]; System.out.println("公钥"+publicKey); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (NoSuchProviderException e) { throw new RuntimeException(e); } } public static String encrypt(String plainText) throws Exception { String encrypt = ConfigTools.encrypt(privateKey, plainText); System.out.println("私钥:"+privateKey); return encrypt; } public static String decrypt(String encrtpeText) throws Exception { String decrypt = ConfigTools.decrypt(publicKey, encrtpeText); System.out.println("公钥:"+publicKey); return decrypt; } public static void main(String[] args) throws Exception { String encrypt = encrypt("123456"); System.out.println("encrypt:"+encrypt); } }
2.加配置
connectionProperties: config.decrypt=true;config.decrypt.key=${publicKey}
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIPpTyT2SO65F2AAOBboZlx5jvr+gC3Vhd9tI12s/GiWYeeuoMAqsDxgMunR8IzluZR4asDZn19+Sy3lCHeShZMCAwEAAQ==
config: enabled: true
私自配置文件
application.yml 与 application-dev.yml
server: port: 3000 spring: profiles: active: dev main: allow-circular-references: true datasource: druid: driver-class-name: ${sky.datasource.driver-class-name} username: ${sky.datasource.username} password: ${sky.datasource.password} url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true initial-size: 20 connectionProperties: config.decrypt=true;config.decrypt.key=${publicKey} min-idle: 20 max-active: 100 max-wait: 6000 stat-view-servlet: enabled: true url-pattern: /druid/* login-password: 123456 login-username: admin filter: stat: enabled: true slow-sql-millis: 2000 log-slow-sql: true wall: enabled: true config: enabled: true publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJEwxNY44tNjNqw4GNA/eoawqzdip7sm9Lk1k5ZTFzjYconI/88TZojh3d8Yki3Wfo9IED7NBbCl14zatCLCKxcCAwEAAQ==
sky: datasource: driver-class-name: com.mysql.cj.jdbc.Driver host: 192.168.200.128 port: 3306 database: jc_clud username: admin password: Jajx8yYky3PItjkKeHkN0tzsRs9AdO9W7B1AWLzTbRgJUMTHWzVimwr/xKOLipTI9hsWEBPZ94gPqfXRPwoCWA==
对象属性拷贝常用的四种方式
对象属性拷贝常用的四种方式(总结出最高效率)_java 对象拷贝-CSDN博客
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.4.2.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.4.2.Final</version> <scope>provided</scope> </dependency>
idea输出sql语句的几种方法
maven国内镜像
<!--国内镜像--> <repositories> <repository> <id>central</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public</url> <layout>default</layout> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories>
为什么要重写redisTemplate
RedisTemplate重写的一些模板_重写redistemplate-CSDN博客
RedisTemplate 重写优化
原生 redis 的 template 的序列化器会产生乱码问题,重写改为 jackson。package com.jingdianjichi.ciud.gateway.redis;/** import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * redis的config处理 * @author 豆浆 * @date */ @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { //1.redisTemplate 是一个key-value存储的模板,它可以操作hash、list、set、zset等数据结构,但是默认序列化是乱码的 RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); StringRedisSerializer redisSerializer = new StringRedisSerializer(); //2.设置了 RedisConnectionFactory 对象,它是 Redis 客户端的连接工厂,它可以创建和管理 Redis 客户端连接。 redisTemplate.setConnectionFactory(redisConnectionFactory); //3.设置了 RedisTemplate 的 key 序列化器为 StringRedisSerializer 对象,它可以将字符串对象转换为 Redis 的键 redisTemplate.setKeySerializer(redisSerializer); //4.设置了 RedisTemplate 的 hash key 序列化器为 StringRedisSerializer 对象,它可以将字符串对象转换为 Redis 的哈希键 redisTemplate.setHashKeySerializer(redisSerializer); //5.设置了 RedisTemplate 的 value 序列化器为 jackson2JsonRedisSerializer 对象,它可以将 Java 对象序列化为 JSON 字符串并将其存储在 Redis 中 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); return redisTemplate; } public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() { // 创建一个 Jackson2JsonRedisSerializer 对象,其泛型类型为 Object Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); // 创建一个 ObjectMapper 对象 ObjectMapper objectMapper = new ObjectMapper(); // 允许所有属性可见,包括私有属性 objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 禁用对未知属性的失败 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 启用默认的类型,即非终态类型 objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); // 将 ObjectMapper 对象设置为 Jackson2JsonRedisSerializer 对象 jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 返回 Jackson2JsonRedisSerializer 对象 return jackson2JsonRedisSerializer; } }
RedisUtil 的封装package com.jingdianjichi.ciud.gateway.redis; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; /** * RedisUtil工具类 */ @Component @Slf4j public class RedisUtil { @Resource private RedisTemplate redisTemplate; private static final String CACHE_KEY_SEPARATOR = "."; /** * 构建缓存key */ public String buildKey(String... strObjs) { return Stream.of(strObjs).collect(Collectors.joining(CACHE_KEY_SEPARATOR)); } /** * 是否存在key */ public boolean exist(String key) { return redisTemplate.hasKey(key); } /** * 删除key */ public boolean del(String key) { return redisTemplate.delete(key); } /** * set(不带过期) */ public void set(String key, String value) { redisTemplate.opsForValue().set(key, value); } /** * set(带过期) */ public boolean setNx(String key, String value, Long time, TimeUnit timeUnit) { return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit); } /** * 获取string类型缓存 */ public String get(String key) { return (String) redisTemplate.opsForValue().get(key); } public Boolean zAdd(String key, String value, Long score) { return redisTemplate.opsForZSet().add(key, value, Double.valueOf(String.valueOf(score))); } public Long countZset(String key) { return redisTemplate.opsForZSet().size(key); } public Set<String> rangeZset(String key, long start, long end) { return redisTemplate.opsForZSet().range(key, start, end); } public Long removeZset(String key, Object value) { return redisTemplate.opsForZSet().remove(key, value); } public void removeZsetList(String key, Set<String> value) { value.stream().forEach((val) -> redisTemplate.opsForZSet().remove(key, val)); } public Double score(String key, Object value) { return redisTemplate.opsForZSet().score(key, value); } public Set<String> rangeByScore(String key, long start, long end) { return redisTemplate.opsForZSet().rangeByScore(key, Double.valueOf(String.valueOf(start)), Double.valueOf(String.valueOf(end))); } public Object addScore(String key, Object obj, double score) { return redisTemplate.opsForZSet().incrementScore(key, obj, score); } public Object rank(String key, Object obj) { return redisTemplate.opsForZSet().rank(key, obj); } }
Druid图形化监控
增加Druid监控
说明:添加数据库连接池Druid的相关监控!
druid: ... stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: 123456 filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 2000 wall: enabled: true
在druid下面添加相关配置:
Mybatis sql优化器
sql优化器 SqlBeautyInterceptor
注意:可以看到Druid监控中是看不到具体的完整SQL的,那么如果我们想要显示完整SQL,就需要添加一个 MybatisPlus 优化器
创建SqlBeautyInterceptor类
SqlBeautyInterceptor 类:import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap; import java.lang.reflect.Field; import java.sql.Statement; import java.util.*; @Intercepts(value = { @Signature(args = {Statement.class, ResultHandler.class}, method = "query", type = StatementHandler.class), @Signature(args = {Statement.class}, method = "update", type = StatementHandler.class), @Signature(args = {Statement.class}, method = "batch", type = StatementHandler.class)}) public class SqlBeautyInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); long startTime = System.currentTimeMillis(); StatementHandler statementHandler = (StatementHandler) target; try { return invocation.proceed(); } finally { long endTime = System.currentTimeMillis(); long sqlCost = endTime - startTime; BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings(); sql = formatSql(sql, parameterObject, parameterMappingList); System.out.println("SQL: [ " + sql + " ]执行耗时[ " + sqlCost + "ms ]"); } } @Override public Object plugin(Object o) { return Plugin.wrap(o, this); } @Override public void setProperties(Properties properties) { } private String formatSql(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) { if (sql == "" || sql.length() == 0) { return ""; } sql = beautifySql(sql); if (parameterObject == null || parameterMappingList == null || parameterMappingList.size() == 0) { return sql; } String sqlWithoutReplacePlaceholder = sql; try { if (parameterMappingList != null) { Class<?> parameterObjectClass = parameterObject.getClass(); if (isStrictMap(parameterObjectClass)) { StrictMap<Collection<?>> strictMap = (StrictMap<Collection<?>>) parameterObject; if (isList(strictMap.get("list").getClass())) { sql = handleListParameter(sql, strictMap.get("list")); } } else if (isMap(parameterObjectClass)) { Map<?, ?> paramMap = (Map<?, ?>) parameterObject; sql = handleMapParameter(sql, paramMap, parameterMappingList); } else { sql = handleCommonParameter(sql, parameterMappingList, parameterObjectClass, parameterObject); } } } catch (Exception e) { return sqlWithoutReplacePlaceholder; } return sql; } private String handleCommonParameter(String sql, List<ParameterMapping> parameterMappingList, Class<?> parameterObjectClass, Object parameterObject) throws Exception { Class<?> originalParameterObjectClass = parameterObjectClass; List<Field> allFieldList = new ArrayList<>(); while (parameterObjectClass != null) { allFieldList.addAll(new ArrayList<>(Arrays.asList(parameterObjectClass.getDeclaredFields()))); parameterObjectClass = parameterObjectClass.getSuperclass(); } Field[] fields = new Field[allFieldList.size()]; fields = allFieldList.toArray(fields); parameterObjectClass = originalParameterObjectClass; for (ParameterMapping parameterMapping : parameterMappingList) { String propertyValue = null; if (isPrimitiveOrPrimitiveWrapper(parameterObjectClass)) { propertyValue = parameterObject.toString(); } else { String propertyName = parameterMapping.getProperty(); Field field = null; for (Field everyField : fields) { if (everyField.getName().equals(propertyName)) { field = everyField; } } field.setAccessible(true); propertyValue = String.valueOf(field.get(parameterObject)); if (parameterMapping.getJavaType().isAssignableFrom(String.class)) { propertyValue = "\"" + propertyValue + "\""; } } sql = sql.replaceFirst("\\?", propertyValue); } return sql; } private String handleMapParameter(String sql, Map<?, ?> paramMap, List<ParameterMapping> parameterMappingList) { for (ParameterMapping parameterMapping : parameterMappingList) { Object propertyName = parameterMapping.getProperty(); Object propertyValue = paramMap.get(propertyName); if (propertyValue != null) { if (propertyValue.getClass().isAssignableFrom(String.class)) { propertyValue = "\"" + propertyValue + "\""; } sql = sql.replaceFirst("\\?", propertyValue.toString()); } } return sql; } private String handleListParameter(String sql, Collection<?> col) { if (col != null && col.size() != 0) { for (Object obj : col) { String value = null; Class<?> objClass = obj.getClass(); if (isPrimitiveOrPrimitiveWrapper(objClass)) { value = obj.toString(); } else if (objClass.isAssignableFrom(String.class)) { value = "\"" + obj.toString() + "\""; } sql = sql.replaceFirst("\\?", value); } } return sql; } private String beautifySql(String sql) { sql = sql.replaceAll("[\\s\n ]+", " "); return sql; } private boolean isPrimitiveOrPrimitiveWrapper(Class<?> parameterObjectClass) { return parameterObjectClass.isPrimitive() || (parameterObjectClass.isAssignableFrom(Byte.class) || parameterObjectClass.isAssignableFrom(Short.class) || parameterObjectClass.isAssignableFrom(Integer.class) || parameterObjectClass.isAssignableFrom(Long.class) || parameterObjectClass.isAssignableFrom(Double.class) || parameterObjectClass.isAssignableFrom(Float.class) || parameterObjectClass.isAssignableFrom(Character.class) || parameterObjectClass.isAssignableFrom(Boolean.class)); } /** * 是否DefaultSqlSession的内部类StrictMap */ private boolean isStrictMap(Class<?> parameterObjectClass) { return parameterObjectClass.isAssignableFrom(StrictMap.class); } /** * 是否List的实现类 */ private boolean isList(Class<?> clazz) { Class<?>[] interfaceClasses = clazz.getInterfaces(); for (Class<?> interfaceClass : interfaceClasses) { if (interfaceClass.isAssignableFrom(List.class)) { return true; } } return false; } /** * 是否Map的实现类 */ private boolean isMap(Class<?> parameterObjectClass) { Class<?>[] interfaceClasses = parameterObjectClass.getInterfaces(); for (Class<?> interfaceClass : interfaceClasses) { if (interfaceClass.isAssignableFrom(Map.class)) { return true; } } return false; } }
创建MybatisConfiguration类
说明:如果我们想要使用 SqlBeautyInterceptor类,那么我们需要将这个类集成进我们的框架,就需要创建相应的配置类!Java复制代码
import com.qj.intecepter.SqlBeautyInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisConfiguration { // 注入Bean容器 @Bean public SqlBeautyInterceptor sqlBeautyInterceptor() { return new SqlBeautyInterceptor(); } }
接口测试
优化:SQL优化器动态生效问题:如果业务方不想要这个打印日志,怎么进行动态生效呢?
在配置类上加上@Conditional注解
说明:只需要在自动装配的Bean上加上 @ConditionalOnProperty 注解即可(@ConditionalOnBean 注解也可实现此功能)Java复制代码
import com.qj.intecepter.SqlBeautyInterceptor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisConfiguration { // 注入Bean容器 @Bean @ConditionalOnProperty(value = {"sql.beauty.show"}, havingValue = "true", matchIfMissing = true) public SqlBeautyInterceptor sqlBeautyInterceptor() { return new SqlBeautyInterceptor(); } }
业务方在配置文件中进行配置
说明:不配置或者配置为true时会打印SQL日志,但是配置为false就不再打印日志,这次是否生效交由业务方来定夺!
ape-user的pom.xml文件:Java复制代码
sql: beauty: show: true
注意:可以发现配置完成后,在控制台已经不再打印日志了!
Swagger集成
注意:因为Swagger需要对代码打注解,所以具有代码侵入性,建议尽量不要使用Swagger!
1. 增加 ape-common-swagger 模块
1)创建 ape-common-swagger 模块:
2)引入相关依赖:
ape-common-swagger的pom.xml文件:
<dependencies> <!-- 引入Swagger依赖 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.7.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.7.0</version> </dependency> </dependencies>
2. 配置Swagger自动装配
说明:在ape-common-swagger下创建SwaggerConfig类进行自动装配!
SwaggerConfig:
package com.doujiang.swagger.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * @Author:豆浆 * @name :SwaggerConfig * @Date:2024/7/2 21:58 */ @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(this.getApiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.doujiang")) .paths(PathSelectors.any()) .build(); } public ApiInfo getApiInfo() { return new ApiInfoBuilder() .title("通用脚手架") .contact(new Contact("豆浆", "git@gitee.com:doujiang-plus/ape-frame.git", "2017454420@qq.com")) .version("1.0版本") .description("开箱即用的脚手架") .build(); } }
3. 业务包引入ape-common-swagger包
4. 运行访问
访问地址:http://localhost:8080/swagger-ui.html
5. 优化:自定义Swagger内容
问题:现在我们的Swagger页面,很多信息都是写死的,陷入这样非常的不灵活,那么我们如何进行自定义的配置,将这些属性交给业务包来进行配置呢?
5.1 独立实现SpringBoot注解功能
说明:如果想要单独实现SpringBoot的注解功能,只要引入spring-boot-autoconfigure包即可!
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>2.4.2</version> </dependency>
5.2 创建 SwaggerBean 类
说明:这个 SwaggerBean 类的作用在于读取配置文件中以swagger开头的配置项!
注意:这里为了减少框架依赖,就没有引入Lombok了!
package com.doujiang.swagger.bean; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @Author:豆浆 * @name :SwaggerInfo * @Date:2024/7/2 22:16 */ @Data @Component @ConfigurationProperties(prefix = "swagger") public class SwaggerInfo { private String basePackage; //扫描的包 private String title; //标题 private String contactName; //联系人 private String contactUrl; //联系人url private String contactEmail; //联系人邮箱 private String version; //版本 private String description; //描述 /*public String getBasePackage() { return basePackage; } public void setBasePackage(String basePackage) { this.basePackage = basePackage; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContactName() { return contactName; } public void setContactName(String contactName) { this.contactName = contactName; } public String getContactUrl() { return contactUrl; } public void setContactUrl(String contactUrl) { this.contactUrl = contactUrl; } public String getContactEmail() { return contactEmail; } public void setContactEmail(String contactEmail) { this.contactEmail = contactEmail; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; }*/ }
5.3 修改 SwaggerConfig 类
package com.doujiang.swagger.config; import com.doujiang.swagger.bean.SwaggerInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import javax.annotation.Resource; /** * @Author:豆浆 * @name :SwaggerConfig * @Date:2024/7/2 21:58 */ @Configuration @EnableSwagger2 public class SwaggerConfig { @Resource private SwaggerInfo swaggerInfo; @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(this.getApiInfo()) .select() .apis(RequestHandlerSelectors.basePackage(swaggerInfo.getBasePackage())) .paths(PathSelectors.any()) .build(); } public ApiInfo getApiInfo() { return new ApiInfoBuilder() .title(swaggerInfo.getTitle()) .contact(new Contact(swaggerInfo.getContactName(), swaggerInfo.getContactUrl(), swaggerInfo.getContactEmail())) .version(swaggerInfo.getVersion()) .description(swaggerInfo.getDescription()) .build(); } }
5.4 在配置文件中配置相应参数
说明:虽然我们字段是 basePackage,但是在配置文件中可以用 驼峰basePackage方式 和 非驼峰base-package方式 两种!
1)驼峰方式:
swagger: basePackage: com.doujiang title: ape-frame contactName: 豆浆 contactUrl: git@git/doujiang-plus.git contactEmail: @2017xxxx.com version: 1.0版本 description: 开箱即用的脚手架
2)非驼峰方式:
swagger: base-package: com.doujiang title: ape-frame contact-name: contactUrl: git contact-email: 2017xx@qq.com version: 1.0版本 description: 开箱即用的脚手架
5.5 运行访问
说明:可以看到,我们运行后依然和之前写死的展示一样,说明自定义配置成功!
6. 优化:@Api相关注解
说明:因为默认的Swagger页面,全是默认接口参数,别人很难知道具体含义,所以我们可以通过相关的@Api相关注解来实现对Controller接口层、接口方法和接口请求参数的起名标注!
6.1 @Api注解
6.2 @ApiOperation注解
6.3 @ApiModel和@ApiModelProperty注解
6.4 结果展示
说明:可以看到相应的位置已经变成了我们注明的提示!
Redis实现自动预热缓存
说明:我们有可能遇到,当我们项目启动的时候,我们就想预热一部分的缓存的场景,所以我们要创建在项目启动时就加载缓存的模块!
1. 定义缓存的抽象类AbstractCache
package com.doujiang.redis.init; import org.springframework.stereotype.Component; @Component public abstract class AbstractCache { public abstract void initCache(); public abstract <T> T getCache(); public abstract void clearCache(); public void reloadCache() { clearCache(); initCache(); } }
2. 实现需要进行缓存的类
说明:此处实现缓存的类在业务模块中创建!
1)SysUserCache
package com.doujiang.user.cache; import com.doujiang.redis.init.AbstractCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class SysUserCache extends AbstractCache { private static final String SYS_USER_CACHE_KEY = "SYS_USER"; @Autowired private RedisTemplate redisTemplate; @Override public void initCache() { // 和数据库做联动,或者和其他数据来源做联动 redisTemplate.opsForValue().set(SYS_USER_CACHE_KEY, "doujiangSysUser"); } @Override public <T> T getCache() { if (!redisTemplate.hasKey(SYS_USER_CACHE_KEY).booleanValue()) { reloadCache(); } return (T) redisTemplate.opsForValue().get(SYS_USER_CACHE_KEY); } @Override public void clearCache() { redisTemplate.delete(SYS_USER_CACHE_KEY); } }
2)UserCache
package com.doujiang.user.cache; import com.doujiang.redis.init.AbstractCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class UserCache extends AbstractCache { private static final String USER_CACHE_KEY = "USER"; @Autowired private RedisTemplate redisTemplate; @Override public void initCache() { // 和数据库做联动,或者和其他数据来源做联动 redisTemplate.opsForValue().set(USER_CACHE_KEY, "doujiangUser"); } @Override public <T> T getCache() { if (!redisTemplate.hasKey(USER_CACHE_KEY).booleanValue()) { reloadCache(); } return (T) redisTemplate.opsForValue().get(USER_CACHE_KEY); } @Override public void clearCache() { redisTemplate.delete(USER_CACHE_KEY); } }
3. 定义类来获取ApplicationContext
说明:当一个类实现了ApplicationContextAware接口之后,这个类就可以方便的获得ApplicationContext对象(Spring上下文),Spring发现某个Bean实现了ApplicationContextAware接口,Spring容器会在创建该Bean之后,自动调用该Bean的setApplicationContext(参数)方法,调用该方法时,会将容器本身ApplicationContext对象作为参数传递给该方法。
package com.doujiang.redis.utils; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; @Component public class SpringContextUtil implements ApplicationContextAware { private static ApplicationContext applicationCtxt; /** * 获取applicationContext * @return */ public static ApplicationContext getApplicationContext() { return applicationCtxt; } /** * 实现ApplicationContextAware接口的context注入函数. * @param applicationContext the ApplicationContext object to be used by this object * @throws BeansException */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { applicationCtxt = applicationContext; } /** * 根据类型获取bean * @param type * @return */ public static Object getBean(Class type) { return applicationCtxt.getBean(type); } }
4. 启动并初始化缓存InitCache
说明:在使用SpringBoot构建项目时,我们通常有一些预先数据的加载。那么SpringBoot提供了CommandLineRunner方式来实现,CommandLineRunner是一个接口,我们需要时,只需实现该接口就行(如果存在多个加载的数据,我们也可以使用@Order注解来排序)
package com.doujiang.redis.init; import com.doujiang.redis.utils.SpringContextUtil; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import java.util.Map; import java.util.Map.Entry; @Component // 标识为组件 @ConditionalOnProperty(name = {"init.cahce.enable"}, havingValue = "true") public class InitCache implements CommandLineRunner { /** * 项目启动时候会直接运行run中内容 */ @Override public void run(String... args) throws Exception { // 需要知道哪些缓存需要进行预热 ApplicationContext applicationContext = SpringContextUtil.getApplicationContext(); // 获取所有AbstractCache的子类 Map<String, AbstractCache> beanMap = applicationContext.getBeansOfType(AbstractCache.class); // 调用其init方法 if (!beanMap.isEmpty()) { for (Entry<String, AbstractCache> entry : beanMap.entrySet()) { // 获取AbstractCache的子类(实现缓存方法的类),并调用其initCache()方法 AbstractCache abstractCache = (AbstractCache) SpringContextUtil.getBean(entry.getValue().getClass()); abstractCache.initCache(); } } System.out.println("缓存成功..."); } }
5. 配置文件开启
init: cahce: enable: true
6. 启动并测试
说明:可以看到在项目启动时,控制台顺利输出“缓存成功...”,说明项目成功运行!
注意:查看Redis集群也正常看到已被缓存的两个Key的数据!
Spring注解缓存
说明1:因为查询的时候,每次都走数据库会导致查询非常缓慢,所以Spring提供了一套缓存机制,在查询相同接口的时候会先查询缓存,再查询数据库,大大提高了接口响应速度!
说明2:Spring Boot会自动配置合适的CacheManager作为相关缓存的提供程序(此处配置了Redis的CacheManager),当你在配置类(@Configuration)上使用@EnableCaching注解时,会触发一个后处理器(post processor ),它检查每个Spring bean,查看是否已经存在注解对应的缓存;如果找到了,就会自动创建一个代理拦截方法调用,使用缓存的bean执行处理。
注意:在实际工作中基本不使用Spring注解缓存,因为无法为每个缓存单独设置过期时间(除非为每个缓存进行单独的配置),很可能导致整个业务产生缓存雪崩现象的出现!
1. 开启缓存
说明:需要在启动类上加上@EnableCaching注解!
2. 加上@Cacheable和@CacheEvict注解
说明:在业务接口上加上@Cacheable注解,并且为了保证数据一致性,需要配合@CacheEvict注解一起使用,用于在增删改的时候进行对缓存数据一致性的保障!
3. 错误测试1:Redis乱码和不过期问题
说明:可以看到存入的缓存数据是乱码,并且TTL时间为-1永不过期!
4. 解决方案:修改RedisCacheManager
说明:在Redis自动配置中,修改注入Bean容器的RedisCacheManager,修改其创建方式即可!
package com.doujiang.redis.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; /** * redis的config处理 * @author 豆浆 * @date */ @Configuration public class RedisConfig { /** * redisTemplate * @param redisConnectionFactory * @return */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { //1.redisTemplate 是一个key-value存储的模板,它可以操作hash、list、set、zset等数据结构,但是默认序列化是乱码的 RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); StringRedisSerializer redisSerializer = new StringRedisSerializer(); //2.设置了 RedisConnectionFactory 对象,它是 Redis 客户端的连接工厂,它可以创建和管理 Redis 客户端连接。 redisTemplate.setConnectionFactory(redisConnectionFactory); //3.设置了 RedisTemplate 的 key 序列化器为 StringRedisSerializer 对象,它可以将字符串对象转换为 Redis 的键 redisTemplate.setKeySerializer(redisSerializer); //4.设置了 RedisTemplate 的 hash key 序列化器为 StringRedisSerializer 对象,它可以将字符串对象转换为 Redis 的哈希键 redisTemplate.setHashKeySerializer(redisSerializer); //5.设置了 RedisTemplate 的 value 序列化器为 jackson2JsonRedisSerializer 对象,它可以将 Java 对象序列化为 JSON 字符串并将其存储在 Redis 中 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer()); return redisTemplate; } /** * 缓存管理器 * @param redisConnectionFactory * @return */ @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration .defaultCacheConfig() .serializeValuesWith(pair) .entryTtl(Duration.ofSeconds(20)); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } /** * 使用Jackson来进行Value的序列化 */ public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() { // 创建一个 Jackson2JsonRedisSerializer 对象,其泛型类型为 Object Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); // 创建一个 ObjectMapper 对象 ObjectMapper objectMapper = new ObjectMapper(); // 允许所有属性可见,包括私有属性 objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 禁用对未知属性的失败 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 启用默认的类型,即非终态类型 objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); // 将 ObjectMapper 对象设置为 Jackson2JsonRedisSerializer 对象 jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 返回 Jackson2JsonRedisSerializer 对象 return jackson2JsonRedisSerializer; } }
/** * 缓存管理器 * @param redisConnectionFactory * @return */ @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration .defaultCacheConfig() .serializeValuesWith(pair) .entryTtl(Duration.ofSeconds(20)); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); }
5. 错误测试2:获取缓存失败
说明:可以看到数据乱码问题解决,并且也实现了10s过期!
问题:但是第一次请求成功的情况下,第二次请求就会发生如下错误,意味着从Redis中获取的数据无法正确的进行类型转换!
6. 解决方案:修改Jackson序列化配置
/** * 使用Jackson来进行Value的序列化 */ public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer() { // 创建一个 Jackson2JsonRedisSerializer 对象,其泛型类型为 Object Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); // 创建一个 ObjectMapper 对象 ObjectMapper objectMapper = new ObjectMapper(); // 允许所有属性可见,包括私有属性 objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 禁用对未知属性的失败 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 启用默认的类型,即非终态类型 objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); // 将 ObjectMapper 对象设置为 Jackson2JsonRedisSerializer 对象 jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 返回 Jackson2JsonRedisSerializer 对象 return jackson2JsonRedisSerializer; }
注意:可以看到Redis缓存的数据中带上了类型!
7. 测试成功
说明:多次操作都能成功从缓存中获取数据,接口响应速度大幅度提高!