首页 > 编程语言 >任务调度处理系列之 Spring源码分析-【SchedulingConfigurer实现原理】转

任务调度处理系列之 Spring源码分析-【SchedulingConfigurer实现原理】转

时间:2023-12-19 18:13:37浏览次数:38  
标签:null Spring private task bean 源码 new 任务调度 public

 

一、可能的场景
在做业务平台的时候我们经常会遇到,某些跟时间打交道的需要修改状态,比如说在时间区间之前,属于未生效状态,区间之内属于有效期,区间之后,属于过期,或者需要每天 每周 每月,甚至是年为单位的做一些固定的操作。通过定时任务可以通过开启定时任务来完成这些需求。

我做合同管理模块,合同有未生效,已生效,已过期,三个状态,不可能每次用户登录的时候去判断这个状态,然后修改,这样做会在登录的逻辑里边耦合了合同业务逻辑,同时消耗了登录时间,不太可取。

还有一些需要报表统计类的批量处理任务,任务量大,类别也很多,就可以使用这个处理。当然也可以使用中间件来处理(elastic-job、xxl-job),不过本文的主题不是这个,而是spring中的实现SchedulingConfigurer。

二、基本原理
需要联合使用@EnableScheduling与org.springframework.scheduling.annotation.SchedulingConfigurer完成调度配置,在SchedulingConfigurer接口中,需要实现一个void configureTasks(ScheduledTaskRegistrar taskRegistrar)方法ScheduledTaskRegistrar,我们发现该对象初始化完成后会执行scheduleTasks()方法,在该方法中添加任务调度信息,最终所有的任务信息都存放在名为scheduledFutures的集合中,通过Jdk的Executors.newSingleThreadScheduledExecutor()和配置的时间规则进行任务的调度执行。

我的Github地址: https://github.com/cheriduk/spring-boot-integration-template

首先全局看一下:

 

 

三、源码实现细节
从开启注解@EnableScheduling开始

 

 

 

 

 

自己觉得还是图解源码好的,截图如下:

 

 

 

 

 

 

 

 

 

 

 

数据流转图:

 

 

 

四、核心类回顾
ScheduledAnnotationBeanPostProcessor

Scheduled注解后处理器,项目启动时会扫描所有标记了@Scheduled注解的方法,封装成ScheduledTask注册起来。

这个处理器是处理定时任务的核心类

// 首先:非常震撼的是,它实现的接口非常的多。还好的是,大部分接口我们都很熟悉了。
// MergedBeanDefinitionPostProcessor:它是个BeanPostProcessor
// DestructionAwareBeanPostProcessor:在销毁此Bean的时候,会调用对应方法
// SmartInitializingSingleton:它会在所有的单例Bean都完成了初始化后,调用这个接口的方法
// EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware:都是些感知接口
// DisposableBean:该Bean销毁的时候会调用
// ApplicationListener<ContextRefreshedEvent>:监听容器的`ContextRefreshedEvent`事件
// ScheduledTaskHolder:维护本地的ScheduledTask实例
public class ScheduledAnnotationBeanPostProcessor
implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {

/**
* The default name of the {@link TaskScheduler} bean to pick up: "taskScheduler".
* <p>Note that the initial lookup happens by type; this is just the fallback
* in case of multiple scheduler beans found in the context.
* @since 4.2
*/
// 看着注释就知道,和@Async的默认处理一样~~~~先类型 在回退到名称
public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";
// 调度器(若我们没有配置,它是null的)
@Nullable
private Object scheduler;

// 这些都是Awire感知接口注入进来的~~
@Nullable
private StringValueResolver embeddedValueResolver;
@Nullable
private String beanName;
@Nullable
private BeanFactory beanFactory;
@Nullable
private ApplicationContext applicationContext;

// ScheduledTaskRegistrar:ScheduledTask注册中心,ScheduledTaskHolder接口的一个重要的实现类,维护了程序中所有配置的ScheduledTask
// 内部会处理调取器得工作,因此我建议先移步,看看这个类得具体分析
private final ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();

// 缓存,没有被标注注解的class们
// 这有个技巧,使用了newSetFromMap,自然而然的这个set也就成了一个线程安全的set
private final Set<Class<?>> nonAnnotatedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>(64));

