您现在的位置是:首页 >技术杂谈 >spring 设计分布式组件之网关网站首页技术杂谈
spring 设计分布式组件之网关
网关在分布式系统中充当的角色与其作用
1. 负载均衡器:网关可以作为负载均衡器,将来自客户端的请求分发到多个后端服务实例中,以提高系统的可用性和扩展性。
2. 安全代理:网关可以通过认证、鉴权、加密等机制,保障系统的安全性和隐私性,并防止恶意攻击和非法访问。
3. 协议转换器:网关可以将不同协议之间的请求和响应进行转换,例如RESTful API与SOAP协议之间的转换。
4. 缓存代理:网关可以缓存后端服务返回的数据,以提高请求响应速度和减轻后端服务的负担。
5. API管理器:网关可以对外部API进行管理和监控,例如限流、流量控制、日志记录等操作,从而保证系统的稳定性和可靠性。
6. 数据聚合器:网关可以将多个后端服务的数据进行聚合和汇总,形成新的业务逻辑或者数据源,以支持更复杂的系统需求。
7. 服务发现代理:网关可以通过与服务注册中心的配合,实现服务发现和路由功能,使得客户端无需知道具体的服务实例地址,而只需通过网关进行访问。
网关实例nignx与gateway的优缺点与区别
1. 性能:Nginx是一款高性能的Web服务器和反向代理软件,具有出色的并发处理能力和低延迟。而Spring Cloud Gateway则基于Spring框架和Netty实现,相对于Nginx来说性能较低。
2. 功能扩展:Nginx提供了丰富的模块和插件,可以方便地扩展和定制各种功能和特性,例如负载均衡、缓存、安全认证等。而Spring Cloud Gateway虽然也提供了类似的功能,但拥有的扩展模块和插件相对较少。
3. 配置和管理:Nginx的配置文件比较简单明了,易于管理和维护,同时支持热部署和动态调整。Spring Cloud Gateway则采用Java DSL或者YAML进行配置,相对较为复杂。
4. 生态系统:Nginx已经成为了业界标准的Web服务器和反向代理软件,拥有庞大的生态系统和丰富的社区支持,有很多开源项目和第三方工具与之集成。而Spring Cloud Gateway作为一个比较新的项目,其生态系统还不够完善。
5. 场景适用:Nginx主要面向静态内容的Web服务器和反向代理场景,可以快速处理高并发请求和负载均衡。而Spring Cloud Gateway则更适用于微服务架构中的API网关场景,可以支持动态路由、请求过滤、数据转换等功能。
本文设计的分布式网关实例特点与不足
1.采用yaml文件配置远程服务,尚未设计web配置。
2.使用head请求探测远程服务在线状态,服务实例高时额外损耗高。
3.选取spring6中的http服务代理工厂发送远程调用请求。
4.利用多线程监听服务心跳,离线重连。
5.资源池化管理,节省资源。
前置条件:了解spring6的新特性,多线程,池化,设计模式。
源码示例与释义(认为你已经将http://t.csdn.cn/yL3O1这篇文章的demo实现了)
这里我们以http://t.csdn.cn/yL3O1这篇文章的demo为基础进行设计。
yml配置文件:
server:
port: 9001
spring:
application:
name: cloud_serviceList
originCenter:
size: 10
serviceList:
- { key: air_check, url: http://127.0.0.1:9000 }
httpClient:
maxTotal: 100
maxPerRoute: 20
validateAfterInactivity: 5000
requestConfig:
connectTimeOut: 5000
socketTimeOut: 5000
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
metrics:
tags:
application: cloud_serviceList
这里的originCenter与httpClient配置项是我们自定义的配置信息,暂时略过,后续使用会解释其用意。值得一提的是management配置项是springboot提供的actuator端点健康检测组件。它可以配置到我们的实例端口某些uri上去,例如/actuator/thread:显示应用程序的线程信息,用于显示服务端点的运行情况。运行实例时类似如下json信息:
{"name":"jvm.threads.live","description":"The current number of live threads including both daemon and non-daemon threads","baseUnit":"threads","measurements":[{"statistic":"VALUE","value":27.0}],"availableTags":[{"tag":"application","values":["cloud_serviceList"]}]}
这段JSON表示了一个名为"jvm.threads.live"的指标,它描述了当前Java虚拟机中所有线程(包括守护线程和非守护线程)的数量。该指标的计量单位为"线程",当前的值为27.0。此外,该指标还有一个可用标签"application",其值为"cloud_serviceList",用于进一步标识和分类该指标。
ServiceModel(承载yml扫描出来的信息基类)
@Data
@EqualsAndHashCode
public class ServiceModel {
private String key;
private String url;
}
OriginCenterProperties(扫描yml信息存储组件)
@Component("OriginCenterProperties")
@ConfigurationProperties(prefix = "origin-center")
public class OriginCenterProperties {
private int size;
private List<ServiceModel> serviceList;
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public List<ServiceModel> getServiceList() {
return serviceList;
}
public void setServiceList(List<ServiceModel> serviceList) {
this.serviceList = serviceList;
}
}
OriginCenter远程服务注册与发现中心
@Configuration
@DependsOn({"OriginCenterProperties"})
@Slf4j
public class OriginCenter implements DisposableBean, InitializingBean {
transient volatile Map<String, HttpServiceProxyFactory> httpServiceProxyFactoryMap;
@Qualifier("OriginCenterProperties")
@Autowired
private OriginCenterProperties originCenterProperties;
@Bean(value = "HttpServiceProxyFactoryMap")
@Scope(value = "singleton")
public Map<String, HttpServiceProxyFactory> getHttpServiceProxyFactoryMap() {
if (httpServiceProxyFactoryMap == null) {
synchronized (this) {
if (httpServiceProxyFactoryMap == null) {
httpServiceProxyFactoryMap = new HashMap<>(originCenterProperties.getSize());
if (!CollectionUtils.isEmpty(originCenterProperties.getServiceList())) {
log.info("解析配置远程服务信息,生成远程服务代理工厂列表");
originCenterProperties.getServiceList().forEach(serviceModel -> {
httpServiceProxyFactoryMap.put(serviceModel.getKey(),
HttpServiceProxyFactory.builder(WebClientAdapter.forClient(WebClient.builder()
.baseUrl(serviceModel.getUrl()).build())).build());
});
log.info("解析成功,远程服务代理列表已就绪");
}
}
}
}
return httpServiceProxyFactoryMap;
}
@Override
public void afterPropertiesSet() {
log.info("初始化远程服务中心");
}
@Override
public void destroy() {
if (!CollectionUtils.isEmpty(httpServiceProxyFactoryMap)){
log.info("销毁服务代理工厂列表");
httpServiceProxyFactoryMap.clear();
}
}
}
其中用到了DependsOn注解、 DisposableBean、InitializingBean接口、transient、volatile、synchronized关键字、双检锁单例。
DependsOn注解使用时需要注意:
-
DependsOn注解只适用于单例Bean,对于原型Bean或其他范围的Bean没有任何作用。
-
DependsOn注解所指定的Bean名称必须是已存在于容器中的Bean,否则会抛出异常。
-
如果多个Bean之间存在循环依赖关系,在使用DependsOn注解时需要非常小心,否则可能会导致死锁或其他问题。
-
在一般情况下,不建议使用DependsOn注解,而是应该采用更加标准和规范的依赖注入方式,例如构造函数、属性注入等。
DisposableBean与InitializingBean 接口作用与注意点:
DisposableBean和InitializingBean是两个核心接口,它们分别用于在Bean初始化和销毁时执行特定的操作。
1. InitializingBean接口:该接口中定义了一个方法afterPropertiesSet(),当Bean的所有属性设置完成之后,Spring容器会自动调用该方法。开发者可以在该方法中执行Bean的初始化操作,例如数据源连接、权限校验、缓存预热等。
如果一个Bean实现了InitializingBean接口,那么其初始化逻辑必须放在afterPropertiesSet()方法中执行。InitializingBean接口与@PostConstruct注解功能类似,但前者更加规范和标准化。
2. DisposableBean接口:该接口中定义了一个方法destroy(),当Bean即将被Spring容器销毁时,容器会自动调用该方法。开发者可以在该方法中完成Bean的清理和释放工作,例如关闭文件流、断开网络连接、从缓存中移除等。
如果一个Bean实现了DisposableBean接口,那么其销毁逻辑必须放在destroy()方法中执行。DisposableBean接口与@PreDestroy注解功能类似,但前者更加规范和标准化。在一般情况下,不建议使用DisposableBean和InitializingBean接口,而是应该采用更加灵活和可定制的方式,例如@Bean注解、@Configuration注解、BeanPostProcessor后置处理器等。
transient关键字:
在Java中,transient是一个关键字,用于修饰类的成员变量。当一个成员变量被声明为transient时,它将不会被序列化到对象的持久化状态中,即在对象进行序列化和反序列化时,这个成员变量的值不会被记录和恢复。
使用场景:
- 对于一些敏感数据或无需持久化的数据,可以使用transient关键字进行标识,避免泄露和意外保存。
- 如果某个类的部分成员变量不适合被持久化(例如临时计算结果、缓存信息等),也可以使用transient关键字进行标识。
注意点:
- 被transient修饰的变量不参与序列化和反序列化过程,因此必须在程序中手动对其进行初始化。
- 在使用transient关键字时,应该根据实际需求去衡量是否需要进行持久化,避免出现重要数据被误删除或遗漏的情况。
- 对于需要序列化和反序列化的对象,如果其中包含了transient成员变量,需要在自定义序列化和反序列化方法中进行处理,否则可能会导致无法正常序列化或反序列化。
volatile关键字:
在Java中,volatile是一个关键字,用于修饰类的成员变量。当一个成员变量被声明为volatile时,它将具有以下特点:
1. 可见性:对volatile变量的写操作会立即刷新到主内存中,而对volatile变量的读操作会从主内存中读取最新值。因此,不同线程中访问同一个volatile变量时,可以保证数据的可见性。
2. 有序性:对volatile变量的读/写操作会按照顺序执行,即保证了操作顺序的一致性。因此,在多线程并发访问时,不会出现指令重排等问题。
3. 不保证原子性:虽然volatile能够保证可见性和有序性,但不能保证原子性。当多个线程同时对volatile变量进行修改时,由于缺乏锁机制,可能会导致数据出现冲突或丢失。
使用场景:
1. 在多线程环境下,需要确保某个变量对所有线程都是可见的时,可以使用volatile关键字进行标识。
2. 在某些情况下,需要禁止JVM对变量进行指令重排优化时,也可以使用volatile关键字进行标识。
注意点:
1. 虽然volatile可以保证可见性和有序性,但不能保证原子性。如果需要对变量进行原子操作,应该使用synchronized、Lock等锁机制。
2. volatile变量对于频繁的读操作和少量的写操作比较适用,而对于频繁的读写操作或需要进行复杂的状态判断时,可能需要使用其他更加高级的并发工具。
3. 在使用volatile关键字时,需要根据具体场景进行权衡和选择。过度使用volatile会导致代码的可读性和维护性降低,因此应该避免不必要的使用。
HttpClientPoolFactory组件
@Configuration
@DependsOn({"SpringUtils"})
@Slf4j
public class HttpClientPoolFactory {
@Value("${httpClient.maxTotal}")
private Integer maxTotal;
@Value("${httpClient.maxPerRoute}")
private Integer maxPerRoute;
@Value("${httpClient.validateAfterInactivity}")
private Integer validateAfterInactivity;
@Value("${httpClient.requestConfig.connectTimeOut}")
private Integer connectTimeOut;
@Value("${httpClient.requestConfig.socketTimeOut}")
private Integer socketTimeOut;
@Bean
@Scope(value = "singleton")
public PoolingHttpClientConnectionManager getHttpClientPool() {
InterHandle<PoolingHttpClientConnectionManager> handledPool = poolingHttpClientConnectionManager -> {
poolingHttpClientConnectionManager.setMaxTotal(maxTotal);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxPerRoute);
poolingHttpClientConnectionManager.setValidateAfterInactivity(validateAfterInactivity);
return poolingHttpClientConnectionManager;
};
return handledPool.handle(new PoolingHttpClientConnectionManager());
}
@Bean("HttpClient")
@Scope(value = "singleton")
@ConditionalOnBean(PoolingHttpClientConnectionManager.class)
public HttpClient getHttpClient() {
return HttpClients.custom()
.setConnectionManager(SpringUtils.getBeanByType(PoolingHttpClientConnectionManager.class))
.setDefaultRequestConfig(RequestConfig.custom().setConnectTimeout(connectTimeOut).setSocketTimeout(socketTimeOut).build())
.setDefaultConnectionConfig(ConnectionConfig.custom().setCharset(StandardCharsets.UTF_8).build())
.build();
}
}
值得一提的是其中实现了我们之前提到过的函数式接口的使用:
@FunctionalInterface
public interface InterHandle <T>{
T handle(T t);
}
上述组件中我们用到了ConditionalOnBean注解:
ConditionalOnBean是一个条件注解,它用于在满足某个或多个特定Bean存在的条件下才创建当前Bean。具体作用和注意点如下:
作用:
1. 在某些场景下,需要根据不同的依赖关系动态创建Bean,可以使用ConditionalOnBean注解进行配置。
2. 当某个Bean存在时,才会创建当前Bean,避免了不必要的Bean创建和资源浪费。
3. ConditionalOnBean注解可以与其他条件注解(例如@Conditional、@ConditionalOnClass等)一起使用,以实现更加灵活的条件判断。
注意点:
1. ConditionalOnBean注解只有在当前应用上下文中包含指定的Bean时才会生效。如果该Bean在其他应用上下文中存在,则条件不符合,当前Bean也不会被创建。
2. 当需要同时判断多个Bean是否存在时,可以使用多个ConditionalOnBean注解来组合,此时所有指定的Bean都存在时才会创建当前Bean。
3. 如果某个Bean存在多个实例对象,可以使用value属性指定其中一个。也可以使用name属性指定Bean的名称,在XML配置文件或JavaConfig中定义。
4. ConditionalOnBean注解只适用于单例Bean,对于原型Bean或其他范围的Bean没有任何作用。
5. 如果当前Bean已经手动注册到容器中,那么即使满足条件,ConditionalOnBean注解也无法阻止该Bean的创建。
RpcServiceListener远程服务心跳检测器:
@Component
@ConditionalOnBean({OriginCenter.class})
@Scope("singleton")
@DependsOn({"ThreadPoolTaskExecutor","OriginCenterProperties","HttpServiceProxyFactoryMap","HttpClient"})
@Slf4j
public class RpcServiceListener{
public final static Long LOOP_TIME = 10000L;
@Qualifier("ThreadPoolTaskExecutor")
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Qualifier("OriginCenterProperties")
@Autowired
private OriginCenterProperties originCenterProperties;
@Qualifier("HttpServiceProxyFactoryMap")
@Autowired
private Map<String, HttpServiceProxyFactory> httpServiceProxyFactoryMap;
@Qualifier("HttpClient")
@Autowired
private HttpClient httpClient;
/*目前只能通过先开启远程服务实例再开启网关服务实例才能监测成功,且没有设置重连逻辑,若服务后续上线则无法监听并连接*/
public void rpcServiceListenTask(){
if (!CollectionUtils.isEmpty(httpServiceProxyFactoryMap) && Objects.equals(originCenterProperties.getServiceList().size(),httpServiceProxyFactoryMap.size())){
originCenterProperties.getServiceList().forEach(serviceModel -> {
threadPoolTaskExecutor.execute(()->{
while (true) {
while (httpServiceProxyFactoryMap.containsKey(serviceModel.getKey())){
try {
int statusCode = httpClient.execute(new HttpHead(URI.create(serviceModel.getUrl()))).getStatusLine().getStatusCode();
if (statusCode >= 200){
log.info("远程服务在线:"+serviceModel.getKey());
Thread.sleep(LOOP_TIME);
log.info("重新监测远程服务状态:"+serviceModel.getKey());
}
} catch (Exception e) {
log.error(e.getMessage());
log.warn("远程服务离线:"+serviceModel.getKey());
HttpServiceProxyFactory remove = httpServiceProxyFactoryMap.remove(serviceModel.getKey());
if (Objects.nonNull(remove)){
log.warn("控制中心移除远程服务成功:"+serviceModel.getKey());
log.info("发布事件尝试重连离线的远程服务");
SpringUtils.pushEvent(new ShutServiceEvent(serviceModel));
}
}
}
try {
Thread.sleep(LOOP_TIME);
} catch (InterruptedException e) {
log.error("监测远程服务线程异常:"+serviceModel.getKey());
break;
}
}
});
});
}
}
}
调用这个监听任务方法的时机一定在启动应用程序容器加载完毕成功初始化之后启动,这里我们需要注意的在检测到远程服务离线时发布了一个事件:
SpringUtils.pushEvent(new ShutServiceEvent(serviceModel));
ShutServiceEvent事件类:
/*服务离线事件,用于配置通知网关组件感知远程服务离线后 进行重连逻辑*/
public class ShutServiceEvent extends ApplicationEvent {
public ShutServiceEvent(ServiceModel source) {
super(source);
}
public ShutServiceEvent(ServiceModel source, Clock clock) {
super(source, clock);
}
@Override
public Object getSource() {
return super.getSource();
}
}
RpcShutServiceListener远程服务离线监听者:
@Component
@Slf4j
@DependsOn({"ThreadPoolTaskExecutor","HttpClient","HttpServiceProxyFactoryMap"})
public class RpcShutServiceListener implements ApplicationListener<ShutServiceEvent> {
@Qualifier("ThreadPoolTaskExecutor")
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Qualifier("HttpClient")
@Autowired
private HttpClient httpClient;
@Qualifier("HttpServiceProxyFactoryMap")
@Autowired
private Map<String, HttpServiceProxyFactory> httpServiceProxyFactoryMap;
@Override
public void onApplicationEvent(ShutServiceEvent event) {
ServiceModel source = (ServiceModel) event.getSource();
if (Objects.nonNull(source)) {
threadPoolTaskExecutor.execute(() -> {
while (!httpServiceProxyFactoryMap.containsKey(source.getKey())) {
try {
log.info("重连远程服务:" + source.getKey() + ":" + source.getUrl());
tryConn(source);
} catch (Exception e) {
log.info("重连远程服务失败:"+source.getKey());
}
try {
Thread.sleep(RpcServiceListener.LOOP_TIME);
} catch (InterruptedException e) {
log.error("重连远程服务线程异常");
break;
}
}
});
}
}
public void tryConn(ServiceModel source) throws IOException {
int statusCode = httpClient.execute(new HttpHead(URI.create(source.getUrl()))).getStatusLine().getStatusCode();
if (statusCode >= 200) {
httpServiceProxyFactoryMap.put(source.getKey(), HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(WebClient.builder().baseUrl(source.getUrl()).build()))
.build());
}
}
}
此处实现了接口ApplicationListener,使得我们的监听者能够收到事件发布者发布的事件,这里的监听者与事件发布者的实例化顺序有一些问题,这里只要我们不要使用ConditionalOnBean注解保证二者的实例化顺序就不会出问题。
SpringUtils事件发布者以及容器信息透传者:
@Component(value = "SpringUtils")
@Lazy(value = false)
@Slf4j
public class SpringUtils implements ApplicationContextAware, DisposableBean {
private static ApplicationContext serviceApplicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
serviceApplicationContext = applicationContext;
}
public static <T> T getBeanByType(Class<T> beanClazz) {
Assert.notNull(serviceApplicationContext, "容器上下文获取失败");
return serviceApplicationContext.getBean(beanClazz);
}
public static void pushEvent(ApplicationEvent applicationEvent){
Assert.notNull(serviceApplicationContext, "容器上下文获取失败");
serviceApplicationContext.publishEvent(applicationEvent);
}
@Override
public void destroy() throws Exception {
serviceApplicationContext = null;
}
}
启动类:
@SpringBootApplication
@Slf4j
public class CloudHttpInterfaceApplication {
public static void main(String[] args) {
SpringApplication.run(CloudHttpInterfaceApplication.class, args);
log.info("启动远程服务监听器");
SpringUtils.getBeanByType(RpcServiceListener.class).rpcServiceListenTask();
}
@Bean(name = "ThreadPoolTaskExecutor")
@Scope("singleton")
@ConditionalOnMissingBean(ThreadPoolTaskExecutor.class)
public ThreadPoolTaskExecutor getUserThreadPoolTaskExecutor() {
InterHandle<ThreadPoolTaskExecutor> handle = executor -> {
int core = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core * 2 + 1);
executor.setKeepAliveSeconds(3);
executor.setQueueCapacity(40);
executor.setThreadNamePrefix("rpc-originCenter-thread-pool");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
};
return handle.handle(new ThreadPoolTaskExecutor());
}
}
既然已经将需要配置的配置类结束了,服务分发的功能也是网关任务的一环,服务调用也在本文中实现:
远程服务接口配置:
public interface AirCheckApi {
/**
* Gets air list.
*
* @return the air list
*/
@GetExchange("getAllAirQualityIndex")
Object getAirList();
/**
* Gets air.
*
* @param id the id
* @return the air
*/
@GetExchange(url = "getAirQualityIndex")
Object getAir(@RequestParam Integer id);
/**
* Add air object.
*
* @param air the air
* @return the object
*/
@PostExchange("postAirQualityIndex")
Object addAir(@RequestBody Map<String, Object> air);
/**
* Del air object.
*
* @param Id the id
* @return the object
*/
@DeleteExchange("deleteAirQualityIndex")
Object delAir(@RequestParam Integer Id);
/**
* Update air object.
*
* @param id the id
* @return the object
*/
@PostExchange("updateAirQualityIndex")
Object updateAir(@RequestParam Integer id);
}
远程服务适配器:
@Configuration
@DependsOn({"HttpServiceProxyFactoryMap","OriginCenterProperties"})
@Slf4j
public class ServiceAdapter {
@Qualifier("HttpServiceProxyFactoryMap")
@Autowired
Map<String,HttpServiceProxyFactory> httpServiceProxyFactoryMap;
@Qualifier("OriginCenterProperties")
@Autowired
OriginCenterProperties originCenterProperties;
public <T> T getServiceApi(Class<T> apiClazz,String rpcServiceName) {
log.info("检测到远程请求,解析请求服务类型,分发服务");
if (StringUtils.hasText(rpcServiceName) && originCenterProperties.getServiceList().stream().map(ServiceModel::getKey).toList().contains(rpcServiceName)) {
HttpServiceProxyFactory httpServiceProxyFactory = httpServiceProxyFactoryMap.get(rpcServiceName);
if (Objects.isNull(httpServiceProxyFactory)) {
return null;
}
return httpServiceProxyFactory.createClient(apiClazz);
}else{
throw new RuntimeException("rpc service is not registered : "+rpcServiceName);
}
}
}
网关访问接口:
@RestController
@CrossOrigin
@Slf4j
public class Api {
@Autowired
private ServiceAdapter serviceAdapter;
@GetMapping("/getAllAirCheck")
@ResponseBody
public CompletableFuture<Object> getAirCheckService() {
return CompletableFuture.supplyAsync(() -> {
try {
AirCheckApi airCheck = serviceAdapter.getServiceApi(AirCheckApi.class, "air_check");
if (Objects.isNull(airCheck)) {
log.info("远程服务离线,降级处理请求");
return "返回网关请求(最近浏览的此服务数据)缓存结果";
}
log.info("远程服务分发成功,返回远程请求结果");
return airCheck.getAirList();
} catch (Exception e) {
log.error("远程服务调用失败:air_check离线");
return null;
}
});
}
}
其中存在新概念:协程,在java中原本是没有比线程更小的资源单位,golang在高并发任务中性能优于java的原因很大一部分就是它拥有比线程更小的资源单位-协程,但是本质上协程是一种轻量级的线程,也能够称其为用户态线程,不需要计算机操作系统进行程序上下文的切换,只需要在程序中设置切换的时机,特点是能够在单线程上运行多个逻辑独立,互不干扰的任务。个人认为其实协程就是可以在多逻辑独立的任务切换时由程序设置切换,不需要操作系统切换从而大大减少了CPU处理并发任务的压力。
当然本文提到的网关设计仍然存在很多问题,这里只是提供给springboot的初学者一种参考实践。学到用到感悟到。