【公共组件设计】刚果商城(congomall)公共组件设计深度解析,优雅!

刚果商城(congomall)

image-20230716181118698

整体架构

img

公共规约组件

一、congomall-base-spring-boot-starter

META-INF/spring.factories 自动装配

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.opengoofy.congomall.springboot.starter.base.config.ApplicationBaseAutoConfiguration
/**
 * 应用基础自动装配
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class ApplicationBaseAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    public ApplicationContextHolder congoApplicationContextHolder() {
        return new ApplicationContextHolder();
    }
    
    @Bean
    @ConditionalOnMissingBean
    public ApplicationContentPostProcessor congoApplicationContentPostProcessor() {
        return new ApplicationContentPostProcessor();
    }
    
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "congomall.fastjson.safa-mode", havingValue = "true")
    public FastJsonSafeMode congoFastJsonSafeMode() {
        return new FastJsonSafeMode();
    }
}

首先,这个类实现了 ApplicationListener<ApplicationReadyEvent> 接口,表示它是一个监听应用启动事件的监听器。

在类的开头,引入了一些必要的包,并对版权进行了声明。

接下来是类的定义和成员变量的声明,其中 @Resource 注解用于注入 ApplicationContext 对象。

代码中的 executeOnlyOnce 是一个标志位,用于确保事件只执行一次。默认情况下,它被设置为 true

onApplicationEvent 方法中,通过加锁的方式保证在多线程环境下只执行一次。在方法内部,首先判断 executeOnlyOnce 是否为 true,如果是,则发布一个 ApplicationInitializingEvent 事件,表示应用正在初始化。然后将 executeOnlyOnce 设置为 false,确保事件只被执行一次。

总的来说,这段代码的作用是在应用启动完成后,通过发布 ApplicationInitializingEvent 事件来触发一些初始化操作。通过 executeOnlyOnce 的设置,确保事件只执行一次,避免重复执行。

请注意,我的解读是基于代码的语法和结构,无法获取代码的具体上下文和业务逻辑。如果你需要更详细的解读或有其他问题,请提供更多的信息。

确保事件只执行一次是为了避免重复执行相同的操作,从而避免可能引发的重复资源消耗或潜在的错误。

在某些情况下,特定的初始化操作只需要在应用启动时执行一次。如果这个事件被多次触发,那么这些初始化操作就会被重复执行,可能导致不必要的资源浪费或产生意外的行为。

通过设置一个标志位 executeOnlyOnce,可以在多线程环境下保证事件只执行一次。当事件第一次被触发时,将标志位设置为 false,后续的事件触发时会检查这个标志位,并不再执行相应的操作。

这种设计模式通常被称为 “单次执行” 或 “只执行一次” 模式,它常用于需要确保某些代码只被执行一次的场景,如初始化操作、注册回调函数等。

总而言之,确保事件只执行一次可以避免重复执行操作,提高代码执行效率,并减少可能出现的问题和错误。

确保事件只执行一次的场景有很多,以下是其中几个常见的例子:

  1. 应用初始化:某些应用启动后需要进行初始化操作,如加载配置、创建数据库连接等。这些初始化操作只需在应用启动时执行一次。
  2. 资源加载:在应用启动时加载一些资源,如读取配置文件、加载字典数据等。这些操作可以通过确保只执行一次来避免资源的重复加载和浪费。
  3. 缓存预热:在应用启动时,将某些数据加载到缓存中,以提高后续请求的响应速度。这个操作通常只需要在应用启动时执行一次,避免重复加载和缓存数据不一致的问题。
  4. 注册回调函数:有时需要在特定事件发生时触发回调函数,但只需在第一次事件发生时注册回调函数即可。后续的事件发生时,可以通过确保只执行一次来避免重复触发回调。
  5. 数据库迁移或升级:在应用升级或迁移时,可能需要执行一些数据库变更操作。通过确保只执行一次,可以避免重复执行数据库变更脚本并保持数据的一致性。

这些场景都需要确保事件只执行一次,避免重复操作和可能出现的问题。通过设置标志位或其他方式,可以在多线程环境下实现事件只执行一次的效果。

FastJson安全模式,开启后关闭类型隐式传递

/**
 * FastJson安全模式,开启后关闭类型隐式传递
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class FastJsonSafeMode implements InitializingBean {
    
    @Override
    public void afterPropertiesSet() throws Exception {
        System.setProperty("fastjson2.parser.safeMode", "true");
    }
}

方便非spring类获取bean

public class ApplicationContextHolder implements ApplicationContextAware {
    
    private static ApplicationContext CONTEXT;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHolder.CONTEXT = applicationContext;
    }
    
    /**
     * Get ioc container bean by type.
     *
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {
        return CONTEXT.getBean(clazz);
    }
    
    /**
     * Get ioc container bean by name and type.
     *
     * @param name
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return CONTEXT.getBean(name, clazz);
    }
    
    /**
     * Get a set of ioc container beans by type.
     *
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> Map<String, T> getBeansOfType(Class<T> clazz) {
        return CONTEXT.getBeansOfType(clazz);
    }
    
    /**
     * Find whether the bean has annotations.
     *
     * @param beanName
     * @param annotationType
     * @param <A>
     * @return
     */
    public static <A extends Annotation> A findAnnotationOnBean(String beanName, Class<A> annotationType) {
        return CONTEXT.findAnnotationOnBean(beanName, annotationType);
    }
    
    /**
     * Get ApplicationContext.
     *
     * @return
     */
    public static ApplicationContext getInstance() {
        return CONTEXT;
    }
}

二、congomall-common-spring-boot-starter

标识枚举

public enum FlagEnum {
    
    /**
     * 正常状态
     */
    FALSE(0),
    
    /**
     * 删除状态
     */
    TRUE(1);
    
    private final Integer flag;
    
    FlagEnum(Integer flag) {
        this.flag = flag;
    }
    
    public Integer code() {
        return this.flag;
    }
    
    public String strCode() {
        return String.valueOf(this.flag);
    }
    
    @Override
    public String toString() {
        return strCode();
    }
}

使用:

productCommentDO.setCommentFlag(FlagEnum.FALSE.code());

封装spring环境工具类

import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.Map;

/**
 * Assert.
 */
public class Assert {
    
    public static void isTrue(boolean expression, String message) {
        if (!expression) {
            throw new IllegalArgumentException(message);
        }
    }
    
    public static void isTrue(boolean expression) {
        isTrue(expression, "[Assertion failed] - this expression must be true");
    }
    
    public static void isNull(Object object, String message) {
        if (object != null) {
            throw new IllegalArgumentException(message);
        }
    }
    
    public static void isNull(Object object) {
        isNull(object, "[Assertion failed] - the object argument must be null");
    }
    
    public static void notNull(Object object, String message) {
        if (object == null) {
            throw new IllegalArgumentException(message);
        }
    }
    
    public static void notNull(Object object) {
        notNull(object, "[Assertion failed] - this argument is required; it must not be null");
    }
    
    public static void notEmpty(Collection<?> collection, String message) {
        if (CollectionUtils.isEmpty(collection)) {
            throw new IllegalArgumentException(message);
        }
    }
    
    public static void notEmpty(Collection<?> collection) {
        notEmpty(collection,
                "[Assertion failed] - this collection must not be empty: it must contain at least 1 element");
    }
    
    public static void notEmpty(Map<?, ?> map, String message) {
        if (CollectionUtils.isEmpty(map)) {
            throw new IllegalArgumentException(message);
        }
    }
    
    public static void notEmpty(Map<?, ?> map) {
        notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry");
    }
    
    public static void notEmpty(String str, String message) {
        if (StringUtils.isEmpty(str)) {
            throw new IllegalArgumentException(message);
        }
    }
    
    public static void notEmpty(String str) {
        if (StringUtils.isEmpty(str)) {
            notEmpty(str, "[Assertion failed] - this string must not be empty");
        }
    }
    
    public static void notBlank(String str, String message) {
        if (org.apache.commons.lang3.StringUtils.isBlank(str)) {
            throw new IllegalArgumentException(message);
        }
    }
    
    public static void notBlank(String str) {
        notBlank(str, "[Assertion failed] - this string must not be blank");
    }
    
    public static void hasText(String text, String message) {
        if (!StringUtils.hasText(text)) {
            throw new IllegalArgumentException(message);
        }
    }
    
    public static void hasText(String text) {
        hasText(text,
                "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank");
    }
}

当前环境判断

/**
 * 环境工具类
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class EnvironmentUtil {
    
    private static List<String> ENVIRONMENT_LIST = new ArrayList<>();
    
    static {
        ENVIRONMENT_LIST.add("dev");
        ENVIRONMENT_LIST.add("test");
    }
    
    /**
     * 判断当前是否为正式环境
     *
     * @return
     */
    public static boolean isProdEnvironment() {
        ConfigurableEnvironment configurableEnvironment = ApplicationContextHolder.getBean(ConfigurableEnvironment.class);
        String propertyActive = configurableEnvironment.getProperty("spring.profiles.active", "dev");
        return !ENVIRONMENT_LIST.stream().filter(each -> propertyActive.contains(each)).findFirst().isPresent();
    }
}

应用:验证码开关

    public void checkoutValidCode(String verifyCode) {
        if (EnvironmentUtil.isProdEnvironment()) {
            if (StrUtil.isBlank(verifyCode)) {
                throw new ClientException("验证码已失效");
            }
            verifyCode = StrUtil.trim(verifyCode);
            this.verifyCode = StrUtil.trim(this.verifyCode);
            if (!StrUtil.equals(verifyCode, this.verifyCode)) {
                throw new ClientException("验证码错误");
            }
        }
    }

sleep方法异常统一捕获

/**
 * 线程池工具类
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public final class ThreadUtil {
    
    /**
     * 睡眠当前线程指定时间 {@param millis}
     *
     * @param millis 睡眠时间,单位毫秒
     */
    @SneakyThrows(value = InterruptedException.class)
    public static void sleep(long millis) {
        Thread.sleep(millis);
    }
}

@SneakyThrows(value = InterruptedException.class) 是 Lombok 注解之一,它的作用是在方法中自动处理 InterruptedException 异常。

在 Java 中,Thread.sleep() 方法会抛出 InterruptedException 异常,当一个线程在睡眠期间被中断时,即另一个线程调用该线程的 interrupt() 方法时,就会抛出 InterruptedException。

通常情况下,我们需要显式地在方法中捕获并处理 InterruptedException 异常。但是使用 @SneakyThrows 注解可以简化这一过程,使我们不需要在方法中编写 try-catch 语句来处理异常。

具体来说,@SneakyThrows(value = InterruptedException.class) 的作用是在编译时自动生成捕获和重新抛出 InterruptedException 异常的代码块。这样,我们就不需要在方法体中显式捕获 InterruptedException,而是将其交给 @SneakyThrows 注解来自动处理。

这样做的好处是减少了代码的冗余,提高了代码的可读性和简洁性。但需要注意的是,在使用 @SneakyThrows 注解时,方法声明必须包含对应的异常类型,否则编译器会报错。

总结来说,@SneakyThrows(value = InterruptedException.class) 的作用是在方法中自动处理 InterruptedException 异常,简化了代码编写,提高了代码的可读性和简洁性。

构建者模式