// 缓存对应的Bean上 里面对应的 ScheduledTask任务。可议有多个哦~~
// 注意:此处使用了IdentityHashMap
private final Map<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16);

// 希望此processor是最后执行的~
@Override
public int getOrder() {
return LOWEST_PRECEDENCE;
}

//Set the {@link org.springframework.scheduling.TaskScheduler} that will invoke the scheduled methods
// 也可以是JDK的ScheduledExecutorService(内部会给你包装成一个TaskScheduler)
// 若没有指定。那就会走默认策略:去从起中先按照类型找`TaskScheduler`该类型(或者ScheduledExecutorService这个类型也成)的。
// 若有多个该类型或者找不到,就安好"taskScheduler"名称去找
// 再找不到,就用系统默认的:
public void setScheduler(Object scheduler) {
this.scheduler = scheduler;
}
...

// 此方法会在该容器内所有的单例Bean已经初始化全部结束后,执行
@Override
public void afterSingletonsInstantiated() {
// Remove resolved singleton classes from cache
// 因为已经是最后一步了,所以这个缓存可议清空了
this.nonAnnotatedClasses.clear();

// 在容器内运行,ApplicationContext都不会为null
if (this.applicationContext == null) {
// Not running in an ApplicationContext -> register tasks early...
// 如果不是在ApplicationContext下运行的,那么就应该提前注册这些任务
finishRegistration();
}
}

// 兼容容器刷新的时间(此时候容器硬启动完成了) 它还在`afterSingletonsInstantiated`的后面执行
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 这个动作务必要做:因为Spring可能有多个容器,所以可能会发出多个ContextRefreshedEvent 事件
// 显然我们只处理自己容器发出来得事件,别的容器发出来我不管~~
if (event.getApplicationContext() == this.applicationContext) {
// Running in an ApplicationContext -> register tasks this late...
// giving other ContextRefreshedEvent listeners a chance to perform
// their work at the same time (e.g. Spring Batch's job registration).
// 为其他ContextRefreshedEvent侦听器提供同时执行其工作的机会(例如,Spring批量工作注册)
finishRegistration();
}
}

private void finishRegistration() {
// 如果setScheduler了,就以调用者指定的为准~~~
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}

// 这里继续厉害了:从容器中找到所有的接口`SchedulingConfigurer`的实现类(我们可议通过实现它定制化scheduler)
if (this.beanFactory instanceof ListableBeanFactory) {
Map<String, SchedulingConfigurer> beans =
((ListableBeanFactory) this.beanFactory).getBeansOfType(SchedulingConfigurer.class);
List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());

// 同@Async只允许设置一个不一样的是,这里每个都会让它生效
// 但是平时使用,我们自顶一个类足矣~~~
AnnotationAwareOrderComparator.sort(configurers);
for (SchedulingConfigurer configurer : configurers) {
configurer.configureTasks(this.registrar);
}
}

// 至于task是怎么注册进registor的,请带回看`postProcessAfterInitialization`这个方法的实现
// 有任务并且registrar.getScheduler() == null,那就去容器里找来试试~~~
if (this.registrar.hasTasks() && this.registrar.getScheduler() == null) {
...
// 这块逻辑和@Async的处理一毛一样。忽略了 主要看看resolveSchedulerBean()这个方法即可
}

