豆浆的代码

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.加入注解

springboot整合log4j2-CSDN博客

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语句的几种方法

idea输出sql语句的几种方法_idea打印sql-CSDN博客

 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下面添加相关配置:

访问地址:http://localhost:8080/druid/login.html

 Mybatis  sql优化器

sql优化器 SqlBeautyInterceptor

注意:可以看到Druid监控中是看不到具体的完整SQL的,那么如果我们想要显示完整SQL,就需要添加一个 MybatisPlus 优化器

image.png


创建SqlBeautyInterceptor类

image.png


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类,那么我们需要将这个类集成进我们的框架,就需要创建相应的配置类!

image.png

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();
    }
}

image.png


接口测试

image.png


优化: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();
    }
}

image.png


业务方在配置文件中进行配置
说明:不配置或者配置为true时会打印SQL日志,但是配置为false就不再打印日志,这次是否生效交由业务方来定夺!

ape-user的pom.xml文件:

Java复制代码

sql:
  beauty:
    show: true

image.png


注意:可以发现配置完成后,在控制台已经不再打印日志了!

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. 测试成功

说明:多次操作都能成功从缓存中获取数据,接口响应速度大幅度提高!

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

豆浆-plus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值