/**
 * 线程池 {@link ThreadPoolExecutor} 构建器, 构建者模式
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public final class ThreadPoolBuilder implements Builder<ThreadPoolExecutor> {
    
    private int corePoolSize = calculateCoreNum();
    
    // 等价1.5倍corepoolsize
    private int maximumPoolSize = corePoolSize + (corePoolSize >> 1);
    
    private long keepAliveTime = 30000L;
    
    private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
    
    private BlockingQueue workQueue = new LinkedBlockingQueue(4096);
    
    private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
    
    private boolean isDaemon = false;
    
    private String threadNamePrefix;
    
    private ThreadFactory threadFactory;
    
    private Integer calculateCoreNum() {
        int cpuCoreNum = Runtime.getRuntime().availableProcessors();
        return new BigDecimal(cpuCoreNum).divide(new BigDecimal("0.2")).intValue();
    }
    
    public ThreadPoolBuilder threadFactory(ThreadFactory threadFactory) {
        this.threadFactory = threadFactory;
        return this;
    }
    
    public ThreadPoolBuilder corePoolSize(int corePoolSize) {
        this.corePoolSize = corePoolSize;
        return this;
    }
    
    public ThreadPoolBuilder maximumPoolSize(int maximumPoolSize) {
        this.maximumPoolSize = maximumPoolSize;
        if (maximumPoolSize < this.corePoolSize) {
            this.corePoolSize = maximumPoolSize;
        }
        return this;
    }
    
    public ThreadPoolBuilder threadFactory(String threadNamePrefix, Boolean isDaemon) {
        this.threadNamePrefix = threadNamePrefix;
        this.isDaemon = isDaemon;
        return this;
    }
    
    public ThreadPoolBuilder keepAliveTime(long keepAliveTime) {
        this.keepAliveTime = keepAliveTime;
        return this;
    }
    
    public ThreadPoolBuilder keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
        this.keepAliveTime = keepAliveTime;
        this.timeUnit = timeUnit;
        return this;
    }
    
    public ThreadPoolBuilder rejected(RejectedExecutionHandler rejectedExecutionHandler) {
        this.rejectedExecutionHandler = rejectedExecutionHandler;
        return this;
    }
    
    public ThreadPoolBuilder workQueue(BlockingQueue workQueue) {
        this.workQueue = workQueue;
        return this;
    }
    
    public static ThreadPoolBuilder builder() {
        return new ThreadPoolBuilder();
    }
    
    @Override
    public ThreadPoolExecutor build() {
        if (threadFactory == null) {
            Assert.notEmpty(threadNamePrefix, "The thread name prefix cannot be empty or an empty string.");
            threadFactory = ThreadFactoryBuilder.builder().prefix(threadNamePrefix).daemon(isDaemon).build();
        }
        ThreadPoolExecutor executorService;
        try {
            executorService = new ThreadPoolExecutor(corePoolSize,
                    maximumPoolSize,
                    keepAliveTime,
                    timeUnit,
                    workQueue,
                    threadFactory,
                    rejectedExecutionHandler);
        } catch (IllegalArgumentException ex) {
            throw new IllegalArgumentException("Error creating thread pool parameter.", ex);
        }
        return executorService;
    }
}

最好根据本机CPU设置核心线程池数

    private Integer calculateCoreNum() {
        int cpuCoreNum = Runtime.getRuntime().availableProcessors();
        return new BigDecimal(cpuCoreNum).divide(new BigDecimal("0.2")).intValue();
    }

拒绝策略

image-20230716210556612

AbortPolicy:抛出异常,终止任务

**CallerRunsPolicy:**使用调用线程执行任务

**DiscardPolicy:**直接丢弃

**DiscardPolicy:**丢弃队列最老任务,添加新任务

动态代理模式:增强线程池拒绝策略

    public static RejectedExecutionHandler createProxy(RejectedExecutionHandler rejectedExecutionHandler, AtomicLong rejectedNum) {
        // 动态代理模式: 增强线程池拒绝策略,比如:拒绝任务报警或加入延迟队列重复放入等逻辑
        return (RejectedExecutionHandler) Proxy
                .newProxyInstance(
                        rejectedExecutionHandler.getClass().getClassLoader(),
                        new Class[]{RejectedExecutionHandler.class},
                        new RejectedProxyInvocationHandler(rejectedExecutionHandler, rejectedNum));
    }

这段代码实现了一个动态代理模式,用于增强线程池的拒绝策略。具体来说,它创建了一个代理对象,将原始的拒绝执行处理器(RejectedExecutionHandler)替换为增强后的处理器。

createProxy 方法中,使用了 Java 的动态代理机制。通过调用 Proxy.newProxyInstance 方法,可以创建一个代理对象,该代理对象实现了 RejectedExecutionHandler 接口,并且通过一个自定义的代理处理器(RejectedProxyInvocationHandler)来处理方法调用。

方法的参数包括原始的拒绝执行处理器(rejectedExecutionHandler)和一个原子长整型对象(rejectedNum)。代理处理器会将所有方法调用委托给原始的拒绝执行处理器,并在适当的时候进行增强处理。

通过使用动态代理模式,可以在不修改原始拒绝执行处理器代码的情况下,对其功能进行扩展。比如可以在任务被拒绝时触发报警、将任务加入延迟队列以便稍后再次尝试执行等额外逻辑。这样可以灵活地定制拒绝策略,并增强线程池的容错能力和业务逻辑。

public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(1, 3, 1024, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1));
        ThreadPoolExecutor.AbortPolicy abortPolicy = new ThreadPoolExecutor.AbortPolicy();
        AtomicLong rejectedNum = new AtomicLong();
        RejectedExecutionHandler proxyRejectedExecutionHandler = RejectedProxyUtil.createProxy(abortPolicy, rejectedNum);
        threadPoolExecutor.setRejectedExecutionHandler(proxyRejectedExecutionHandler);
        for (int i = 0; i < 5; i++) {
            try {
                threadPoolExecutor.execute(() -> ThreadUtil.sleep(100000L));
            } catch (Exception ignored) {
                ignored.printStackTrace();
            }
        }
        System.out.println("================ 线程池拒绝策略执行次数: " + rejectedNum.get());
    }

这段代码实现了一个简单的线程池示例,使用了之前提到的动态代理增强了线程池的拒绝策略。下面是代码的执行流程:

  1. 创建一个带有边界和容量的 ThreadPoolExecutor 对象,其中核心线程数为 1,最大线程数为 3,队列容量为 1。
  2. 使用 ThreadPoolExecutor.AbortPolicy 创建一个拒绝策略对象 abortPolicy,它会在任务被拒绝时抛出异常。
  3. 创建一个原子长整型对象 rejectedNum,用于记录被拒绝的任务数量。
  4. 使用之前提到的 RejectedProxyUtil.createProxy 方法创建代理的拒绝执行处理器 proxyRejectedExecutionHandler,将原始的 abortPolicyrejectedNum 作为参数传入。
  5. 将代理的拒绝执行处理器设置给线程池执行器,以替换默认的拒绝策略。
  6. 进行一个循环,共执行 5 次。在每次循环中,通过调用 threadPoolExecutor.execute 方法提交一个新的任务,该任务会暂停执行 100000 毫秒(100 秒)。
  7. 如果任务被拒绝,即超出了线程池的容量和队列容量限制,会进入 catch 块,并打印异常信息。
  8. 最后输出被拒绝的任务数量 rejectedNum.get()

这段代码展示了如何创建线程池、设置拒绝策略,并使用动态代理对拒绝策略进行增强。通过输出被拒绝的任务数量,可以观察线程池拒绝策略执行的次数。

AtomicLong

AtomicLong 是一个原子长整型类,用于在并发场景下进行线程安全的长整型操作。在这段代码中,rejectedNum 创建了一个新的 AtomicLong 对象,用于记录被拒绝的任务数量。

由于线程池的拒绝策略会在任务被拒绝时执行,为了线程安全地更新计数器,使用了 AtomicLong。通过调用 rejectedNum.get() 方法可以获取当前的计数值,而通过调用 rejectedNum.incrementAndGet() 方法可以对计数进行原子性的自增操作。

decrementAndGet

快速消费线程池

/**
 * 快速消费线程池
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class EagerThreadPoolExecutor extends ThreadPoolExecutor {
    
    public EagerThreadPoolExecutor(int corePoolSize,
                                   int maximumPoolSize,
                                   long keepAliveTime,
                                   TimeUnit unit,
                                   TaskQueue<Runnable> workQueue,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }
    
    private final AtomicInteger submittedTaskCount = new AtomicInteger(0);
    
    public int getSubmittedTaskCount() {
        return submittedTaskCount.get();
    }
    
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        submittedTaskCount.decrementAndGet();
    }
    
    @Override
    public void execute(Runnable command) {
        submittedTaskCount.incrementAndGet();
        try {
            super.execute(command);
        } catch (RejectedExecutionException ex) {
            TaskQueue taskQueue = (TaskQueue) super.getQueue();
            try {
                if (!taskQueue.retryOffer(command, 0, TimeUnit.MILLISECONDS)) {
                    submittedTaskCount.decrementAndGet();
                    throw new RejectedExecutionException("Queue capacity is full.", ex);
                }
            } catch (InterruptedException iex) {
                submittedTaskCount.decrementAndGet();
                throw new RejectedExecutionException(iex);
            }
        } catch (Exception ex) {
            submittedTaskCount.decrementAndGet();
            throw ex;
        }
    }
}
/**
 * 快速消费任务队列
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class TaskQueue<R extends Runnable> extends LinkedBlockingQueue<Runnable> {
    
    @Setter
    private EagerThreadPoolExecutor executor;
    
    public TaskQueue(int capacity) {
        super(capacity);
    }
    
    @Override
    public boolean offer(Runnable runnable) {
        int currentPoolThreadSize = executor.getPoolSize();
        // 如果有核心线程正在空闲,将任务加入阻塞队列,由核心线程进行处理任务
        if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
            return super.offer(runnable);
        }
        // 当前线程池线程数量小于最大线程数,返回 False,根据线程池源码,会创建非核心线程
        if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
            return false;
        }
        // 如果当前线程池数量大于最大线程数,任务加入阻塞队列
        return super.offer(runnable);
    }
    
    public boolean retryOffer(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
        if (executor.isShutdown()) {
            throw new RejectedExecutionException("Executor is shutdown!");
        }
        return super.offer(o, timeout, unit);
    }
}

这样写的目的是为了实现一种特定的任务处理策略。在这个策略中,首先会尽可能地将任务分配给空闲的核心线程来处理,以保证快速消费和处理任务。

为什么要优先将任务分配给空闲的核心线程呢?这是因为核心线程是线程池的基础,它们始终存在并且可以立即执行任务,避免了线程创建和销毁的开销。通过利用核心线程处理任务,可以最大限度地利用线程池的资源,提高任务处理的效率。

如果核心线程都已经被占用,那么会进一步判断当前线程池的线程数量是否已达到最大限制。如果还未达到最大限制,则返回 false,让线程池创建一个非核心线程来处理任务。这样可以控制线程池的线程数量,避免无限制地创建线程,从而保护系统资源的稳定性。

如果线程池的线程数量已经达到或超过最大限制,那么新任务将被添加到阻塞队列中等待处理。这样,即使线程池已满,任务也不会被丢弃,而是会在阻塞队列中等待有可用线程来处理。

综上所述,这样的设计可以根据线程池的状态和任务数量,采取不同的策略来处理任务,以实现快速消费和高效利用线程池资源的目标。

三、congomall-cache-spring-boot-starter

缓存穿透布隆过滤器

    /**
     * 防止缓存穿透的布隆过滤器
     */
    @Bean
    @ConditionalOnProperty(prefix = BloomFilterPenetrateProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
    public RBloomFilter<String> cachePenetrationBloomFilter(RedissonClient redissonClient, BloomFilterPenetrateProperties bloomFilterPenetrateProperties) {
        RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter(bloomFilterPenetrateProperties.getName());
        cachePenetrationBloomFilter.tryInit(bloomFilterPenetrateProperties.getExpectedInsertions(), bloomFilterPenetrateProperties.getFalseProbability());
        return cachePenetrationBloomFilter;
    }
    @Override
    public void safePut(String key, Object value, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter) {
        put(key, value, timeout, timeUnit);
        bloomFilter.add(key);
    }

get

    @Override
    public <T> T safeGet(String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit,
                         RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheGetFilter, CacheGetIfAbsent<String> cacheGetIfAbsent) {
        T result = get(key, clazz);
        // 缓存结果不等于空或空字符串直接返回;通过函数判断是否返回空,为了适配布隆过滤器无法删除的场景;两者都不成立,判断布隆过滤器是否存在,不存在返回空
        if (!CacheUtil.isNullOrBlank(result)
                || Optional.ofNullable(cacheGetFilter).map(each -> each.filter(key)).orElse(false)
                || Optional.ofNullable(bloomFilter).map(each -> !each.contains(key)).orElse(false)) {
            return result;
        }
        RLock lock = redissonClient.getLock(SAFE_GET_DISTRIBUTED_LOCK_KEY_PREFIX + key);
        lock.lock();
        try {
            // 双重判定锁,减轻获得分布式锁后线程访问数据库压力
            if (CacheUtil.isNullOrBlank(result = get(key, clazz))) {
                // 如果访问 cacheLoader 加载数据为空,执行后置函数操作
                if (CacheUtil.isNullOrBlank(result = loadAndSet(key, cacheLoader, timeout, timeUnit, true, bloomFilter))) {
                    Optional.ofNullable(cacheGetIfAbsent).ifPresent(each -> each.execute(key));
                }
            }
        } finally {
            lock.unlock();
        }
        return result;
    }

缓存穿透、缓存击穿和缓存雪崩是常见的与缓存相关的问题和现象,它们可以对系统性能和可用性造成影响。

  1. 缓存穿透(Cache Penetration): 缓存穿透指的是在缓存中无法命中所需数据,并且每次请求都会导致数据库查询。这通常发生在恶意攻击或者非法用户请求不存在的数据时。由于缓存中不存在该数据,每次请求都直接访问数据库,从而导致数据库负载过高,甚至可能引起系统崩溃。

    解决方案:可以通过在缓存中预先设置一个“空值”来避免缓存穿透。当查询到结果为空时,将空值存入缓存,并设置较短的过期时间,以防止攻击者频繁请求。

  2. 缓存击穿(Cache Breakdown): 缓存击穿指的是某个热点数据过期或被移除,而此时正好有大量并发请求同时访问该数据,导致请求直接访问数据库,增加了数据库负载。相比于缓存穿透,缓存击穿通常发生在存在合法数据的情况下。

    解决方案:一种解决方案是使用互斥锁(例如分布式锁)来保护缓存数据的访问,只允许一个线程去重新加载缓存,并在加载期间屏蔽其他请求对该缓存的访问。另一种方式是使用缓存预热,提前主动加载热点数据到缓存中,避免在关键时刻过期。

  3. 缓存雪崩(Cache Avalanche): 缓存雪崩指的是在某个时间段内,大量缓存数据同时失效或过期,导致大量请求直接访问数据库,造成数据库负载剧增。通常这是由于缓存服务器宕机、网络故障或者大规模缓存数据同时失效等情况导致的。

    解决方案:为了避免缓存雪崩,可以采用以下几种策略:

    • 设置不同的缓存过期时间,避免同时大量缓存失效。
    • 使用分布式缓存,将缓存数据部署在多个节点上,提高缓存的可用性和容错能力。
    • 实施限流和熔断机制,控制缓存失效时的请求量,避免对数据库造成过大压力。

以上是对缓存穿透、缓存击穿和缓存雪崩的简要介绍及解决方案。在实际开发中,可根据具体情况选择适合的缓存策略和技术来应对这些问题。

静态代理模式

    @Bean
    // 静态代理模式: Redis 客户端代理类增强
    public StringRedisTemplateProxy stringRedisTemplateProxy(RedisKeySerializer redisKeySerializer,
                                                             StringRedisTemplate stringRedisTemplate,
                                                             RedissonClient redissonClient) {
        stringRedisTemplate.setKeySerializer(redisKeySerializer);
        return new StringRedisTemplateProxy(stringRedisTemplate, redisDistributedProperties, redissonClient);
    }

静态代理模式是一种设计模式,它通过创建一个代理对象来间接访问原始对象,并在代理对象中提供额外的功能或控制访问。在静态代理模式中,代理类和原始类是在编译时就确定的,并且代理类和原始类实现相同的接口或继承相同的父类。

静态代理模式通常由以下几个角色组成:

  1. 抽象角色(接口或抽象类):定义了代理类和原始类之间的公共方法,是代理类和原始类的共同接口。
  2. 原始角色:实际执行业务逻辑的类,也称为被代理类或目标类。
  3. 代理角色:代理类,持有对原始类的引用,并在其方法中调用原始类的方法,在调用前后可以执行额外的操作。
  4. 客户端:使用代理对象来访问原始对象的类。

静态代理模式的主要优点是可以在不修改原始类的情况下,通过代理类来增强原始类的功能,可以实现对原始类的访问控制、增加日志记录、性能监控等功能。缺点是当原始类的接口发生变化时,代理类也需要相应修改。

单例模式

// 饿汉式
public class singleton{
    private static finnal singleton instance = new singleton();
    
    private singleton(){
        
    }
    
    public static singleton get(){
        return instance;
    }
}
// 懒汉式
public class singleton{
    private static singleton instance;
    
    private singleton(){
        
    }
    
    public static singleton get(){
        if(instance == null){
            instance = = new singleton();
        }
        return instance;
    }
    
    // 双重判定锁
     public static singleton get(){
        if(instance == null){
           synchronized(singleton.class){
            if(instance == null){
                instance = = new singleton();
            }   
           }
        }
        return instance;
    }
}
// 静态内部类
public class singleton{
    
    private singleton(){
        
    }
    
    private static class s{
        private static finnal singleton instance = new singleton(); 
    }
    
    public static singleton get(){
      
        return s.instance;
    }
}
// 枚举类
public enum Singleton {
    INSTANCE;

    // 添加其他方法和属性

    public void doSomething() {
        // 单例对象的操作
    }
}

四、congomall-convention-spring-boot-starter

异常公约

/**
 * 基础错误码定义
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public enum BaseErrorCode implements IErrorCode {
    
    // ========== 一级宏观错误码 客户端错误 ==========
    CLIENT_ERROR("A000001", "用户端错误"),
    
    // ========== 二级宏观错误码 用户注册错误 ==========
    USER_REGISTER_ERROR("A000100", "用户注册错误"),
    USER_NAME_VERIFY_ERROR("A000110", "用户名校验失败"),
    USER_NAME_EXIST_ERROR("A000111", "用户名已存在"),
    USER_NAME_SENSITIVE_ERROR("A000112", "用户名包含敏感词"),
    USER_NAME_SPECIAL_CHARACTER_ERROR("A000113", "用户名包含特殊字符"),
    PASSWORD_VERIFY_ERROR("A000120", "密码校验失败"),
    PASSWORD_SHORT_ERROR("A000121", "密码长度不够"),
    PHONE_VERIFY_ERROR("A000151", "手机格式校验失败"),
    
    // ========== 二级宏观错误码 系统请求缺少幂等Token ==========
    IDEMPOTENT_TOKEN_NULL_ERROR("A000200", "幂等Token为空"),
    IDEMPOTENT_TOKEN_DELETE_ERROR("A000201", "幂等Token已被使用或失效"),
    
    // ========== 一级宏观错误码 系统执行出错 ==========
    SERVICE_ERROR("B000001", "系统执行出错"),
    // ========== 二级宏观错误码 系统执行超时 ==========
    SERVICE_TIMEOUT_ERROR("B000100", "系统执行超时"),
    
    // ========== 一级宏观错误码 调用第三方服务出错 ==========
    REMOTE_ERROR("C000001", "调用第三方服务出错");
    
    private final String code;
    
    private final String message;
    
    BaseErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }
    
    @Override
    public String code() {
        return code;
    }
    
    @Override
    public String message() {
        return message;
    }
}
/**
 * 服务端异常
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class ServiceException extends AbstractException {
    
    public ServiceException(String message) {
        this(message, null, BaseErrorCode.SERVICE_ERROR);
    }
    
    public ServiceException(IErrorCode errorCode) {
        this(null, errorCode);
    }
    
    public ServiceException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }
    
    public ServiceException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable, errorCode);
    }
    
    @Override
    public String toString() {
        return "ServiceException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}

分页和全局数据对象

/**
 * 分页请求对象
 *
 * <p> {@link PageRequest}、{@link PageResponse}
 * 可以理解是防腐层的一种实现,不论底层 ORM 框架,对外分页参数属性不变
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Data
public class PageRequest {
    
    /**
     * 当前页
     */
    private Long current;
    
    /**
     * 每页显示条数
     */
    private Long size;
}

