首页 > 其他分享 >【SpringBoot】服务对注册中心的注册时机

【SpringBoot】服务对注册中心的注册时机

时间:2024-05-25 20:11:55浏览次数:11  
标签:clientConfig DiscoveryClient SpringBoot 注册 registration new 时机 logger

1  前言

我们看过很多的时机,比如服务数据源的关闭时机服务正式开始处理请求的时机或者Tomcat和SpringBoot的协同、还有 mybatis等一些插件的入场时机等,这节我们要再看一个时机,就是关于跟注册中心(Eureka、Nacos)的时机,比如你有没有思考过:

我服务还没起来,你就到注册中心上线了,那请求过来岂不是就乱了,或者我服务要停止了我数据源都释放了,你还没从注册中心下线,那请求还过来是不是也会乱,所以我们就要看看微服务里上线或者叫注册和下线的时机都是什么时候,做到心中有数。本节我们先看注册时机。

环境的话,我本地有 Eureka,本节我们就拿 Eureka 注册中心调试看看。

服务的话,我有几个简单的微服务,都是平时用来调试的哈

2  注册时机

先来看个效果,当我的 demo 服务起来后:

问题来了,我怎么知道它的注册时机,从哪看呢?我从官网的文档里看了看:中文官网英文官网,发现它只是从使用方式上介绍了怎么使用以及使用上的细节,并没说原理。

那怎么看呢?那就从服务的日志以及代码的依赖看起,当你看的源码多了,大多融合 SpringBoot 的方式都差不多。

找到注册的 Bean了没?就是他:EurekaAutoServiceRegistration 

@Bean
@ConditionalOnBean({AutoServiceRegistrationProperties.class})
@ConditionalOnProperty(
    value = {"spring.cloud.service-registry.auto-registration.enabled"},
    matchIfMissing = true
)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationContext context, EurekaServiceRegistry registry, EurekaRegistration registration) {
    return new EurekaAutoServiceRegistration(context, registry, registration);
}

那我们就从 EurekaAutoServiceRegistration 看起,先看看它的类关系:

正如我图上所联系的,这个类有两个动作来驱动他执行,(1)事件监听(2)生命周期接口

(1)针对事件监听,它主要监听了 WebServerInitializedEvent(Web容器初始化完毕的事件) 和 ContextClosedEvent(上下文关闭事件也就是服务停止的事件) 两个事件

public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof WebServerInitializedEvent) {
        this.onApplicationEvent((WebServerInitializedEvent)event);
    } else if (event instanceof ContextClosedEvent) {
        this.onApplicationEvent((ContextClosedEvent)event);
    }
}

(2)SmartLifecycle 生命周期接口,有两个重要的动作就是启动和停止

我们这里关注的是服务注册,也就是关注事件的监听里的 WebServerInitializedEvent 事件和生命周期的 start 方法。

那哪个先执行的呢?是生命周期的 start 先执行。我这里画个图简单回忆下:

看图哈,都是在刷新上下文的最后有个 finishRefresh 即结束上下文中的动作。具体可以看我之前的文章哈,就不阐述了。那我们就先看看 EurekaAutoServiceRegistration 的 start 方法:

// EurekaAutoServiceRegistration 的 start 方法
// private AtomicInteger port = new AtomicInteger(0);
public void start() {
    // 这个时候进来 port 还是0
    // 这里提前告诉你 他是通过监听事件 当监听到我们的 web容器启动完毕后,接收到监听拉更改端口的 此时还没启动web容器 所以这里还是0
    if (this.port.get() != 0) {
        if (this.registration.getNonSecurePort() == 0) {
            this.registration.setNonSecurePort(this.port.get());
        }
        if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
            this.registration.setSecurePort(this.port.get());
        }
    }
    // 这里的 nonSecurePort 默认就是我们服务的端口号
    // 那么当刷新上下文的时候,这里会执行
    if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
        this.serviceRegistry.register(this.registration);
        this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));
        this.running.set(true);
    }
}

this.registration.getNonSecurePort()的来源:

this.registration.getNonSecurePort(),这个端口号是来自 registration 我们这里是 Eureka 即 EurekaRegistration,而它又来自于:CloudEurekaInstanceConfig instanceConfig,

那我们看到第一次到 start 会执行这段代码:

// 这里的 nonSecurePort 默认就是我们服务的端口号
// 那么当刷新上下文的时候,这里会执行
if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
    this.serviceRegistry.register(this.registration);
    this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));
    this.running.set(true);
}

那么看到这里,我们得先了解下 serviceRegistry(EurekaServiceRegistry)和 registration(EurekaRegistration)的来源,都是在自动装配类里 EurekaClientAutoConfiguration:

EurekaServiceRegistry 的来源比较简单:

EurekaRegistration的来源:

可以看到 EurekaRegistration 有很多的依赖:EurekaClient、CloudEurekaInstanceConfig、ApplicationInfoManager、HealthCheckHandler。

那我们看看这四个怎么来的,先看 EurekaClient:

// 见 EurekaClientAutoConfiguration
@Bean(
    destroyMethod = "shutdown"
)
@ConditionalOnMissingBean(
    value = {EurekaClient.class},
    search = SearchStrategy.CURRENT
)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config, EurekaInstanceConfig instance, @Autowired(required = false) HealthCheckHandler healthCheckHandler) {
    ApplicationInfoManager appManager;
    // 拿到原始对象
    if (AopUtils.isAopProxy(manager)) {
        appManager = (ApplicationInfoManager)ProxyUtils.getTargetObject(manager);
    } else {
        appManager = manager;
    }
    // 创建一个 client 对象
    CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);
    // 注册健康检查
    cloudEurekaClient.registerHealthCheck(healthCheckHandler);
    return cloudEurekaClient;
}

可以看到它也依赖很多: EurekaInstanceConfig、ApplicationInfoManager、HealthCheckHandler、EurekaClientConfig ,它跟 EurekaRegistration 的依赖差不多。

EurekaClientConfig 是 eureka.client 开头的配置 Bean:

EurekaInstanceConfig 是 eureka.instance 开头的配置 Bean:

HealthCheckHandler 健康检查是来源于 EurekaDiscoveryClientConfiguration 自动装配,当你开启了 eureka.client.healthcheck.enabled 的配置(默认=false 不开启)就会注册一个健康检查的 Bean:

ApplicationInfoManager 是对当前服务信息的一个管理 Bean,来源于 EurekaClientAutoConfiguration 自动装配类:

@Bean
@ConditionalOnMissingBean(
    value = {ApplicationInfoManager.class},
    search = SearchStrategy.CURRENT
)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {
    // EurekaInstanceConfig eureka.instance 开头的配置Bean
    // InstanceInfoFactory 工厂来创建 InstanceInfo 也就是针对当前服务的信息封装到 InstanceInfo里
    InstanceInfo instanceInfo = (new InstanceInfoFactory()).create(config);
    // 实例化
    return new ApplicationInfoManager(config, instanceInfo);
}

了解完这四个依赖,我们继续看 EurekaClient 的创建:

// 创建一个 client 对象
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);

CloudEurekaClient 最后会走到 DiscoveryClient 的构造器:

DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
    // 省略...
    logger.info("Initializing Eureka in region {}", this.clientConfig.getRegion());
    // 不开启注册或者服务发现的话走这里  这个不看 我们重点看 else 的逻辑
    if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
        logger.info("Client configured to neither register nor query for data.");
        this.scheduler = null;
        this.heartbeatExecutor = null;
        this.cacheRefreshExecutor = null;
        this.eurekaTransport = null;
        this.instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), this.clientConfig.getRegion());
        DiscoveryManager.getInstance().setDiscoveryClient(this);
        DiscoveryManager.getInstance().setEurekaClientConfig(config);
        this.initTimestampMs = System.currentTimeMillis();
        this.initRegistrySize = this.getApplications().size();
        this.registrySize = this.initRegistrySize;
        logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", this.initTimestampMs, this.initRegistrySize);
    } else {
        try {
            // 调度线程池 
            this.scheduler = Executors.newScheduledThreadPool(2, (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-%d").setDaemon(true).build());
            // 健康检查的线程池 
            this.heartbeatExecutor = new ThreadPoolExecutor(1, this.clientConfig.getHeartbeatExecutorThreadPoolSize(), 0L, TimeUnit.SECONDS, new SynchronousQueue(), (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-HeartbeatExecutor-%d").setDaemon(true).build());
            this.cacheRefreshExecutor = new ThreadPoolExecutor(1, this.clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0L, TimeUnit.SECONDS, new SynchronousQueue(), (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d").setDaemon(true).build());
            this.eurekaTransport = new DiscoveryClient.EurekaTransport();
            this.scheduleServerEndpointTask(this.eurekaTransport, args);
            Object azToRegionMapper;
            if (this.clientConfig.shouldUseDnsForFetchingServiceUrls()) {
                azToRegionMapper = new DNSBasedAzToRegionMapper(this.clientConfig);
            } else {
                azToRegionMapper = new PropertyBasedAzToRegionMapper(this.clientConfig);
            }

            if (null != this.remoteRegionsToFetch.get()) {
                ((AzToRegionMapper)azToRegionMapper).setRegionsToFetch(((String)this.remoteRegionsToFetch.get()).split(","));
            }

            this.instanceRegionChecker = new InstanceRegionChecker((AzToRegionMapper)azToRegionMapper, this.clientConfig.getRegion());
        } catch (Throwable var12) {
            throw new RuntimeException("Failed to initialize DiscoveryClient!", var12);
        }
        
        // 服务注册并且强制在初始化的时候就注册 默认是false 也就是不会在初始化的时候注册
        if (this.clientConfig.shouldRegisterWithEureka() && this.clientConfig.shouldEnforceRegistrationAtInit()) {
            try {
                // 调用注册
                if (!this.register()) {
                    throw new IllegalStateException("Registration error at startup. Invalid server response.");
                }
            } catch (Throwable var10) {
                logger.error("Registration error at startup: {}", var10.getMessage());
                throw new IllegalStateException(var10);
            }
        }
        // 初始化线程任务 
        this.initScheduledTasks();

        try {
            Monitors.registerObject(this);
        } catch (Throwable var9) {
            logger.warn("Cannot register timers", var9);
        }

        DiscoveryManager.getInstance().setDiscoveryClient(this);
        DiscoveryManager.getInstance().setEurekaClientConfig(config);
        this.initTimestampMs = System.currentTimeMillis();
        this.initRegistrySize = this.getApplications().size();
        this.registrySize = this.initRegistrySize;
        logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", this.initTimestampMs, this.initRegistrySize);
    }
}

在这个构造器里,我调试发现它默认是不在初始化进行强制注册到 Eureka的,并且创建了几个线程池,那我们继续看看初始化线程任务里都要做什么事情:

// TimedSupervisorTask 它这个看似是一个体系用于做线程任务的  我们本节暂时不看它原理
private void initScheduledTasks() {
    int renewalIntervalInSecs;
    int expBackOffBound;
    // 默认是开启的
    if (this.clientConfig.shouldFetchRegistry()) {
        // 间隔时间 默认30秒
        renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
        expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        // cacheRefresh 续租任务 类似告诉服务器我还活着 别把我下掉
        this.cacheRefreshTask = new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread());
        this.scheduler.schedule(this.cacheRefreshTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);
    }
    // 默认开启的 注册到 eureka 这个是我们本节关注的
    if (this.clientConfig.shouldRegisterWithEureka()) {
        renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);
        // heartbeat 健康检查的
        this.heartbeatTask = new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread());
        this.scheduler.schedule(this.heartbeatTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);
        // InstanceInfoReplicator 实现了 Runnable
        this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
        this.statusChangeListener = new StatusChangeListener() {
            public String getId() {
                return "statusChangeListener";
            }
            
            public void notify(StatusChangeEvent statusChangeEvent) {
                DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
                DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
            }
        };
        // 默认开启 往 applicationInfoManager 注册了一个监听
        if (this.clientConfig.shouldOnDemandUpdateStatusChange()) {
            this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
        }
        // 启动 我们的服务注册就在这里
        this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

关于 EurekaClient 的构造我们就暂时看到这里,我们继续看一个 InstanceInfoReplicator,看看这个任务干了些什么:

class InstanceInfoReplicator implements Runnable {
    ...
    // 构造器
    InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {
        this.discoveryClient = discoveryClient;
        this.instanceInfo = instanceInfo;
        // 初始化了一个线程池
        this.scheduler = Executors.newScheduledThreadPool(1, (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d").setDaemon(true).build());
        this.scheduledPeriodicRef = new AtomicReference();
        this.started = new AtomicBoolean(false);
        this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);
        this.replicationIntervalSeconds = replicationIntervalSeconds;
        this.burstSize = burstSize;
        this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;
        logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", this.allowedRatePerMinute);
    }

    // start 方法就是往线程池里提交了一个任务 任务的内容就是自己的 run 方法
    public void start(int initialDelayMs) {
        if (this.started.compareAndSet(false, true)) {
            this.instanceInfo.setIsDirty();
            Future next = this.scheduler.schedule(this, (long)initialDelayMs, TimeUnit.SECONDS);
            this.scheduledPeriodicRef.set(next);
        }

    }
    // this.instanceInfo.setIsDirty() 方法内容
    // public synchronized void setIsDirty() {
    //     this.isInstanceInfoDirty = true;
    //     this.lastDirtyTimestamp = System.currentTimeMillis();
    // }
    
    // ...
    public void run() {
        boolean var6 = false;

        ScheduledFuture next;
        label53: {
            try {
                var6 = true;
                this.discoveryClient.refreshInstanceInfo();
                Long dirtyTimestamp = this.instanceInfo.isDirtyWithTime();
                // 不为空 执行注册
                if (dirtyTimestamp != null) {
                    this.discoveryClient.register();
                    this.instanceInfo.unsetIsDirty(dirtyTimestamp);
                    var6 = false;
                } else {
                    var6 = false;
                }
                break label53;
            } catch (Throwable var7) {
                logger.warn("There was a problem with the instance info replicator", var7);
                var6 = false;
            } finally {
                if (var6) {
                    ScheduledFuture next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
                    this.scheduledPeriodicRef.set(next);
                }
            }

            next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
            this.scheduledPeriodicRef.set(next);
            return;
        }

        next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
        this.scheduledPeriodicRef.set(next);
    }
}

哎哟,这个注册,第一次的落点在 EurekaClient 的构造器里,通过启动 InstanceInfoReplicator 来进行注册的。

但是有个重要的信息,就是它都是带 @Lazy 标记的,也就是不会在刷新上下文的最后阶段主动初始化这些类,而是在第一次使用的时候才进行实例化的。

那么回到我们本节的主题,它的注册时机在哪里?或者第一次调用这就要回到我们最初的EurekaAutoServiceRegistration 生命周期 start 方法里,就是我下边红色加粗的这里:

// EurekaAutoServiceRegistration 的 start 方法
// private AtomicInteger port = new AtomicInteger(0);
public void start() {
    // 这个时候进来 port 还是0
    // 这里提前告诉你 他是通过监听事件 当监听到我们的 web容器启动完毕后,接收到监听拉更改端口的 此时还没启动web容器 所以这里还是0
    if (this.port.get() != 0) {
        if (this.registration.getNonSecurePort() == 0) {
            this.registration.setNonSecurePort(this.port.get());
        }
        if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
            this.registration.setSecurePort(this.port.get());
        }
    }
    // 这里的 nonSecurePort 默认就是我们服务的端口号
    // 那么当刷新上下文的时候,这里会执行    也就是第一次在这里的执行
    if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
        this.serviceRegistry.register(this.registration);
        this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));
        this.running.set(true);
    }
}

走到这里:this.serviceRegistry.register(this.registration);

// 服务注册
public void register(EurekaRegistration reg) {
    // 看人家这个取名  maybe 或许 初始化 client 
    this.maybeInitializeClient(reg);
    if (log.isInfoEnabled()) {
        log.info("Registering application " + reg.getApplicationInfoManager().getInfo().getAppName() + " with eureka with status " + reg.getInstanceConfig().getInitialStatus());
    }
    reg.getApplicationInfoManager().setInstanceStatus(reg.getInstanceConfig().getInitialStatus());
    reg.getHealthCheckHandler().ifAvailable((healthCheckHandler) -> {
        reg.getEurekaClient().registerHealthCheck(healthCheckHandler);
    });
}
// getApplicationInfoManager 是不是就会开始创建 我们的 ApplicationInfoManager 它由依赖于 EurekaClient
// 是不是就都创建起来了
private void maybeInitializeClient(EurekaRegistration reg) {
    reg.getApplicationInfoManager().getInfo();
    reg.getEurekaClient().getApplications();
}

好啦,至此到这里,创建 EurekaClient 的时候,进行服务注册的。

最后再看看注册方法,哎哟,这里就是第一次的注册:

boolean register() throws Throwable {
    logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);
    EurekaHttpResponse httpResponse;
    try {
        // 服务注册
        httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);
    } catch (Exception var3) {
        logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3});
        throw var3;
    }
    if (logger.isInfoEnabled()) {
        logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

跟我们的日志也能对上:

所以服务第一次注册的时机,默认的情况下是在 刷新上下文的 finishRefresh 里调用 Bean 生命周期的 start,通过 serviceRegistry.registry 方法来第一次加载 EurekaClient 相关的 Bean,在 EurekaClient 的构造器里通过 InstanceInfoReplicator 来进行服务的注册。

3  小结

好啦,本节我们就暂时看到这里,下节我们再看下线时机,有理解不对的地方欢迎指正哈。

标签:clientConfig,DiscoveryClient,SpringBoot,注册,registration,new,时机,logger
From: https://www.cnblogs.com/kukuxjx/p/18212295

相关文章

  • springboot3.0+shardingsphere5.2 最简单的分库分表
    先看表结构两个数据库test1,test2每个库有4张sys_user表分别命名sys_user_0-4maven依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>......
  • SpringBoot自动装配原理是什么?
    1创建上下文publicConfigurableApplicationContextrun(String...args){//记录程序运行时间StopWatchstopWatch=newStopWatch();stopWatch.start();//ConfigurableApplicationContextSpring的上下文ConfigurableAppl......
  • Springboot计算机毕业设计信息学院网络工程党支部小程序【附源码】开题+论文+mysql+程
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景在信息化高速发展的今天,党建工作也需要与时俱进,利用信息技术手段提升工作效率和影响力。信息学院网络工程党支部作为学院党建工作的前沿阵地,面临着如......
  • 【精品毕设推荐】基于Springboot的学生心理咨询评估系统的设计与实现
    点击下载原文及代码,可辅助在本地配置运行摘 要使用旧方法对学生心理咨询评估信息进行系统化管理已经不再让人们信赖了,把现在的网络信息技术运用在学生心理咨询评估信息的管理上面可以解决许多信息管理上面的难题,比如处理数据时间很长,数据存在错误不能及时纠正等问题。这次......
  • JAVA计算机毕业设计基于SpringBoot的在线古玩市场系统的设计与实现(附源码+springboot+
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着互联网的迅猛发展和电子商务的普及,传统行业纷纷寻求数字化转型以适应市场的新需求。古玩市场作为一个历史悠久、文化底蕴深厚的行业,在数字化浪潮......
  • 【人民卫生音像-注册安全分析报告】
    前言由于网站注册入口容易被黑客攻击,存在如下安全问题:暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞所以大部分网站及App都采取图形验证码或滑动验证码等交互解决方案,但在机器学习能力提......
  • 基于springboot+vue的招聘信息管理系统
    开发语言:Java框架:springbootJDK版本:JDK1.8服务器:tomcat7数据库:mysql5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:Maven3.3.9系统展示系统首页企业招聘界面求职信息界面社区留言界面个人中心管理员登录管理员功能界面用户管理......
  • 基于SpringBoot的球队训练信息管理系统
    一、系统介绍球队训练信息管理系统:可以方便管理人员对球队训练信息管理系统的管理,提高信息管理工作效率及查询效率,有利于更好的为用户提供服务。主要的模块包括:1、后台功能:管理员角色:首页、个人中心、基础数据管理、公告类型管理、球队类型管理、教练管理、加入的球队......
  • Shell编程完成用户注册登录
    目录需求1.menu界面welcome(1)注册用户(2)用户登录(3)退出2.注册用户(1)用户名(首字母大写)(2)密码(8位及其以上必须有三个字符其中一个以上如:&$_隐试密码)(3)手机号码(必须以139开头)(4)邮箱邮箱名数字开头@qq.com(5)检测是否重名,重手机号和邮箱(6)不重名、手机号、邮箱写入......
  • 基于Springboot的在线英语阅读分级平台(有报告)。Javaee项目,springboot项目。
    演示视频:基于Springboot的在线英语阅读分级平台(有报告)。Javaee项目,springboot项目。项目介绍:采用M(model)V(view)C(controller)三层体系结构,通过Spring+SpringBoot+Mybatis+Vue+Maven+Layui+Elementui来实现。MySQL数据库作为系统数据储存平台,实现了基于B/S结构的Web......