this.registrar.afterPropertiesSet();
}
// 从容器中去找一个
private <T> T resolveSchedulerBean(BeanFactory beanFactory, Class<T> schedulerType, boolean byName) {
// 若按名字去查找,那就按照名字找
if (byName) {
T scheduler = beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME, schedulerType);

// 这个处理非常非常有意思,就是说倘若找到了你可以在任意地方直接@Autowired这个Bean了,可以拿这个共用Scheduler来调度我们自己的任务啦~~
if (this.beanName != null && this.beanFactory instanceof ConfigurableBeanFactory) {
((ConfigurableBeanFactory) this.beanFactory).registerDependentBean(
DEFAULT_TASK_SCHEDULER_BEAN_NAME, this.beanName);
}
return scheduler;
}
// 按照schedulerType该类型的名字匹配resolveNamedBean 底层依赖:getBeanNamesForType
else if (beanFactory instanceof AutowireCapableBeanFactory) {
NamedBeanHolder<T> holder = ((AutowireCapableBeanFactory) beanFactory).resolveNamedBean(schedulerType);
if (this.beanName != null && beanFactory instanceof ConfigurableBeanFactory) {
((ConfigurableBeanFactory) beanFactory).registerDependentBean(holder.getBeanName(), this.beanName);
}
return holder.getBeanInstance();
}
// 按照类型找
else {
return beanFactory.getBean(schedulerType);
}
}


@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}

// Bean初始化完成后执行。去看看Bean里面有没有标注了@Scheduled的方法~~
@Override
public Object postProcessAfterInitialization(final Object bean, String beanName) {
// 拿到目标类型(因为此类有可能已经被代理过)
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
// 这里对没有标注注解的类做了一个缓存,防止从父去扫描(毕竟可能有多个容器,可能有重复扫描的现象)
if (!this.nonAnnotatedClasses.contains(targetClass)) {

// 如下:主要用到了MethodIntrospector.selectMethods 这个内省方法工具类的这个g工具方法,去找指定Class里面,符合条件的方法们
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {

//过滤Method的核心逻辑就是是否标注有此注解(Merged表示标注在父类、或者接口处也是ok的)
Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
});
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(targetClass);
if (logger.isTraceEnabled()) {
logger.trace("No @Scheduled annotations found on bean class: " + bean.getClass());
}
}
// 此处相当于已经找到了对应的注解方法~~~
else {
// Non-empty set of methods
// 这里有一个双重遍历。因为一个方法上,可能重复标注多个这样的注解~~~~~
// 所以最终遍历出来后,就交给processScheduled(scheduled, method, bean)去处理了
annotatedMethods.forEach((method, scheduledMethods) ->
scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
if (logger.isDebugEnabled()) {
logger.debug(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + "': " + annotatedMethods);
}
}
}
return bean;
}

// 这个方法就是灵魂了。就是执行这个注解,最终会把这个任务注册进去,并且启动的~~~
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
// 标注此注解的方法必须是无参的方法
Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");

// 拿到最终要被调用的方法 做这么一步操作主要是防止方法被代理了
Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass());
// 把该方法包装成一个Runnable 线程~~~
Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod);
boolean processedSchedule = false;
String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

// 装载任务,这里长度定为4,因为Spring认为标注4个注解还不够你用的?
Set<ScheduledTask> tasks = new LinkedHashSet<>(4);

// Determine initial delay
// 计算出延时多长时间执行 initialDelayString 支持占位符如:@Scheduled(fixedDelayString = "${time.fixedDelay}")
// 这段话得意思是,最终拿到一个initialDelay值~~~~~Long型的
long initialDelay = scheduled.initialDelay();
String initialDelayString = scheduled.initialDelayString();
if (StringUtils.hasText(initialDelayString)) {
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
if (this.embeddedValueResolver != null) {
initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
}
if (StringUtils.hasLength(initialDelayString)) {
try {
initialDelay = parseDelayAsLong(initialDelayString);
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
}
}
}

// Check cron expression
// 解析cron
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
String zone = scheduled.zone();
// 由此可见,cron也可以使用占位符。把它配置在配置文件里就成~~~zone也是支持占位符的
if (this.embeddedValueResolver != null) {
cron = this.embeddedValueResolver.resolveStringValue(cron);
zone = this.embeddedValueResolver.resolveStringValue(zone);
}
if (StringUtils.hasLength(cron)) {
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
processedSchedule = true;
TimeZone timeZone;
if (StringUtils.hasText(zone)) {
timeZone = StringUtils.parseTimeZoneString(zone);
}
else {
timeZone = TimeZone.getDefault();
}

// 这个相当于,如果配置了cron,它就是一个task了,就可以吧任务注册进registrar里面了
// 这里面的处理是。如果已经有调度器taskScheduler了,那就立马准备执行了
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
}