/**
 * 分页返回对象
 *
 * <p> {@link PageRequest}、{@link PageResponse}
 * 可以理解是防腐层的一种实现,不论底层 ORM 框架,对外分页参数属性不变
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 当前页
     */
    private Long current;
    
    /**
     * 每页显示条数
     */
    private Long size = 10L;
    
    /**
     * 总数
     */
    private Long total;
    
    /**
     * 查询数据列表
     */
    private List<T> records = Collections.emptyList();
    
    public PageResponse(long current, long size) {
        this(current, size, 0);
    }
    
    public PageResponse(long current, long size, long total) {
        if (current > 1) {
            this.current = current;
        }
        this.size = size;
        this.total = total;
    }
    
    public PageResponse setRecords(List<T> records) {
        this.records = records;
        return this;
    }
    
    public <R> PageResponse<R> convert(Function<? super T, ? extends R> mapper) {
        List<R> collect = this.getRecords().stream().map(mapper).collect(Collectors.toList());
        return ((PageResponse<R>) this).setRecords(collect);
    }
}
/**
 * 全局返回对象
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Data
@Accessors(chain = true)
public class Result<T> implements Serializable {
    
    private static final long serialVersionUID = 5679018624309023727L;
    
    /**
     * 正确返回码
     */
    public static final String SUCCESS_CODE = "0";
    
    /**
     * 返回码
     */
    private String code;
    
    /**
     * 返回消息
     */
    private String message;
    
    /**
     * 响应数据
     */
    private T data;
    
    /**
     * 请求ID
     */
    private String requestId;
    
    public boolean isSuccess() {
        return SUCCESS_CODE.equals(code);
    }
}

五、congomall-database-spring-boot-starter

MybatisPlus配置

/**
 * MybatisPlus 配置文件
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class MybatisPlusAutoConfiguration {
    
    /**
     * 分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
    
    /**
     * 元数据填充
     */
    @Bean
    public MyMetaObjectHandler myMetaObjectHandler() {
        return new MyMetaObjectHandler();
    }
    
    /**
     * 自定义雪花算法 ID 生成器
     */
    @Bean
    @Primary
    public IdentifierGenerator idGenerator() {
        return new CustomIdGenerator();
    }
}

六、congomall-ddd-framework-core

命令处理器

/**
 * 命令处理器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public interface CommandHandler<T, R> {
    
    /**
     * 命令执行
     *
     * @param requestParam
     * @return
     */
    R handler(T requestParam);
}

七、congomall-designpattern-spring-boot-starterj

建造者模式

/**
 * Builder 模式抽象接口
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public interface Builder<T> extends Serializable {
    
    /**
     * 构建方法
     *
     * @return 构建后的对象
     */
    T build();
}
public final class ThreadPoolBuilder implements Builder<ThreadPoolExecutor> {}

ThreadPoolBuilder builder = ThreadPoolBuilder.builder();

责任链模式

/**
 * 抽象责任链上下文
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public final class AbstractChainContext<T> implements CommandLineRunner {
    
    private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer = Maps.newHashMap();
    
    /**
     * 责任链组件执行
     *
     * @param mark         责任链组件标识
     * @param requestParam 请求参数
     */
    public void handler(String mark, T requestParam) {
        List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(mark);
        if (CollectionUtils.isEmpty(abstractChainHandlers)) {
            throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
        }
        abstractChainHandlers.forEach(each -> each.handler(requestParam));
    }
    
    @Override
    public void run(String... args) throws Exception {
        Map<String, AbstractChainHandler> chainFilterMap = ApplicationContextHolder
                .getBeansOfType(AbstractChainHandler.class);
        chainFilterMap.forEach((beanName, bean) -> {
            List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(bean.mark());
            if (CollectionUtils.isEmpty(abstractChainHandlers)) {
                abstractChainHandlers = new ArrayList();
            }
            abstractChainHandlers.add(bean);
            List<AbstractChainHandler> actualAbstractChainHandlers = abstractChainHandlers.stream()
                    .sorted(Comparator.comparing(Ordered::getOrder))
                    .collect(Collectors.toList());
            abstractChainHandlerContainer.put(bean.mark(), actualAbstractChainHandlers);
        });
    }
}

抽象出组件接口

/**
 * 抽象业务责任链组件
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public interface AbstractChainHandler<T> extends Ordered {
    
    /**
     * 执行责任链逻辑
     *
     * @param requestParam 责任链执行入参
     */
    void handler(T requestParam);
    
    /**
     * @return 责任链组件标识
     */
    String mark();
}

在应用启动时加载所有责任链的组件,根据标识和order排序,保证执行顺序

image-20230718171334326

创建订单抽象出3个责任组件,分别order-0,-1,-2,标识为ORDER_CREATE_FILTER

/**
 * 订单创建参数必填检验
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Component
public final class OrderCreateParamNotNullChainHandler implements OrderCreateChainFilter<OrderCreateCommand> {
    
    @Override
    public void handler(OrderCreateCommand requestParam) {
        if (Objects.isNull(requestParam.getCustomerUserId())) {
            throw new ClientException(OrderCreateErrorCodeEnum.CUSTOMER_USER_ID_NOTNULL);
        } else if (Objects.isNull(requestParam.getTotalAmount())) {
            throw new ClientException(OrderCreateErrorCodeEnum.TOTAL_AMOUNT_NOTNULL);
        } else if (Objects.isNull(requestParam.getPayAmount())) {
            throw new ClientException(OrderCreateErrorCodeEnum.PAY_AMOUNT_NOTNULL);
        } else if (Objects.isNull(requestParam.getFreightAmount())) {
            throw new ClientException(OrderCreateErrorCodeEnum.FREIGHT_AMOUNT_NOTNULL);
        } else if (Objects.isNull(requestParam.getSource())) {
            throw new ClientException(OrderCreateErrorCodeEnum.SOURCE_NOTNULL);
        }
        // xxx 这里应该把所有订单入参校验必填,因为重复工作量所以暂时验证上述这些
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

标识

/**
 * 订单创建责任链过滤器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public interface OrderCreateChainFilter<T extends OrderCreateCommand> extends AbstractChainHandler<OrderCreateCommand> {
    
    @Override
    default String mark() {
        return OrderChainMarkEnum.ORDER_CREATE_FILTER.name();
    }
}

最后执行

    private final AbstractChainContext<OrderCreateCommand> abstractChainContext;
    
    @Transactional
    @ShardingSphereTransactionType(TransactionType.BASE)
    @Override
    public String createOrder(OrderCreateCommand requestParam) {
        // 责任链模式: 执行订单创建参数验证
        abstractChainContext.handler(OrderChainMarkEnum.ORDER_CREATE_FILTER.name(), requestParam);

这段代码是一个泛型类 AbstractChainContext<T>,它实现了 CommandLineRunner 接口。它表示一个责任链上下文,用于执行责任链组件。

代码中的 abstractChainHandlerContainer 是一个用于存储责任链组件的容器,使用 Map 数据结构来存储不同标识的责任链组件列表。每个标识对应一个责任链组件列表,列表中的责任链组件按照其顺序进行处理。

handler 方法用于执行责任链组件。它接收一个标识 mark 和一个请求参数 requestParam,根据标识从容器中获取对应的责任链组件列表,并依次调用每个组件的 handler 方法进行处理。

run 方法中,通过 ApplicationContextHolder.getBeansOfType(AbstractChainHandler.class) 获取所有实现了 AbstractChainHandler 抽象类的 bean,然后遍历每个 bean,并将其添加到对应标识的责任链组件列表中。之后,对每个责任链组件列表进行排序,并更新到容器中。

通过实现 CommandLineRunner 接口,该类可以在 Spring Boot 应用程序启动时自动执行,并根据配置的责任链组件进行初始化和排序。之后,可以通过调用 handler 方法来执行责任链组件的处理逻辑。

请注意,该类依赖于其他类 AbstractChainHandlerApplicationContextHolder,它们可能在代码的其他部分中实现。此处提供的代码片段是对责任链模式在 Spring Boot 中的一种实现示例,具体实现可能需要了解这些类的定义和使用方式。

抛出异常->执行中断

在责任链模式中,当一个处理程序(或处理者)处理请求时,它可以选择继续将请求传递给下一个处理程序,也可以选择终止责任链的执行。

如果某个处理程序在处理请求时发生了异常,并且没有进行适当的处理或捕获该异常,那么该异常将向上级调用栈抛出,可能导致责任链的执行被中断。

在这种情况下,除非有适当的异常处理机制或错误处理策略来捕获和处理异常,否则可能会导致责任链的后续处理程序不会执行。

要确保在责任链中的处理程序发生异常时能够正确处理,建议在每个处理程序中使用适当的异常处理机制(例如 try-catch 块)来捕获和处理异常。这样可以防止异常在责任链中传播,并保证后续的处理程序能够继续执行。

策略模式

[策略模式详解]https://zhuanlan.zhihu.com/p/346607652

策略模式(Strategy Pattern)定义了一组同类型的算法,在不同的类中封装起来,每种算法可以根据当前场景相互替换,从而使算法的变化独立于使用它们的客户端(即算法的调用者)。《GoF 设计模式》书中,它是这样定义的:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

例如:在网购中,我在支付的时候,可以根据实际情况来选择不同的支付方式(微信支付、支付宝、银行卡支付等等),这些支付方式即是不同的策略。我们通常会看到如下的实现代码:

Order order = 订单信息
if (payType == 微信支付) {
    微信支付流程
} else if (payType == 支付宝) {
    支付宝支付流程
} else if (payType == 银行卡) {
    银行卡支付流程
} else {
    暂不支持的支付方式
}

如上代码,虽然写起来简单,但违反了面向对象的 2 个基本原则:

  • 单一职责原则:一个类只有1个发生变化的原因
    之后修改任何逻辑,当前方法都会被修改
  • 开闭原则:对扩展开放,对修改关闭
    当我们需要增加、减少某种支付方式(积分支付/组合支付),或者增加优惠券等功能时,不可避免的要修改该段代码

特别是当 if-else 块中的代码量比较大时,后续的扩展和维护会变得非常复杂且容易出错。在阿里《Java开发手册》中,有这样的规则:超过3层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现

策略模式是解决过多 if-else(或者 switch-case) 代码块的方法之一,提高代码的可维护性、可扩展性和可读性。下面我将从策略的定义、创建和使用这三个方面以上述网购支付为示例来分别进行说明。

1. 策略的定义

策略接口的定义,通常包含两个方法:获取策略类型的方法和处理策略业务逻辑的方法。

/**
 * 第三方支付
 */
public interface Payment {

    /**
     * 获取支付方式
     * 
     * @return 响应,支付方式
     */
    PayTypeEnum getPayType();

    /**
     * 支付调用
     * 
     * @param order 订单信息
     * @return 响应,支付结果
     */
    PayResult pay(Order order);

}

策略接口的实现,每种支付类都实现了上述接口(基于接口而非实现编程),这样我们可以灵活的替换不同的支付方式。下边示例代码展示了每种支付方式的实现:

/**
 * 微信支付
 */
@Component
public class WxPayment implements Payment {

    @Override
    public PayTypeEnum getPayType() {
        return PayTypeEnum.WX;
    }

    @Override
    public PayResult pay(Order order) {
        调用微信支付
        if (成功) {
            return PayResult.SUCCESS;
        } else {
            return PayResult.FAIL;
        }
    }

}
/**
 * 支付宝支付
 */
@Component
public class AlipayPayment implements Payment {

    @Override
    public PayTypeEnum getPayType() {
        return PayTypeEnum.ALIPAY;
    }

    @Override
    public PayResult pay(Order order) {
        调用支付宝支付
        if (成功) {
            return PayResult.SUCCESS;
        } else {
            return PayResult.FAIL;
        }
    }

}
/**
 * 银行卡支付
 */
@Component
public class BankCardPayment implements Payment {

    @Override
    public PayTypeEnum getPayType() {
        return PayTypeEnum.BANK_CARD;
    }

    @Override
    public PayResult pay(Order order) {
        调用银行卡支付
        if (成功) {
            return PayResult.SUCCESS;
        } else {
            return PayResult.FAIL;
        }
    }

}
2. 策略的创建

策略模式包含一组同类的策略,在使用时我们通常通过类型来判断创建哪种策略来进行使用。我们可以使用工厂模式来创建策略,以屏蔽策略的创建细节。如下代码所示:

public class PaymentFactory {
    private static final Map<PayTypeEnum, Payment> payStrategies = new HashMap<>();

    static {
        payStrategies.put(PayTypeEnum.WX, new WxPayment());
        payStrategies.put(PayTypeEnum.ALIPAY, new AlipayPayment());
        payStrategies.put(PayTypeEnum.BANK_CARD, new BankCardPayment());
    }

    public static Payment getPayment(PayTypeEnum payType) {
        if (payType == null) {
            throw new IllegalArgumentException("pay type is empty.");
        }
        if (!payStrategies.containsKey(payType)) {
            throw new IllegalArgumentException("pay type not supported.");
        }
        return payStrategies.get(payType);
    }

}

或者使用 Spring 创建:

@Component
public class PaymentFactory implements InitializingBean, ApplicationContextAware {
    private static final Map<PayTypeEnum, Payment> payStrategies = new HashMap<>();

    private ApplicationContext appContext;

    public static Payment getPayment(PayTypeEnum payType) {
        if (payType == null) {
            throw new IllegalArgumentException("pay type is empty.");
        }
        if (!payStrategies.containsKey(payType)) {
            throw new IllegalArgumentException("pay type not supported.");
        }
        return payStrategies.get(payType);
    }

    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
        appContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() {
        // 将 Spring 容器中所有的 Payment 接口实现类注册到 payStrategies
        appContext.getBeansOfType(Payment.class)
                  .values()
                  .forEach(payment -> payStrategies.put(payment.getPayType(), payment));
    }
}

注意:以上两种创建方式,都是无状态的,即不包含成员变量,它们可以被共享使用。如果策略类是有状态的,需要根据业务场景每次创建新的策略对象,那么我们可以在工厂方法中,每次生成新的策略对象,而不是使用已经提前缓存好的策略对象。如下代码所示:

public class PaymentFactory {
    public static Payment getPayment(PayTypeEnum payType) {
        if (payType == null) {
            throw new IllegalArgumentException("pay type is empty.");
        }
        if (payType == PayTypeEnum.WX) {
            return new WxPayment();
        }
        if (payType == PayTypeEnum.ALIPAY) {
            return new AlipayPayment();
        }
        if (payType == PayTypeEnum.BANK_CARD) {
            return new BankCardPayment();
        }
        throw new IllegalArgumentException("pay type not supported.");
    }

}
3. 策略的使用

通常我们事先并不知道会使用哪个策略,在程序运行时根据配置、用户输入、计算结果等来决定到底使用哪种策略。例如,前边支付方式的例子,我们会根据用户的选择来决定使用哪种支付方式。使用策略模式的代码实现如下:

Order order = 订单信息
PayResult payResult = PaymentFactory.getPayment(payType).pay(order);
if (payResult == PayResult.SUCCESS) {
    System.out.println("支付成功");
} else if (payType == 支付宝) {
    System.out.println("支付失败");
}

综上代码中,接口类只负责业务策略的定义,每个策略的具体实现单独放在实现类中,工厂类 Factory 只负责获取具体实现类,而具体调用代码则负责业务逻辑的编排。这些实现用到了面向接口而非实现编程,满足了职责单一、开闭原则,从而达到了功能上的高内聚低耦合、提高了可维护性、扩展性以及代码的可读性。

onApplicationEvent()与run()区别

ApplicationListener<ApplicationInitializingEvent>接口的onApplicationEvent()方法与实现CommandLineRunner接口的run()方法在功能和触发时机上有一些区别。

  1. 功能不同:onApplicationEvent()方法是一个事件监听器,用于在应用程序初始化过程中处理特定的事件。它可以执行与应用程序初始化相关的逻辑操作。而run()方法是CommandLineRunner接口的方法,用于在应用程序启动后执行一些命令行任务或操作。
  2. 触发时机不同:onApplicationEvent()方法的触发时机是在应用程序的初始化过程中,当特定的事件被触发时才会调用。而run()方法是在应用程序启动后立即执行,无需等待特定的事件触发。
  3. 使用方式不同:onApplicationEvent()方法需要将实现类注册为事件监听器,并且根据具体的事件类型进行触发和处理。而run()方法则是直接在实现类上实现该方法,在应用程序启动时自动执行。

综上所述,onApplicationEvent()方法适用于在应用程序初始化过程中监听和处理特定事件的场景,而run()方法适用于在应用程序启动后执行命令行任务或操作的场景。它们分别针对不同的功能和触发时机来提供相应的处理能力。

抽象接口行为

/**
 * 策略执行抽象
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public interface AbstractExecuteStrategy<REQUEST, RESPONSE> {
    
    /**
     * 执行策略标识
     */
    String mark();
    
    /**
     * 执行策略
     *
     * @param requestParam 执行策略入参
     */
    default void execute(REQUEST requestParam) {
        
    }
    
    /**
     * 执行策略,带返回值
     *
     * @param requestParam 执行策略入参
     * @return 执行策略后返回值
     */
    default RESPONSE executeResp(REQUEST requestParam) {
        return null;
    }
}

策略选择器