// At this point we don't need to differentiate between initial delay set or not anymore
if (initialDelay < 0) {
initialDelay = 0;
}
...
// 下面就不再说了,就是解析fixed delay、fixed rated、

// Check whether we had any attribute set
Assert.isTrue(processedSchedule, errorMessage);

// Finally register the scheduled tasks
// 最后吧这些任务都放在全局属性里保存起来~~~~
// getScheduledTasks()方法是会把所有的任务都返回出去的~~~ScheduledTaskHolder接口就一个Set<ScheduledTask> getScheduledTasks();方法嘛
synchronized (this.scheduledTasks) {
Set<ScheduledTask> registeredTasks = this.scheduledTasks.get(bean);
if (registeredTasks == null) {
registeredTasks = new LinkedHashSet<>(4);
this.scheduledTasks.put(bean, registeredTasks);
}
registeredTasks.addAll(tasks);
}
}
catch (IllegalArgumentException ex) {
throw new IllegalStateException(
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
}
}

private static long parseDelayAsLong(String value) throws RuntimeException {
if (value.length() > 1 && (isP(value.charAt(0)) || isP(value.charAt(1)))) {
return Duration.parse(value).toMillis();
}
return Long.parseLong(value);
}

private static boolean isP(char ch) {
return (ch == 'P' || ch == 'p');
}

//@since 5.0.2 获取到所有的任务。包含本实例的,以及registrar(手动注册)的所有任务
@Override
public Set<ScheduledTask> getScheduledTasks() {
Set<ScheduledTask> result = new LinkedHashSet<>();
synchronized (this.scheduledTasks) {
Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values();
for (Set<ScheduledTask> tasks : allTasks) {
result.addAll(tasks);
}
}
result.addAll(this.registrar.getScheduledTasks());
return result;
}

// Bean销毁之前执行。移除掉所有的任务,并且取消所有的任务
@Override
public void postProcessBeforeDestruction(Object bean, String beanName) {
Set<ScheduledTask> tasks;
synchronized (this.scheduledTasks) {
tasks = this.scheduledTasks.remove(bean);
}
if (tasks != null) {
for (ScheduledTask task : tasks) {
task.cancel();
}
}
}
...
}

 

 

 

 

 

 

 

 

 

这里已经把Spring怎么发现Task、执行Task的流程讲解通了。还有一个重要的类:ScheduledTaskRegistrar,它整体作为一个注册中心的角色,非常的重要 。

ScheduledTaskRegistrar

ScheduledTask注册中心,ScheduledTaskHolder接口的一个重要的实现类,维护了程序中所有配置的ScheduledTask。

//@since 3.0 它在Spring3.0就有了
// 这里面又有一个重要的接口:我们可以通过扩展实现此接口,来定制化属于自己的ScheduledTaskRegistrar 下文会有详细介绍
// 它实现了InitializingBean和DisposableBean,所以我们也可以把它放进容器里面
public class ScheduledTaskRegistrar implements ScheduledTaskHolder, InitializingBean, DisposableBean {

// 任务调度器
@Nullable
private TaskScheduler taskScheduler;
// 该类是JUC包中的类
@Nullable
private ScheduledExecutorService localExecutor;

// 对任务进行分类 管理
@Nullable
private List<TriggerTask> triggerTasks;
@Nullable
private List<CronTask> cronTasks;
@Nullable
private List<IntervalTask> fixedRateTasks;
@Nullable
private List<IntervalTask> fixedDelayTasks;
private final Map<Task, ScheduledTask> unresolvedTasks = new HashMap<>(16);
private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<>(16);

// 调用者可以自己指定一个TaskScheduler
public void setTaskScheduler(TaskScheduler taskScheduler) {
Assert.notNull(taskScheduler, "TaskScheduler must not be null");
this.taskScheduler = taskScheduler;
}

// 这里,如果你指定的是个TaskScheduler、ScheduledExecutorService都是可以的
// ConcurrentTaskScheduler也是一个TaskScheduler的实现类
public void setScheduler(@Nullable Object scheduler) {
if (scheduler == null) {
this.taskScheduler = null;
}
else if (scheduler instanceof TaskScheduler) {
this.taskScheduler = (TaskScheduler) scheduler;
}
else if (scheduler instanceof ScheduledExecutorService) {
this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler));
}
else {
throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
}
}