/**
 * 策略选择器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class AbstractStrategyChoose implements ApplicationListener<ApplicationInitializingEvent> {
    
    /**
     * 执行策略集合
     */
    private final Map<String, AbstractExecuteStrategy> abstractExecuteStrategyMap = new HashMap<>();
    
    /**
     * 根据 mark 查询具体策略
     *
     * @param mark 策略标识
     * @return 实际执行策略
     */
    public AbstractExecuteStrategy choose(String mark) {
        return Optional.ofNullable(abstractExecuteStrategyMap.get(mark)).orElseThrow(() -> new ServiceException(String.format("[%s] 策略未定义", mark)));
    }
    
    /**
     * 根据 mark 查询具体策略并执行
     *
     * @param mark         策略标识
     * @param requestParam 执行策略入参
     * @param <REQUEST>    执行策略入参范型
     */
    public <REQUEST> void chooseAndExecute(String mark, REQUEST requestParam) {
        AbstractExecuteStrategy executeStrategy = choose(mark);
        executeStrategy.execute(requestParam);
    }
    
    /**
     * 根据 mark 查询具体策略并执行,带返回结果
     *
     * @param mark         策略标识
     * @param requestParam 执行策略入参
     * @param <REQUEST>    执行策略入参范型
     * @param <RESPONSE>   执行策略出参范型
     * @return
     */
    public <REQUEST, RESPONSE> RESPONSE chooseAndExecuteResp(String mark, REQUEST requestParam) {
        AbstractExecuteStrategy executeStrategy = choose(mark);
        return (RESPONSE) executeStrategy.executeResp(requestParam);
    }
    
    @Override
    public void onApplicationEvent(ApplicationInitializingEvent event) {
        Map<String, AbstractExecuteStrategy> actual = ApplicationContextHolder.getBeansOfType(AbstractExecuteStrategy.class);
        actual.forEach((beanName, bean) -> {
            AbstractExecuteStrategy beanExist = abstractExecuteStrategyMap.get(bean.mark());
            if (beanExist != null) {
                throw new ServiceException(String.format("[%s] Duplicate execution policy", bean.mark()));
            }
            abstractExecuteStrategyMap.put(bean.mark(), bean);
        });
    }
}

支付策略

/**
 * 阿里支付组件
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Slf4j
@Service
@RequiredArgsConstructor
public final class AliPayNativeHandler extends AbstractPayHandler implements AbstractExecuteStrategy<PayRequest, PayResponse> {
    
    private final AliPayProperties aliPayProperties;
    
    @SneakyThrows(value = AlipayApiException.class)
    @Override
    public PayResponse pay(PayRequest payRequest) {
        AliPayRequest aliPayRequest = payRequest.getAliPayRequest();
        AlipayConfig alipayConfig = BeanUtil.convert(aliPayProperties, AlipayConfig.class);
        AlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);
        AlipayTradePagePayModel model = new AlipayTradePagePayModel();
        model.setOutTradeNo(aliPayRequest.getOrderRequestId());
        model.setTotalAmount(aliPayRequest.getTotalAmount());
        model.setSubject(aliPayRequest.getSubject());
        model.setProductCode("FAST_INSTANT_TRADE_PAY");
        AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
        request.setNotifyUrl(aliPayProperties.getNotifyUrl());
        request.setBizModel(model);
        AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
        log.info("发起支付宝支付,订单号:{},子订单号:{},订单请求号:{},订单金额:{} \n调用支付返回:\n\n{}\n",
                aliPayRequest.getOrderSn(),
                aliPayRequest.getOutOrderSn(),
                aliPayRequest.getOrderRequestId(),
                aliPayRequest.getTotalAmount(),
                response.getBody());
        return new PayResponse(response.getBody());
    }
    
    @Override
    public String mark() {
        return StrBuilder.create()
                .append(PayChannelEnum.ALI_PAY.name())
                .append("_")
                .append(PayTradeTypeEnum.NATIVE.name())
                .toString();
    }
    
    @Override
    public PayResponse executeResp(PayRequest requestParam) {
        return pay(requestParam);
    }
}

八、congomall-distributedid-spring-boot-starter

模板方法模式

/**
 * 雪花算法模板生成
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Slf4j
public abstract class AbstractWorkIdChooseTemplate {
    
    /**
     * 是否使用 {@link SystemClock} 获取当前时间戳
     */
    @Value("${congomall.distributed.id.snowflake.is-use-system-clock:false}")
    private boolean isUseSystemClock;
    
    /**
     * 根据自定义策略获取 WorkId 生成器
     *
     * @return
     */
    protected abstract WorkIdWrapper chooseWorkId();
    
    /**
     * 选择 WorkId 并初始化雪花
     */
    public void chooseAndInit() {
        // 模板方法模式: 通过抽象方法获取 WorkId 包装器创建雪花算法
        WorkIdWrapper workIdWrapper = chooseWorkId();
        long workId = workIdWrapper.getWorkId();
        long dataCenterId = workIdWrapper.getDataCenterId();
        Snowflake snowflake = new Snowflake(workId, dataCenterId, isUseSystemClock);
        log.info("Snowflake type: {}, workId: {}, dataCenterId: {}", this.getClass().getSimpleName(), workId, dataCenterId);
        SnowflakeIdUtil.initSnowflake(snowflake);
    }
}

抽象出chooseWorkId();方法,通过继承实现模板方法

/**
 * 使用 Redis 获取雪花 WorkId
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Slf4j
public class LocalRedisWorkIdChoose extends AbstractWorkIdChooseTemplate implements InitializingBean {
    
    private RedisTemplate stringRedisTemplate;
    
    public LocalRedisWorkIdChoose() {
        this.stringRedisTemplate = ApplicationContextHolder.getBean(StringRedisTemplate.class);
    }
    
    @Override
    public WorkIdWrapper chooseWorkId() {
        DefaultRedisScript redisScript = new DefaultRedisScript();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/chooseWorkIdLua.lua")));
        List<Long> luaResultList = null;
        try {
            redisScript.setResultType(List.class);
            luaResultList = (ArrayList) this.stringRedisTemplate.execute(redisScript, null);
        } catch (Exception ex) {
            log.error("Redis Lua 脚本获取 WorkId 失败", ex);
        }
        return CollUtil.isNotEmpty(luaResultList) ? new WorkIdWrapper(luaResultList.get(0), luaResultList.get(1)) : new RandomWorkIdChoose().chooseWorkId();
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        chooseAndInit();
    }
}

这段代码是一个抽象类AbstractWorkIdChooseTemplate,用于生成雪花算法模板。

  1. isUseSystemClock是一个布尔类型的变量,通过@Value注解从配置文件中读取congomall.distributed.id.snowflake.is-use-system-clock属性的值。如果找不到对应的属性值或值为空,则使用默认值false
  2. chooseWorkId()方法是一个抽象方法,由子类实现,用于根据自定义策略获取WorkIdWrapper对象。
  3. chooseAndInit()方法是选择WorkId并初始化雪花算法的方法。它使用模板方法模式,先调用chooseWorkId()方法获取WorkIdWrapper对象,然后根据该对象的属性值和isUseSystemClock变量的值创建Snowflake对象。最后,使用SnowflakeIdUtil.initSnowflake(snowflake)方法进行雪花算法的初始化。

整体上,这段代码通过抽象类和模板方法模式提供了一个统一的雪花算法生成模板,可以根据具体的策略选择WorkId并初始化雪花算法,提供了灵活性和可扩展性。

雪花算法

/**
 * Twitter的Snowflake 算法<br>
 * 分布式系统中,有一些需要使用全局唯一ID的场景,有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。
 *
 * <p>
 * snowflake的结构如下(每部分用-分开):<br>
 *
 * <pre>
 * 符号位(1bit)- 时间戳相对值(41bit)- 数据中心标志(5bit)- 机器标志(5bit)- 递增序号(12bit)
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
 * </pre>
 * <p>
 * 第一位为未使用(符号位表示正数),接下来的41位为毫秒级时间(41位的长度可以使用69年)<br>
 * 然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点)<br>
 * 最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号)
 * <p>
 * 并且可以通过生成的id反推出生成时间,datacenterId和workerId
 * <p>
 * 参考:http://www.cnblogs.com/relucent/p/4955340.html<br>
 * 关于长度是18还是19的问题见:https://blog.csdn.net/unifirst/article/details/80408050
 *
 * @author Looly
 * @since 3.0.1
 */
public class Snowflake implements Serializable, IdGenerator {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 默认的起始时间,为Thu, 04 Nov 2010 01:42:54 GMT
     */
    private static long DEFAULT_TWEPOCH = 1288834974657L;
    
    /**
     * 默认回拨时间,2S
     */
    private static long DEFAULT_TIME_OFFSET = 2000L;
    
    private static final long WORKER_ID_BITS = 5L;
    
    // 最大支持机器节点数0~31,一共32个
    @SuppressWarnings({"PointlessBitwiseExpression", "FieldCanBeLocal"})
    private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
    
    private static final long DATA_CENTER_ID_BITS = 5L;
    
    // 最大支持数据中心节点数0~31,一共32个
    @SuppressWarnings({"PointlessBitwiseExpression", "FieldCanBeLocal"})
    private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
    
    // 序列号12位(表示只允许workId的范围为:0-4095)
    private static final long SEQUENCE_BITS = 12L;
    
    // 机器节点左移12位
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    
    // 数据中心节点左移17位
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    
    // 时间毫秒数左移22位
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
    
    // 序列掩码,用于限定序列最大值不能超过4095
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
    
    /**
     * 初始化时间点
     */
    private final long twepoch;
    
    private final long workerId;
    
    private final long dataCenterId;
    
    private final boolean useSystemClock;
    
    /**
     * 允许的时钟回拨毫秒数
     */
    private final long timeOffset;
    
    /**
     * 当在低频模式下时,序号始终为0,导致生成ID始终为偶数<br>
     * 此属性用于限定一个随机上限,在不同毫秒下生成序号时,给定一个随机数,避免偶数问题。<br>
     * 注意次数必须小于{@link #SEQUENCE_MASK},{@code 0}表示不使用随机数。<br>
     * 这个上限不包括值本身。
     */
    private final long randomSequenceLimit;
    
    /**
     * 自增序号,当高频模式下时,同一毫秒内生成N个ID,则这个序号在同一毫秒下,自增以避免ID重复。
     */
    private long sequence = 0L;
    
    private long lastTimestamp = -1L;
    
    /**
     * 构造,使用自动生成的工作节点ID和数据中心ID
     */
    public Snowflake() {
        this(IdUtil.getWorkerId(IdUtil.getDataCenterId(MAX_DATA_CENTER_ID), MAX_WORKER_ID));
    }
    
    /**
     * @param workerId 终端ID
     */
    public Snowflake(long workerId) {
        this(workerId, IdUtil.getDataCenterId(MAX_DATA_CENTER_ID));
    }
    
    /**
     * @param workerId     终端ID
     * @param dataCenterId 数据中心ID
     */
    public Snowflake(long workerId, long dataCenterId) {
        this(workerId, dataCenterId, false);
    }
    
    /**
     * @param workerId         终端ID
     * @param dataCenterId     数据中心ID
     * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
     */
    public Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) {
        this(null, workerId, dataCenterId, isUseSystemClock);
    }
    
    /**
     * @param epochDate        初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用
     * @param workerId         工作机器节点id
     * @param dataCenterId     数据中心id
     * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
     * @since 5.1.3
     */
    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) {
        this(epochDate, workerId, dataCenterId, isUseSystemClock, DEFAULT_TIME_OFFSET);
    }
    
    /**
     * @param epochDate        初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用
     * @param workerId         工作机器节点id
     * @param dataCenterId     数据中心id
     * @param isUseSystemClock 是否使用{@link SystemClock} 获取当前时间戳
     * @param timeOffset       允许时间回拨的毫秒数
     * @since 5.8.0
     */
    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) {
        this(epochDate, workerId, dataCenterId, isUseSystemClock, timeOffset, 0);
    }
    
    /**
     * @param epochDate           初始化时间起点(null表示默认起始日期),后期修改会导致id重复,如果要修改连workerId dataCenterId,慎用
     * @param workerId            工作机器节点id
     * @param dataCenterId        数据中心id
     * @param isUseSystemClock    是否使用{@link SystemClock} 获取当前时间戳
     * @param timeOffset          允许时间回拨的毫秒数
     * @param randomSequenceLimit 限定一个随机上限,在不同毫秒下生成序号时,给定一个随机数,避免偶数问题,0表示无随机,上限不包括值本身。
     * @since 5.8.0
     */
    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset, long randomSequenceLimit) {
        this.twepoch = (null != epochDate) ? epochDate.getTime() : DEFAULT_TWEPOCH;
        this.workerId = Assert.checkBetween(workerId, 0, MAX_WORKER_ID);
        this.dataCenterId = Assert.checkBetween(dataCenterId, 0, MAX_DATA_CENTER_ID);
        this.useSystemClock = isUseSystemClock;
        this.timeOffset = timeOffset;
        this.randomSequenceLimit = Assert.checkBetween(randomSequenceLimit, 0, SEQUENCE_MASK);
    }
    
    /**
     * 根据Snowflake的ID,获取机器id
     *
     * @param id snowflake算法生成的id
     * @return 所属机器的id
     */
    public long getWorkerId(long id) {
        return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);
    }
    
    /**
     * 根据Snowflake的ID,获取数据中心id
     *
     * @param id snowflake算法生成的id
     * @return 所属数据中心
     */
    public long getDataCenterId(long id) {
        return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);
    }
    
    /**
     * 根据Snowflake的ID,获取生成时间
     *
     * @param id snowflake算法生成的id
     * @return 生成的时间
     */
    public long getGenerateDateTime(long id) {
        return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;
    }
    
    /**
     * 下一个ID
     *
     * @return ID
     */
    public synchronized long nextId() {
        long timestamp = genTime();
        if (timestamp < this.lastTimestamp) {
            if (this.lastTimestamp - timestamp < timeOffset) {
                // 容忍指定的回拨,避免NTP校时造成的异常
                timestamp = lastTimestamp;
            } else {
                // 如果服务器时间有问题(时钟后退) 报错。
                throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
            }
        }
        if (timestamp == this.lastTimestamp) {
            final long sequence = (this.sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
            this.sequence = sequence;
        } else {
            // issue#I51EJY
            if (randomSequenceLimit > 1) {
                sequence = RandomUtil.randomLong(randomSequenceLimit);
            } else {
                sequence = 0L;
            }
        }
        lastTimestamp = timestamp;
        return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT) | (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence;
    }
    
    /**
     * 下一个ID(字符串形式)
     *
     * @return ID 字符串形式
     */
    public String nextIdStr() {
        return Long.toString(nextId());
    }
    
    // ------------------------------------------------------------------------------------------------------------------------------------ Private method start
    
    /**
     * 循环等待下一个时间
     *
     * @param lastTimestamp 上次记录的时间
     * @return 下一个时间
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = genTime();
        // 循环直到操作系统时间戳变化
        while (timestamp == lastTimestamp) {
            timestamp = genTime();
        }
        if (timestamp < lastTimestamp) {
            // 如果发现新的时间戳比上次记录的时间戳数值小,说明操作系统时间发生了倒退,报错
            throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
        }
        return timestamp;
    }
    
    /**
     * 生成时间戳
     *
     * @return 时间戳
     */
    private long genTime() {
        return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
    }
    
    // ------------------------------------------------------------------------------------------------------------------------------------ Private method end
    
    /**
     * 解析雪花算法生成的 ID 为对象
     *
     * @param snowflakeId 雪花算法 ID
     * @return
     */
    public SnowflakeIdInfo parseSnowflakeId(long snowflakeId) {
        SnowflakeIdInfo snowflakeIdInfo = SnowflakeIdInfo.builder().sequence((int) (snowflakeId & ~(-1L << SEQUENCE_BITS))).workerId((int) ((snowflakeId >> WORKER_ID_SHIFT)
                & ~(-1L << WORKER_ID_BITS))).dataCenterId((int) ((snowflakeId >> DATA_CENTER_ID_SHIFT)
                        & ~(-1L << DATA_CENTER_ID_BITS)))
                .timestamp((snowflakeId >> TIMESTAMP_LEFT_SHIFT) + twepoch).build();
        return snowflakeIdInfo;
    }
}

lua脚本

local hashKey = 'snowflake_work_id_key'
local dataCenterIdKey = 'dataCenterId'
local workIdKey = 'workId'

if (redis.call('exists', hashKey) == 0) then
    redis.call('hincrby', hashKey, dataCenterIdKey, 0)
    redis.call('hincrby', hashKey, workIdKey, 0)
    return { 0, 0 }
end

local dataCenterId = tonumber(redis.call('hget', hashKey, dataCenterIdKey))
local workId = tonumber(redis.call('hget', hashKey, workIdKey))

local max = 31
local resultWorkId = 0
local resultDataCenterId = 0

if (dataCenterId == max and workId == max) then
    redis.call('hset', hashKey, dataCenterIdKey, '0')
    redis.call('hset', hashKey, workIdKey, '0')

elseif (workId ~= max) then
    resultWorkId = redis.call('hincrby', hashKey, workIdKey, 1)
    resultDataCenterId = dataCenterId

elseif (dataCenterId ~= max) then
    resultWorkId = 0
    resultDataCenterId = redis.call('hincrby', hashKey, dataCenterIdKey, 1)
    redis.call('hset', hashKey, workIdKey, '0')

end

return { resultWorkId, resultDataCenterId }

九、congomall-flow-monitor-agent

十、congomall-order

观察者模式

(1)自定义事件:继承ApplicationEvent

/**
 * 订单创建事件
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public final class OrderCreateEvent extends ApplicationEvent {
    
    /**
     * 订单聚合根
     */
    @Getter
    private Order order;
    
    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with
     *               which the event is associated (never {@code null})
     */
    public OrderCreateEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }
}

(2)定义事件监听器:实现ApplicationListener

image-20230718232614099

/**
 * 订单创建监听
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Order(0)
@Component
@RequiredArgsConstructor
public final class OrderCreateListener implements ApplicationListener<OrderCreateEvent> {
    
    private final OrderRepository orderRepository;
    
    @Override
    public void onApplicationEvent(OrderCreateEvent event) {
        orderRepository.createOrder(event.getOrder());
    }
}

(3)使用容器发布事件

// 观察者模式: 发布商品下单事件
        ApplicationContextHolder.getInstance().publishEvent(new OrderCreateEvent(this, order));

十一、congomall-idempotent-spring-boot-starter

幂等组件

/**
 * 幂等自动装配
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@EnableConfigurationProperties(IdempotentProperties.class)
public class IdempotentAutoConfiguration {
    
    /**
     * 幂等切面
     */
    @Bean
    public IdempotentAspect idempotentAspect() {
        return new IdempotentAspect();
    }
    
    /**
     * 参数方式幂等实现,基于 RestAPI 场景
     */
    @Bean
    @ConditionalOnMissingBean
    public IdempotentParamService idempotentParamExecuteHandler(RedissonClient redissonClient) {
        return new IdempotentParamExecuteHandler(redissonClient);
    }
    
    /**
     * Token 方式幂等实现,基于 RestAPI 场景
     */
    @Bean
    @ConditionalOnMissingBean
    public IdempotentTokenService idempotentTokenExecuteHandler(DistributedCache distributedCache,
                                                                IdempotentProperties idempotentProperties) {
        return new IdempotentTokenExecuteHandler(distributedCache, idempotentProperties);
    }
    
    /**
     * 申请幂等 Token 控制器,基于 RestAPI 场景
     */
    @Bean
    public IdempotentTokenController idempotentTokenController(IdempotentTokenService idempotentTokenService) {
        return new IdempotentTokenController(idempotentTokenService);
    }
    
    /**
     * SpEL 方式幂等实现,基于 RestAPI 场景
     */
    @Bean
    @ConditionalOnMissingBean
    public IdempotentSpELService idempotentSpELByRestAPIExecuteHandler(RedissonClient redissonClient) {
        return new IdempotentSpELByRestAPIExecuteHandler(redissonClient);
    }
    
    /**
     * SpEL 方式幂等实现,基于 MQ 场景
     */
    @Bean
    @ConditionalOnMissingBean
    public IdempotentSpELByMQExecuteHandler idempotentSpELByMQExecuteHandler(DistributedCache distributedCache) {
        return new IdempotentSpELByMQExecuteHandler(distributedCache);
    }
}

幂等执行处理器工厂

/**
 * 幂等执行处理器工厂
 * <p>
 * Q:可能会有同学有疑问:这里为什么要采用简单工厂模式?策略模式不行么?
 * A:策略模式同样可以达到获取真正幂等处理器功能。但是从设计模式语义来说,简单工厂模式会更合适
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public final class IdempotentExecuteHandlerFactory {
    
    /**
     * 获取幂等执行处理器
     *
     * @param scene 指定幂等验证场景类型
     * @param type  指定幂等处理类型
     * @return 幂等执行处理器
     */
    public static IdempotentExecuteHandler getInstance(IdempotentSceneEnum scene, IdempotentTypeEnum type) {
        IdempotentExecuteHandler result = null;
        switch (scene) {
            case RESTAPI:
                switch (type) {
                    case PARAM:
                        result = ApplicationContextHolder.getBean(IdempotentParamService.class);
                        break;
                    case TOKEN:
                        result = ApplicationContextHolder.getBean(IdempotentTokenService.class);
                        break;
                    case SPEL:
                        result = ApplicationContextHolder.getBean(IdempotentSpELByRestAPIExecuteHandler.class);
                        break;
                    default:
                        break;
                }
                break;
            case MQ:
                result = ApplicationContextHolder.getBean(IdempotentSpELByMQExecuteHandler.class);
                break;
            default:
                break;
        }
        return result;
    }
}
/**
 * 幂等执行处理器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public interface IdempotentExecuteHandler {
    
    /**
     * 幂等处理逻辑
     *
     * @param wrapper 幂等参数包装器
     */
    void handler(IdempotentParamWrapper wrapper);
    
    /**
     * 执行幂等处理逻辑
     *
     * @param joinPoint  AOP 方法处理
     * @param idempotent 幂等注解
     */
    void execute(ProceedingJoinPoint joinPoint, Idempotent idempotent);
    
    /**
     * 异常流程处理
     */
    default void exceptionProcessing() {
        
    }
    
    /**
     * 后置处理
     */
    default void postProcessing() {
        
    }
}

image-20230905231305875

/**
 * 基于 Token 验证请求幂等性, 通常应用于 RestAPI 方法
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@RequiredArgsConstructor
public final class IdempotentTokenExecuteHandler extends AbstractIdempotentTemplate implements IdempotentTokenService {
    
    private final DistributedCache distributedCache;
    private final IdempotentProperties idempotentProperties;
    
    private static final String TOKEN_KEY = "idempotent-token";
    private static final String TOKEN_PREFIX_KEY = "idempotent:token:";
    private static final long TOKEN_EXPIRED_TIME = 6000;
    
    @Override
    protected IdempotentParamWrapper buildWrapper(ProceedingJoinPoint joinPoint) {
        return new IdempotentParamWrapper();
    }
    
    @Override
    public String createToken() {
        String token = Optional.ofNullable(Strings.emptyToNull(idempotentProperties.getPrefix())).orElse(TOKEN_PREFIX_KEY) + UUID.randomUUID();
        distributedCache.put(token, "", Optional.ofNullable(idempotentProperties.getTimeout()).orElse(TOKEN_EXPIRED_TIME));
        return token;
    }
    
    @Override
    public void handler(IdempotentParamWrapper wrapper) {
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        String token = request.getHeader(TOKEN_KEY);
        if (StrUtil.isBlank(token)) {
            token = request.getParameter(TOKEN_KEY);
            if (StrUtil.isBlank(token)) {
                throw new ClientException(BaseErrorCode.IDEMPOTENT_TOKEN_NULL_ERROR);
            }
        }
        Boolean tokenDelFlag = distributedCache.delete(token);
        if (!tokenDelFlag) {
            String errMsg = StrUtil.isNotBlank(wrapper.getIdempotent().message())
                    ? wrapper.getIdempotent().message()
                    : BaseErrorCode.IDEMPOTENT_TOKEN_DELETE_ERROR.message();
            throw new ClientException(errMsg, BaseErrorCode.IDEMPOTENT_TOKEN_DELETE_ERROR);
        }
    }
}

这段代码中利用了分布式缓存 Redis 来实现幂等性。distributedCache 属性就是用来操作 Redis 缓存的对象。在 createToken() 方法中,通过调用 distributedCache.put() 将生成的幂等 Token 存储到 Redis 缓存中,并设置过期时间。在 handler() 方法中,使用 distributedCache.delete() 方法从 Redis 缓存中删除幂等 Token。

通过将幂等 Token 存储在 Redis 中,可以实现分布式环境下的幂等性控制。不同节点上的服务可以通过共享同一个 Redis 缓存,确保相同的 Token 在任意节点上只能被成功处理一次。这样就能避免重复执行相同操作所带来的副作用和数据不一致性问题。

抽象模板方法

/**
 * 抽象幂等执行处理器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public abstract class AbstractIdempotentTemplate implements IdempotentExecuteHandler {
    
    /**
     * 构建幂等验证过程中所需要的参数包装器
     *
     * @param joinPoint AOP 方法处理
     * @return 幂等参数包装器
     */
    protected abstract IdempotentParamWrapper buildWrapper(ProceedingJoinPoint joinPoint);
    
    /**
     * 执行幂等处理逻辑
     *
     * @param joinPoint  AOP 方法处理
     * @param idempotent 幂等注解
     */
    public void execute(ProceedingJoinPoint joinPoint, Idempotent idempotent) {
        // 模板方法模式:构建幂等参数包装器
        IdempotentParamWrapper idempotentParamWrapper = buildWrapper(joinPoint).setIdempotent(idempotent);
        handler(idempotentParamWrapper);
    }
}