@Nullable
public TaskScheduler getScheduler() {
return this.taskScheduler;
}
// 将触发的任务指定为可运行文件(任务)和触发器对象的映射 typically custom implementations of the {@link Trigger} interface
// org.springframework.scheduling.Trigger
public void setTriggerTasks(Map<Runnable, Trigger> triggerTasks) {
this.triggerTasks = new ArrayList<>();
triggerTasks.forEach((task, trigger) -> addTriggerTask(new TriggerTask(task, trigger)));
}

// 主要处理` <task:*>`这种配置
public void setTriggerTasksList(List<TriggerTask> triggerTasks) {
this.triggerTasks = triggerTasks;
}
public List<TriggerTask> getTriggerTaskList() {
return (this.triggerTasks != null? Collections.unmodifiableList(this.triggerTasks) :
Collections.emptyList());
}

// 这个一般是最常用的 CronTrigger:Trigger的一个实现类。另一个实现类为PeriodicTrigger
public void setCronTasks(Map<Runnable, String> cronTasks) {
this.cronTasks = new ArrayList<>();
cronTasks.forEach(this::addCronTask);
}
public void setCronTasksList(List<CronTask> cronTasks) {
this.cronTasks = cronTasks;
}
public List<CronTask> getCronTaskList() {
return (this.cronTasks != null ? Collections.unmodifiableList(this.cronTasks) :
Collections.emptyList());
}
...
// 判断是否还有任务
public boolean hasTasks() {
return (!CollectionUtils.isEmpty(this.triggerTasks) ||
!CollectionUtils.isEmpty(this.cronTasks) ||
!CollectionUtils.isEmpty(this.fixedRateTasks) ||
!CollectionUtils.isEmpty(this.fixedDelayTasks));
}


/**
* Calls {@link #scheduleTasks()} at bean construction time.
*/
// 这个方法很重要:开始执行所有已经注册的任务们~~
@Override
public void afterPropertiesSet() {
scheduleTasks();
}
@SuppressWarnings("deprecation")
protected void scheduleTasks() {
// 这一步非常重要:如果我们没有指定taskScheduler ,这里面会new一个newSingleThreadScheduledExecutor
// 显然它并不是是一个真的线程池,所以他所有的任务还是得一个一个的One by One的执行的 请务必注意啊~~~~
// 默认是它:Executors.newSingleThreadScheduledExecutor() 所以肯定串行啊
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}


// 加下来做得事,就是借助TaskScheduler来启动每个任务
// 并且把启动了的任务最终保存到scheduledTasks里面~~~ 后面还会介绍TaskScheduler的两个实现
if (this.triggerTasks != null) {
for (TriggerTask task : this.triggerTasks) {
addScheduledTask(scheduleTriggerTask(task));
}
}
if (this.cronTasks != null) {
for (CronTask task : this.cronTasks) {
addScheduledTask(scheduleCronTask(task));
}
}
if (this.fixedRateTasks != null) {
for (IntervalTask task : this.fixedRateTasks) {
addScheduledTask(scheduleFixedRateTask(task));
}
}
if (this.fixedDelayTasks != null) {
for (IntervalTask task : this.fixedDelayTasks) {
addScheduledTask(scheduleFixedDelayTask(task));
}
}
}

private void addScheduledTask(@Nullable ScheduledTask task) {
if (task != null) {
this.scheduledTasks.add(task);
}
}
@Override
public Set<ScheduledTask> getScheduledTasks() {
return Collections.unmodifiableSet(this.scheduledTasks);
}