AOP拦截器

/**
 * 幂等注解 AOP 拦截器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Aspect
public final class IdempotentAspect {
    
    /**
     * 增强方法标记 {@link Idempotent} 注解逻辑
     */
    @Around("@annotation(org.opengoofy.congomall.springboot.starter.idempotent.annotation.Idempotent)")
    public Object idempotentHandler(ProceedingJoinPoint joinPoint) throws Throwable {
        Idempotent idempotent = getIdempotent(joinPoint);
        IdempotentExecuteHandler instance = IdempotentExecuteHandlerFactory.getInstance(idempotent.scene(), idempotent.type());
        try {
            instance.execute(joinPoint, idempotent);
            return joinPoint.proceed();
        } catch (RepeatConsumptionException ex) {
            if (!ex.getError()) {
                return null;
            }
            throw ex;
        } finally {
            instance.postProcessing();
            IdempotentContext.clean();
        }
    }
    
    public static Idempotent getIdempotent(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
        return targetMethod.getAnnotation(Idempotent.class);
    }
}

十二、congomall-log-spring-boot-starter

@MLog

/**
 * Log 注解打印,可以标记在类或者方法上
 * 标记在类上,类下所有方法都会打印;标记在方法上,仅打印标记方法;如果类或者方法上都有标记,以方法上注解为准
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MLog {
    
    /**
     * 入参打印
     *
     * @return 打印结果中是否包含入参,{@link Boolean#TRUE} 打印,{@link Boolean#FALSE} 不打印
     */
    boolean input() default true;
    
    /**
     * 出参打印
     *
     * @return 打印结果中是否包含出参,{@link Boolean#TRUE} 打印,{@link Boolean#FALSE} 不打印
     */
    boolean output() default true;
}
日志输出控制台
/**
 * {@link MLog} 日志打印 AOP 切面
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Aspect
public class MLogPrintAspect {
    
    /**
     * 打印类或方法上的 {@link MLog}
     */
    @Around("@within(org.opengoofy.congomall.springboot.starter.log.annotation.MLog) || @annotation(org.opengoofy.congomall.springboot.starter.log.annotation.MLog)")
    public Object printMLog(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = SystemClock.now();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Logger log = LoggerFactory.getLogger(methodSignature.getDeclaringType());
        String beginTime = DateUtil.now();
        Object result = null;
        try {
            result = joinPoint.proceed();
        } finally {
            Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
            MLog logAnnotation = Optional.ofNullable(targetMethod.getAnnotation(MLog.class)).orElse(joinPoint.getTarget().getClass().getAnnotation(MLog.class));
            if (logAnnotation != null) {
                MLogPrint logPrint = new MLogPrint();
                logPrint.setBeginTime(beginTime);
                if (logAnnotation.input()) {
                    logPrint.setInputParams(buildInput(joinPoint));
                }
                if (logAnnotation.output()) {
                    logPrint.setOutputParams(result);
                }
                String methodType = "", requestURI = "";
                try {
                    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                    methodType = servletRequestAttributes.getRequest().getMethod();
                    requestURI = servletRequestAttributes.getRequest().getRequestURI();
                } catch (Exception ignored) {
                }
                log.info("[{}] {}, executeTime: {}ms, info: {}", methodType, requestURI, SystemClock.now() - startTime, JSON.toJSONString(logPrint));
            }
        }
        return result;
    }
    
    private Object[] buildInput(ProceedingJoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Object[] printArgs = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if ((args[i] instanceof HttpServletRequest) || args[i] instanceof HttpServletResponse) {
                continue;
            }
            if (args[i] instanceof byte[]) {
                printArgs[i] = "byte array";
            } else if (args[i] instanceof MultipartFile) {
                printArgs[i] = "file";
            } else {
                printArgs[i] = args[i];
            }
        }
        return printArgs;
    }
    
    @Data
    private class MLogPrint {
        
        private String beginTime;
        
        private Object[] inputParams;
        
        private Object outputParams;
    }
}

十三、congomall-minio-spring-boot-starter

整合minio

/**
 * Minio 自动装配配置
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@AllArgsConstructor
@EnableConfigurationProperties(MinioProperties.class)
public class MinioAutoConfiguration {
    
    private final MinioProperties minioProperties;
    
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(minioProperties.getEndpoint())
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();
    }
    
    @Bean
    public MinioTemplate minioTemplate(MinioClient minioClient) {
        return new MinioTemplate(minioClient, minioProperties);
    }
}
/**
 * Minio 配置类
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Data
@ConfigurationProperties(prefix = MinioProperties.PREFIX)
public class MinioProperties {
    
    public static final String PREFIX = "minio";
    
    /**
     * 端点,minio 地址
     */
    private String endpoint;
    
    /**
     * accessKey
     */
    private String accessKey;
    
    /**
     * secretKey
     */
    private String secretKey;
    
    /**
     * bucket
     */
    private String bucket;
}

抽象方法

/**
 * 抽象一组基本 Minio 操作的接口,由 {@link MinioTemplate} 实现
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public interface MinioOperations {
    
    /**
     * 上传文件
     *
     * @param args
     * @return
     */
    ObjectWriteResponse upload(PutObjectArgs args);
    
    /**
     * 上传文件
     *
     * @param multipartFile
     * @return 文件名
     */
    String upload(MultipartFile multipartFile);
}

模板方法使用-MinioTemplate

/**
 * Minio 模板操作
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@AllArgsConstructor
public class MinioTemplate implements MinioOperations {
    
    private final MinioClient minioClient;
    
    private final MinioProperties minioProperties;
    
    @SneakyThrows
    @Override
    public ObjectWriteResponse upload(PutObjectArgs args) {
        return minioClient.putObject(args);
    }
    
    @SneakyThrows
    @Override
    public String upload(MultipartFile multipartFile) {
        String fileName = multipartFile.getOriginalFilename();
        String[] fileNameSplit = fileName.split("\\.");
        fileName = fileNameSplit.length > 1 ? fileNameSplit[0] + "_" + System.currentTimeMillis() + "." + fileNameSplit[1] : fileName + "_" + System.currentTimeMillis();
        try (InputStream inputStream = multipartFile.getInputStream()) {
            PutObjectArgs objectArgs = PutObjectArgs.builder()
                    .bucket(minioProperties.getBucket())
                    .object(fileName)
                    .stream(inputStream, inputStream.available(), -1L)
                    .contentType(multipartFile.getContentType())
                    .build();
            minioClient.putObject(objectArgs);
        }
        return fileName;
    }
}

十四、congomall-openfeign-spring-boot-starter

十五、congomall-rocketmq-spring-boot-starter

整合@StreamListener 是 Spring Cloud Stream 框架提供的注解,用于定义消息流的消息监听器。它可以与各种消息中间件(如 RabbitMQ、Apache Kafka 等)进行集成。

/**
 * {@link StreamListener} 日志环绕打印
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Aspect
public final class StreamListenerLogPrintAspect {
    
    @SneakyThrows
    @Around("@within(org.springframework.cloud.stream.annotation.StreamListener) || @annotation(org.springframework.cloud.stream.annotation.StreamListener)")
    public Object streamListenerLogPrint(ProceedingJoinPoint joinPoint) {
        Object result;
        boolean executeResult = true;
        long startTime = System.currentTimeMillis();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Logger log = LoggerFactory.getLogger(methodSignature.getDeclaringType());
        try {
            result = joinPoint.proceed();
        } catch (Throwable ex) {
            executeResult = false;
            throw ex;
        } finally {
            Object[] args = joinPoint.getArgs();
            if (args != null && args.length > 0) {
                Optional<MessageWrapper> messageWrapperOptional = Arrays.stream(args)
                        .filter(each -> each instanceof MessageWrapper)
                        .map(each -> (MessageWrapper) each)
                        .findFirst();
                if (messageWrapperOptional.isPresent()) {
                    MessageWrapper messageWrapper = messageWrapperOptional.get();
                    log.info("Execute result: {}, Keys: {}, Dispatch time: {} ms, Execute time: {} ms, Message: {}",
                            executeResult,
                            messageWrapper.getKeys(),
                            System.currentTimeMillis() - messageWrapper.getTimestamp(),
                            System.currentTimeMillis() - startTime,
                            JSON.toJSONString(messageWrapper.getMessage()));
                }
            }
        }
        return result;
    }
}

发送消费消息

/**
 * 发送延迟队列取消未付款订单监听
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Order(3)
@Component
@RequiredArgsConstructor
public class DelayCloseOrderListener implements ApplicationListener<OrderCreateEvent> {
    
    private final DelayCloseOrderProvide delayCloseOrderProvide;
    
    @Override
    public void onApplicationEvent(OrderCreateEvent event) {
        delayCloseOrderProvide.delayCloseOrderSend(
                new DelayCloseOrderEvent(
                        event.getOrder().getOrderSn(),
                        event.getOrder().getOrderProducts().stream()
                                .map(each -> new ProductSkuStockDTO(String.valueOf(each.getProductId()), String.valueOf(each.getProductSkuId()), each.getProductQuantity()))
                                .collect(Collectors.toList())));
    }
}
    public void delayCloseOrderSend(DelayCloseOrderEvent delayCloseOrderEvent) {
        String keys = UUID.randomUUID().toString();
        Message<?> message = MessageBuilder
                .withPayload(new MessageWrapper(keys, delayCloseOrderEvent))
                .setHeader(MessageConst.PROPERTY_KEYS, keys)
                .setHeader(MessageConst.PROPERTY_TAGS, OrderRocketMQConstants.DELAY_CLOSE_ORDER_TAG)
                // RocketMQ 延迟消息级别 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
                // 16 代表 30m,为了演示效果所以选择该级别,正常按照需求设置
                .setHeader(MessageConst.PROPERTY_DELAY_TIME_LEVEL, 16)
                .build();
        long startTime = System.currentTimeMillis();
        boolean sendResult = false;
        try {
            sendResult = orderOutput.send(message, 2000L);
        } finally {
            log.info("延迟关闭订单消息发送,发送状态: {}, Keys: {}, 执行时间: {} ms, 消息内容: {}", sendResult, keys, System.currentTimeMillis() - startTime, JSON.toJSONString(delayCloseOrderEvent));
        }
    }

监听消费消息

/**
 * 支付结果通知消息消费者
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class PayResultNotifyMessageConsumer {
    
    private final OrderRepository orderRepository;
    
    @StreamListener(OrderSink.PAY_RESULT_NOTIFY)
    public void delayCloseOrderConsumer(MessageWrapper<PayResultNotifyMessageEvent> messageWrapper) {
        PayResultNotifyMessageEvent event = messageWrapper.getMessage();
        Order order = Order.builder()
                .orderSn(event.getOrderSn())
                .status(OrderStatusEnum.TO_BE_DELIVERED.getStatus())
                .build();
        orderRepository.statusReversal(order);
    }
}

装饰者模式

new MessageWrapper(keys, customerOperationLogEvent)

装饰各类事件

/**
 * 消息体包装器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
public final class MessageWrapper<T> implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 消息发送Keys
     */
    @NonNull
    private String keys;
    
    /**
     * 消息体
     */
    @NonNull
    private T message;
    
    /**
     * 唯一标识,用于客户端幂等验证
     */
    private String uuid = UUID.randomUUID().toString();
    
    /**
     * 消息发送时间
     */
    private Long timestamp = System.currentTimeMillis();
}

image-20230906014009722

事件消息打印

/**
 * {@link StreamListener} 日志环绕打印
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Aspect
public final class StreamListenerLogPrintAspect {
    
    @SneakyThrows
    @Around("@within(org.springframework.cloud.stream.annotation.StreamListener) || @annotation(org.springframework.cloud.stream.annotation.StreamListener)")
    public Object streamListenerLogPrint(ProceedingJoinPoint joinPoint) {
        Object result;
        boolean executeResult = true;
        long startTime = System.currentTimeMillis();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Logger log = LoggerFactory.getLogger(methodSignature.getDeclaringType());
        try {
            result = joinPoint.proceed();
        } catch (Throwable ex) {
            executeResult = false;
            throw ex;
        } finally {
            Object[] args = joinPoint.getArgs();
            if (args != null && args.length > 0) {
                Optional<MessageWrapper> messageWrapperOptional = Arrays.stream(args)
                        .filter(each -> each instanceof MessageWrapper)
                        .map(each -> (MessageWrapper) each)
                        .findFirst();
                if (messageWrapperOptional.isPresent()) {
                    MessageWrapper messageWrapper = messageWrapperOptional.get();
                    log.info("Execute result: {}, Keys: {}, Dispatch time: {} ms, Execute time: {} ms, Message: {}",
                            executeResult,
                            messageWrapper.getKeys(),
                            System.currentTimeMillis() - messageWrapper.getTimestamp(),
                            System.currentTimeMillis() - startTime,
                            JSON.toJSONString(messageWrapper.getMessage()));
                }
            }
        }
        return result;
    }
}

十六、congomall-sensitive-spring-boot-starter

十七、congomall-swagger-spring-boot-starter

启动打印api地址

/**
 * Knife4j api 地址打印
 */
@Slf4j
@AllArgsConstructor
public class Knife4jDocUrlPrintHandler implements ApplicationRunner {
    
    private final ConfigurableEnvironment environment;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("API Document: http://127.0.0.1:{}{}/doc.html", environment.getProperty("server.port", "8080"), environment.getProperty("server.servlet.context-path", ""));
    }
}

Swagger 自动装配

/**
 * Swagger 自动装配
 */
@EnableOpenApi
@Profile({"dev", "local", "test"})
@ConditionalOnProperty(name = "congomall.swagger.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties({SwaggerProperties.class})
public class SwaggerAutoConfiguration {
    
    @Bean
    public Docket docketApi(SwaggerProperties swaggerProperties) {
        List<Predicate<String>> excludePath = new ArrayList<>();
        swaggerProperties.getExcludePath().forEach(path -> excludePath.add(PathSelectors.ant(path)));
        Docket defaultDocket = new Docket(DocumentationType.OAS_30)
                .host(swaggerProperties.getHost())
                .apiInfo(apiInfo(swaggerProperties)).select()
                .apis(RequestHandlerSelectors.basePackage(swaggerProperties.getBasePackage()))
                .paths(PathSelectors.regex("/error.*").negate())
                .paths(PathSelectors.regex("/initialize/dispatcher-servlet").negate())
                .paths(PathSelectors.regex("/actuator.*").negate())
                .paths(PathSelectors.any())
                .build()
                .securityContexts(Collections.singletonList(securityContext(swaggerProperties)));
        String groupName = swaggerProperties.getGroupName();
        if (StrUtil.isNotBlank(groupName)) {
            defaultDocket.groupName(groupName);
        }
        return defaultDocket;
    }
    
    private SecurityContext securityContext(SwaggerProperties swaggerProperties) {
        return SecurityContext.builder()
                .securityReferences(defaultAuth(swaggerProperties))
                .build();
    }
    
    private List<SecurityReference> defaultAuth(SwaggerProperties swaggerProperties) {
        ArrayList<AuthorizationScope> authorizationScopeList = new ArrayList<>();
        swaggerProperties.getAuthorization().getAuthorizationScopeList()
                .forEach(authorizationScope -> authorizationScopeList.add(new AuthorizationScope(authorizationScope.getScope(), authorizationScope.getDescription())));
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[authorizationScopeList.size()];
        return Collections.singletonList(SecurityReference.builder()
                .reference(swaggerProperties.getAuthorization().getName())
                .scopes(authorizationScopeList.toArray(authorizationScopes))
                .build());
    }
    
    private ApiInfo apiInfo(SwaggerProperties swaggerProperties) {
        return new ApiInfoBuilder()
                .title(swaggerProperties.getTitle())
                .description(swaggerProperties.getDescription())
                .license(swaggerProperties.getLicense())
                .licenseUrl(swaggerProperties.getLicenseUrl())
                .termsOfServiceUrl(swaggerProperties.getTermsOfServiceUrl())
                .contact(new Contact(swaggerProperties.getContact().getName(), swaggerProperties.getContact().getUrl(), swaggerProperties.getContact().getEmail()))
                .version(swaggerProperties.getVersion())
                .build();
    }
    
    @Bean
    public Knife4jDocUrlPrintHandler knife4jDocUrlPrintHandler(ConfigurableEnvironment environment) {
        return new Knife4jDocUrlPrintHandler(environment);
    }
}

十八、congomall-web-spring-boot-starter

Web 组件自动装配

/**
 * Web 组件自动装配
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Configuration
public class WebAutoConfiguration {
    
    public final static String INITIALIZE_PATH = "/initialize/dispatcher-servlet";
    
    @Bean
    @ConditionalOnMissingBean
    public GlobalExceptionHandler congoMallGlobalExceptionHandler() {
        return new GlobalExceptionHandler();
    }
    
    @Bean
    public InitializeDispatcherServletController initializeDispatcherServletController() {
        return new InitializeDispatcherServletController();
    }
    
    @Bean
    public RestTemplate simpleRestTemplate(ClientHttpRequestFactory factory) {
        return new RestTemplate(factory);
    }
    
    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(5000);
        factory.setConnectTimeout(5000);
        return factory;
    }
    
    @Bean
    public InitializeDispatcherServletHandler initializeDispatcherServletHandler(RestTemplate simpleRestTemplate, ConfigurableEnvironment configurableEnvironment) {
        return new InitializeDispatcherServletHandler(simpleRestTemplate, configurableEnvironment);
    }
}

显式初始化DispatcherServlet

如果不进行显式的初始化 DispatcherServlet,可能会导致以下后果:

  1. 请求无法正确分发:没有初始化 DispatcherServlet 的话,它将无法接收和分发请求。这意味着无论客户端发送什么请求,都无法找到合适的处理器来处理该请求,最终会返回 404 Not Found 错误或者其他类似的错误。
  2. 缺少必要的配置:DispatcherServlet 在初始化过程中会加载和解析配置文件,如 Spring MVC 配置文件。如果没有进行初始化,相关的配置信息将无法加载,导致框架行为无法正常工作。例如,请求的处理方式、拦截器、视图解析器等功能将无法使用,从而影响请求的处理结果。
  3. Bean 对象无法管理:DispatcherServlet 在初始化过程中会创建和管理一系列的 Bean 对象,包括控制器、视图解析器、数据绑定器等。如果没有进行初始化,这些 Bean 将无法被正确地创建和管理,导致在请求处理过程中无法使用这些重要的组件。
  4. URL 映射错误:DispatcherServlet 在初始化时会建立 URL 与处理器的映射关系,以便根据请求的 URL 找到对应的处理器进行处理。如果没有进行初始化,就无法建立正确的映射关系,因此无法通过 URL 来定位到具体的处理器,导致请求无法被正确处理。

综上所述,没有进行初始化 DispatcherServlet 将导致请求无法正确分发、缺少必要的配置、无法管理 Bean 对象以及 URL 映射错误等后果。这将导致应用程序无法正常处理请求,功能无法运作,最终会影响用户体验和应用程序的正常运行。因此,正确地初始化 DispatcherServlet 是确保应用程序能够正常工作的关键步骤之一。

/**
 * 通过 {@link InitializeDispatcherServletController} 初始化 {@link DispatcherServlet}
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@RequiredArgsConstructor
public final class InitializeDispatcherServletHandler implements CommandLineRunner {
    
    private final RestTemplate restTemplate;
    
    private final ConfigurableEnvironment configurableEnvironment;
    
    @Override
    public void run(String... args) throws Exception {
        String url = String.format("http://127.0.0.1:%s%s",
                configurableEnvironment.getProperty("server.port", "8080") + configurableEnvironment.getProperty("server.servlet.context-path", ""),
                INITIALIZE_PATH);
        try {
            restTemplate.execute(url, HttpMethod.GET, null, null);
        } catch (Throwable ignored) {
        }
    }
}

全局异常处理器

/**
 * 全局异常处理器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 拦截参数验证异常
     */
    @SneakyThrows
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
        String exceptionStr = Optional.ofNullable(firstFieldError)
                .map(FieldError::getDefaultMessage)
                .orElse(StrUtil.EMPTY);
        log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
        return Results.failure(BaseErrorCode.CLIENT_ERROR.code(), exceptionStr);
    }
    
    /**
     * 拦截应用内抛出的异常
     */
    @ExceptionHandler(value = {AbstractException.class})
    public Result abstractException(HttpServletRequest request, AbstractException ex) {
        if (ex.getCause() != null) {
            log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString(), ex.getCause());
            return Results.failure(ex);
        }
        log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString());
        return Results.failure(ex);
    }
    
    /**
     * 拦截未捕获异常
     */
    @ExceptionHandler(value = Throwable.class)
    public Result defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
        log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
        return Results.failure();
    }
    
    private String getUrl(HttpServletRequest request) {
        if (StringUtils.isEmpty(request.getQueryString())) {
            return request.getRequestURL().toString();
        }
        return request.getRequestURL().toString() + "?" + request.getQueryString();
    }
}

通用全局返回对象

/**
 * 全局返回对象构造器
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public final class Results {
    
    /**
     * 构造成功响应
     *
     * @return
     */
    public static Result<Void> success() {
        return new Result<Void>()
                .setCode(Result.SUCCESS_CODE)
                .setRequestId(TraceContext.traceId());
    }
    
    /**
     * 构造带返回数据的成功响应
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> Result<T> success(T data) {
        return new Result<T>()
                .setCode(Result.SUCCESS_CODE)
                .setRequestId(TraceContext.traceId())
                .setData(data);
    }
    
    /**
     * 构建服务端失败响应
     *
     * @return
     */
    protected static Result<Void> failure() {
        return new Result<Void>()
                .setCode(BaseErrorCode.SERVICE_ERROR.code())
                .setRequestId(TraceContext.traceId())
                .setMessage(BaseErrorCode.SERVICE_ERROR.message());
    }
    
    /**
     * 通过 {@link AbstractException} 构建失败响应
     *
     * @param abstractException
     * @return
     */
    protected static Result<Void> failure(AbstractException abstractException) {
        String errorCode = Optional.ofNullable(abstractException.getErrorCode())
                .orElse(BaseErrorCode.SERVICE_ERROR.code());
        String errorMessage = Optional.ofNullable(abstractException.getErrorMessage())
                .orElse(BaseErrorCode.SERVICE_ERROR.message());
        return new Result<Void>()
                .setCode(errorCode)
                .setRequestId(TraceContext.traceId())
                .setMessage(errorMessage);
    }
    
    /**
     * 通过 errorCode、errorMessage 构建失败响应
     *
     * @param errorCode
     * @param errorMessage
     * @return
     */
    protected static Result<Void> failure(String errorCode, String errorMessage) {
        return new Result<Void>()
                .setCode(errorCode)
                .setRequestId(TraceContext.traceId())
                .setMessage(errorMessage);
    }
}

十九、congomall-xxljob-spring-boot-starter

XXL-Job 自动装配

/**
 * XXL-Job 自动装配
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @公众号 马丁玩编程,关注回复:资料,领取后端技术专家成长手册
 */
public class XXLJobAutoConfiguration {
    
    @Value("${xxl-job.admin.addresses}")
    private String adminAddresses;
    
    @Value("${xxl-job.accessToken}")
    private String accessToken;
    
    @Value("${xxl-job.executor.appname}")
    private String appname;
    
    @Value("${xxl-job.executor.ip}")
    private String ip;
    
    @Value("${xxl-job.executor.port}")
    private int port;
    
    @Value("${xxl-job.executor.logpath}")
    private String logPath;
    
    @Value("${xxl-job.executor.logretentiondays}")
    private int logRetentionDays;
    
    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
        return xxlJobSpringExecutor;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值