// 销毁: task.cancel()取消所有的任务 调用的是Future.cancel()
// 并且关闭线程池localExecutor.shutdownNow()
@Override
public void destroy() {
for (ScheduledTask task : this.scheduledTasks) {
task.cancel();
}
if (this.localExecutor != null) {
this.localExecutor.shutdownNow();
}
}

}
文章
————————————————
版权声明:本文为CSDN博主「Coder_Boy_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Coder_Boy_/article/details/110676333

 

标签:null,Spring,private,task,bean,源码,new,任务调度,public
From: https://www.cnblogs.com/softidea/p/17914374.html

相关文章

  • spring boot 配置get方法枚举转换策略
    配置转换器@SuppressWarnings({"rawtypes","unchecked"})publicclassCompositeEnumConverterFactoryimplementsConverterFactory<String,Enum<?>>{ @Override public<TextendsEnum<?>>Converter<String,T>getC......
  • Spring Boot学习随笔- 实现AOP(JoinPoint、ProceedingJoinPoint、自定义注解类实现切面
    学习视频:【编程不良人】2021年SpringBoot最新最全教程第十一章、AOP11.1为什么要使用AOP问题现有业务层开发存在问题额外功能代码存在大量冗余每个方法都需要书写一遍额外功能代码不利于项目维护Spring中的AOPAOP:Aspect切面+Oriented面向Programmaing......
  • SpringBoot中使用Aspect实现切面
    相关概念切面(Aspect):首先要理解‘切’字,需要把对象想象成一个立方体,传统的面向对象变成思维,类定义完成之后(封装)。每次实例化一个对象,对类定义中的成员变量赋值,就相当于对这个立方体进行了一个定义,定义完成之后,就等着被使用,等着被回收。面向切面编程则是指,对于一个我们已经封装......
  • 构建可扩展的网校平台:在线教育系统源码设计与架构最佳实践
    随着科技的不断发展,在线教育系统在教育领域扮演着越来越重要的角色。本文将深入探讨如何构建一个可扩展的网校平台,重点关注在线教育系统的源码设计和架构最佳实践。 一、引言在当前信息时代,教育已经超越了传统的教学方式,转向更加灵活和便捷的在线教育平台。构建一个可扩展的网校平......
  • 最近在使用SpringBoot整合MyBatis-Plus时出现的问题
    版本信息:IDEA2022、jdk17、maven3.8.6、SpringBoot3+MyBatis-Plus依赖版本信息:<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3</version&g......
  • springboot 记录使用log4j与logback发送日志到syslog服务器
    Linux服务器配置编辑/etc/rsyslog.conf文件。(我的在这里,根据自己系统查看)#取消注释这几行$ModLoadimudp$UDPServerRun514#ProvidesTCPsyslogreception$ModLoadimtcp$InputTCPServerRun514..........#末位追加:local2.info/var/log/login_info.log......
  • SpringBoot读取resources下的文件以及resources的资源路径
    1.这种可以但是在容器中获取不到(以下几种都可以只要不在容器)。InputStreaminputStream=this.getClass().getResourceAsStream("/static/imgs/aha.png");Propertiespps=newProperties();Filefile=ResourceUtils.getFile("classpath:defult.properties");pps.loa......
  • SpringBoot启动热部署
    1.在pom.xml中添加依赖<!--devtools热部署--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</opt......
  • 【转载】Springboot2.x 使用 Redis
    参考https://blog.csdn.net/weixin_43749805/article/details/131399516https://juejin.cn/post/7076244567569203208https://blog.csdn.net/oJingZhiYuan12/article/details/126386904注意classjava.lang.Integercannotbecasttoclasscom.xiaqiuchu.demo.entity.S......
  • 2023最新中级难度Spring Cloud面试题,包含答案。刷题必备!记录一下。
    好记性不如烂笔头内容来自[面试宝典-中级难度SpringCloud面试题合集](https://offer.houxu6.top/tag/SpringCloud)问:SpringCloud是什么?SpringCloud是一个微服务框架,它提供了一系列分布式系统解决方案。它利用了SpringBoot的开发便利性巧妙地简化了分布式系统基础设......