apollo与springboot集成实现动态刷新配置的教程详解

分布式apollo简介

Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。

本文主要介绍如何使用apollo与springboot实现动态刷新配置,如果之前不了解apollo可以查看如下文档

https://github.com/ctripcorp/apollo

学习了解一下apollo,再来查看本文

正文

apollo与spring实现动态刷新配置本文主要演示2种刷新,一种基于普通字段刷新、一种基于bean上使用了@ConfigurationProperties刷新

1、普通字段刷新

a、pom.xml配置

 <dependency>
  <groupId>com.ctrip.framework.apollo</groupId>
  <artifactId>apollo-client</artifactId>
  <version>1.6.0</version>
 </dependency>

b、客户端配置AppId,Apollo Meta Server

此配置有多种方法,本示例直接在application.yml配置,配置内容如下

app:
  id: ${spring.application.name}
apollo:
  meta: http://192.168.88.128:8080,http://192.168.88.129:8080
  bootstrap:
    enabled: true
    eagerLoad:
      enabled: true

c、项目中启动类上加上@EnableApolloConfig注解,形如下

@SpringBootApplication
@EnableApolloConfig(value = {"application","user.properties","product.properties","order.properties"})
public class ApolloApplication {

	public static void main(String[] args) {

		SpringApplication.run(ApolloApplication.class, args);
	}

}

@EnableApolloConfig不一定要加在启动类上,加在被spring管理的类上即可

d、在需刷新的字段上配置@Value注解,形如

 @Value("${hello}")
 private String hello;

通过以上三步就可以实现普通字段的动态刷新

2.bean使用@ConfigurationProperties动态刷新

bean使用@ConfigurationProperties注解目前还不支持自动刷新,得编写一定的代码实现刷新。目前官方提供2种刷新方案

  • 基于RefreshScope实现刷新
  • 基于EnvironmentChangeEvent实现刷新
  • 本文再提供一种,当bean上如果使用了@ConditionalOnProperty如何实现刷新

a、基于RefreshScope实现刷新

1、pom.xml要额外引入

 <dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-context</artifactId>
  <version>2.0.3.RELEASE</version>
 </dependency>

2、bean上使用@RefreshScope注解

@Component
@ConfigurationProperties(prefix = "product")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@RefreshScope
public class Product {

 private Long id;

 private String productName;

 private BigDecimal price;

}

3、利用RefreshScope搭配@ApolloConfigChangeListener监听实现bean的动态刷新,其代码实现如下

@ApolloConfigChangeListener(value="product.properties",interestedKeyPrefixes = {"product."})
 private void refresh(ConfigChangeEvent changeEvent){

 refreshScope.refresh("product");

 PrintChangeKeyUtils.printChange(changeEvent);
 }

b、基于EnvironmentChangeEvent实现刷新

利用spring的事件驱动配合@ApolloConfigChangeListener监听实现bean的动态刷新,其代码如下

@Component
@Slf4j
public class UserPropertiesRefresh implements ApplicationContextAware {

 private ApplicationContext applicationContext;

 @ApolloConfigChangeListener(value="user.properties",interestedKeyPrefixes = {"user."})
 private void refresh(ConfigChangeEvent changeEvent){
 applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));

 PrintChangeKeyUtils.printChange(changeEvent);
 }

 @Override
 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 this.applicationContext = applicationContext;
 }
}

c、当bean上有@ConditionalOnProperty如何实现刷新

当bean上有@ConditionalOnProperty注解时,上述的两种方案可以说失效了,因为@ConditionalOnProperty是一个条件注解,当不满足条件注解时,bean是没法注册到spring容器中的。如果我们要实现此种情况的下的动态刷新,我们就得自己手动注册或者销毁bean了。其实现流程如下

1、当满足条件注解时,则手动创建bean,然后配合@ApolloConfigChangeListener监听该bean的属性变化。当该bean属性有变化时,手动把属性注入bean。同时刷新依赖该bean的其他bean

2、当不满足条件注解时,则手动从spring容器中移除bean,同时刷新依赖该bean的其他bean

其刷新核心代码如下

public class OrderPropertiesRefresh implements ApplicationContextAware {

 private ApplicationContext applicationContext;

 @ApolloConfig(value = "order.properties")
 private Config config;

 @ApolloConfigChangeListener(value="order.properties",interestedKeyPrefixes = {"order."},interestedKeys = {"model.isShowOrder"})
 private void refresh(ConfigChangeEvent changeEvent){
 for (String basePackage : listBasePackages()) {
  Set<Class> conditionalClasses = ClassScannerUtils.scan(basePackage, ConditionalOnProperty.class);
  if(!CollectionUtils.isEmpty(conditionalClasses)){
  for (Class conditionalClass : conditionalClasses) {
   ConditionalOnProperty conditionalOnProperty = (ConditionalOnProperty) conditionalClass.getAnnotation(ConditionalOnProperty.class);
   String[] conditionalOnPropertyKeys = conditionalOnProperty.name();
   String beanChangeCondition = this.getChangeKey(changeEvent,conditionalOnPropertyKeys);
   String conditionalOnPropertyValue = conditionalOnProperty.havingValue();
   boolean isChangeBean = this.changeBean(conditionalClass, beanChangeCondition, conditionalOnPropertyValue);
   if(!isChangeBean){
   // 更新相应的bean的属性值,主要是存在@ConfigurationProperties注解的bean
   applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
   }
  }
  }
 }

 PrintChangeKeyUtils.printChange(changeEvent);
 printAllBeans();
 }

 /**
 * 根据条件对bean进行注册或者移除
 * @param conditionalClass
 * @param beanChangeCondition bean发生改变的条件
 * @param conditionalOnPropertyValue
 */
 private boolean changeBean(Class conditionalClass, String beanChangeCondition, String conditionalOnPropertyValue) {
 boolean isNeedRegisterBeanIfKeyChange = this.isNeedRegisterBeanIfKeyChange(beanChangeCondition,conditionalOnPropertyValue);
 boolean isNeedRemoveBeanIfKeyChange = this.isNeedRemoveBeanIfKeyChange(beanChangeCondition,conditionalOnPropertyValue);
 String beanName = StringUtils.uncapitalize(conditionalClass.getSimpleName());
 if(isNeedRegisterBeanIfKeyChange){
  boolean isAlreadyRegisterBean = this.isExistBean(beanName);
  if(!isAlreadyRegisterBean){
  this.registerBean(beanName,conditionalClass);
  return true;
  }
 }else if(isNeedRemoveBeanIfKeyChange){
  this.unregisterBean(beanName);
  return true;
 }
 return false;
 }

 /**
 * bean注册
 * @param beanName
 * @param beanClass
 */
 public void registerBean(String beanName,Class beanClass) {
 log.info("registerBean->beanName:{},beanClass:{}",beanName,beanClass);
 BeanDefinitionBuilder beanDefinitionBurinilder = BeanDefinitionBuilder.genericBeanDefinition(beanClass);
 BeanDefinition beanDefinition = beanDefinitionBurinilder.getBeanDefinition();
 setBeanField(beanClass, beanDefinition);
 getBeanDefinitionRegistry().registerBeanDefinition(beanName,beanDefinition);

 }

 /**
 * 设置bean字段值
 * @param beanClass
 * @param beanDefinition
 */
 private void setBeanField(Class beanClass, BeanDefinition beanDefinition) {
 ConfigurationProperties configurationProperties = (ConfigurationProperties) beanClass.getAnnotation(ConfigurationProperties.class);
 if(ObjectUtils.isNotEmpty(configurationProperties)){
  String prefix = configurationProperties.prefix();
  for (String propertyName : config.getPropertyNames()) {
  String fieldPrefix = prefix + ".";
  if(propertyName.startsWith(fieldPrefix)){
   String fieldName = propertyName.substring(fieldPrefix.length());
   String fieldVal = config.getProperty(propertyName,null);
   log.info("setBeanField-->fieldName:{},fieldVal:{}",fieldName,fieldVal);
   beanDefinition.getPropertyValues().add(fieldName,fieldVal);
  }
  }
 }
 }

 /**
 * bean移除
 * @param beanName
 */
 public void unregisterBean(String beanName){
 log.info("unregisterBean->beanName:{}",beanName);
 getBeanDefinitionRegistry().removeBeanDefinition(beanName);
 }

 public <T> T getBean(String name) {
 return (T) applicationContext.getBean(name);
 }

 public <T> T getBean(Class<T> clz) {
 return (T) applicationContext.getBean(clz);
 }

 public boolean isExistBean(String beanName){
 return applicationContext.containsBean(beanName);
 }

 public boolean isExistBean(Class clz){
 try {
  Object bean = applicationContext.getBean(clz);
  return true;
 } catch (BeansException e) {
  // log.error(e.getMessage(),e);
 }
 return false;
 }

 private boolean isNeedRegisterBeanIfKeyChange(String changeKey,String conditionalOnPropertyValue){
 if(StringUtils.isEmpty(changeKey)){
  return false;
 }
 String apolloConfigValue = config.getProperty(changeKey,null);
 return conditionalOnPropertyValue.equals(apolloConfigValue);
 }

 private boolean isNeedRemoveBeanIfKeyChange(String changeKey,String conditionalOnPropertyValue){
 if(!StringUtils.isEmpty(changeKey)){
  String apolloConfigValue = config.getProperty(changeKey,null);
  return !conditionalOnPropertyValue.equals(apolloConfigValue);
 }

 return false;

 }

 private boolean isChangeKey(ConfigChangeEvent changeEvent,String conditionalOnPropertyKey){
 Set<String> changeKeys = changeEvent.changedKeys();
 if(!CollectionUtils.isEmpty(changeKeys) && changeKeys.contains(conditionalOnPropertyKey)){
  return true;
 }
 return false;
 }

 private String getChangeKey(ConfigChangeEvent changeEvent, String[] conditionalOnPropertyKeys){
 if(ArrayUtils.isEmpty(conditionalOnPropertyKeys)){
  return null;
 }
 String changeKey = null;
 for (String conditionalOnPropertyKey : conditionalOnPropertyKeys) {
  if(isChangeKey(changeEvent,conditionalOnPropertyKey)){
  changeKey = conditionalOnPropertyKey;
  break;
  }
 }

 return changeKey;
 }

 private BeanDefinitionRegistry getBeanDefinitionRegistry(){
 ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
 BeanDefinitionRegistry beanDefinitionRegistry = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
 return beanDefinitionRegistry;
 }

 private List<String> listBasePackages(){
 ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
 return AutoConfigurationPackages.get(configurableContext.getBeanFactory());
 }

 @Override
 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 this.applicationContext = applicationContext;
 }

 public void printAllBeans() {
 String[] beans = applicationContext.getBeanDefinitionNames();
 Arrays.sort(beans);
 for (String beanName : beans) {
  Class<?> beanType = applicationContext.getType(beanName);
  System.out.println(beanType);
 }
 }
}

如果条件注解的值也是配置在apollo上,可能会出现依赖条件注解的bean的其他bean,在项目拉取apollo配置时,就已经注入spring容器中,此时就算条件注解满足条件,则引用该条件注解bean的其他bean,也会拿不到条件注解bean。此时有2种方法解决,一种是在依赖条件注解bean的其他bean注入之前,先手动注册条件注解bean到spring容器中,其核心代码如下

@Component
@Slf4j
public class RefreshBeanFactory implements BeanFactoryPostProcessor {

 @Override
 public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
 Config config = ConfigService.getConfig("order.properties");
 List<String> basePackages = AutoConfigurationPackages.get(configurableListableBeanFactory);
 for (String basePackage : basePackages) {
  Set<Class> conditionalClasses = ClassScannerUtils.scan(basePackage, ConditionalOnProperty.class);
  if(!CollectionUtils.isEmpty(conditionalClasses)){
  for (Class conditionalClass : conditionalClasses) {
   ConditionalOnProperty conditionalOnProperty = (ConditionalOnProperty) conditionalClass.getAnnotation(ConditionalOnProperty.class);
   String[] conditionalOnPropertyKeys = conditionalOnProperty.name();
   String beanConditionKey = this.getConditionalOnPropertyKey(config,conditionalOnPropertyKeys);
   String conditionalOnPropertyValue = conditionalOnProperty.havingValue();
   this.registerBeanIfMatchCondition((DefaultListableBeanFactory)configurableListableBeanFactory,config,conditionalClass,beanConditionKey,conditionalOnPropertyValue);
  }
  }
 }

 }

 private void registerBeanIfMatchCondition(DefaultListableBeanFactory beanFactory,Config config,Class conditionalClass, String beanConditionKey, String conditionalOnPropertyValue) {
 boolean isNeedRegisterBean = this.isNeedRegisterBean(config,beanConditionKey,conditionalOnPropertyValue);
 String beanName = StringUtils.uncapitalize(conditionalClass.getSimpleName());
 if(isNeedRegisterBean){
  this.registerBean(config,beanFactory,beanName,conditionalClass);

 }

 }

 public void registerBean(Config config,DefaultListableBeanFactory beanFactory, String beanName, Class beanClass) {
 log.info("registerBean->beanName:{},beanClass:{}",beanName,beanClass);
 BeanDefinitionBuilder beanDefinitionBurinilder = BeanDefinitionBuilder.genericBeanDefinition(beanClass);
 BeanDefinition beanDefinition = beanDefinitionBurinilder.getBeanDefinition();
 setBeanField(config,beanClass, beanDefinition);
 beanFactory.registerBeanDefinition(beanName,beanDefinition);

 }

 private void setBeanField(Config config,Class beanClass, BeanDefinition beanDefinition) {
 ConfigurationProperties configurationProperties = (ConfigurationProperties) beanClass.getAnnotation(ConfigurationProperties.class);
 if(ObjectUtils.isNotEmpty(configurationProperties)){
  String prefix = configurationProperties.prefix();
  for (String propertyName : config.getPropertyNames()) {
  String fieldPrefix = prefix + ".";
  if(propertyName.startsWith(fieldPrefix)){
   String fieldName = propertyName.substring(fieldPrefix.length());
   String fieldVal = config.getProperty(propertyName,null);
   log.info("setBeanField-->fieldName:{},fieldVal:{}",fieldName,fieldVal);
   beanDefinition.getPropertyValues().add(fieldName,fieldVal);
  }
  }
 }
 }

 public boolean isNeedRegisterBean(Config config,String beanConditionKey,String conditionalOnPropertyValue){
 if(StringUtils.isEmpty(beanConditionKey)){
  return false;
 }
 String apolloConfigValue = config.getProperty(beanConditionKey,null);
 return conditionalOnPropertyValue.equals(apolloConfigValue);
 }

 private String getConditionalOnPropertyKey(Config config, String[] conditionalOnPropertyKeys){
 if(ArrayUtils.isEmpty(conditionalOnPropertyKeys)){
  return null;
 }
 String changeKey = null;
 for (String conditionalOnPropertyKey : conditionalOnPropertyKeys) {
  if(isConditionalOnPropertyKey(config,conditionalOnPropertyKey)){
  changeKey = conditionalOnPropertyKey;
  break;
  }
 }

 return changeKey;
 }
 private boolean isConditionalOnPropertyKey(Config config,String conditionalOnPropertyKey){
 Set<String> propertyNames = config.getPropertyNames();
 if(!CollectionUtils.isEmpty(propertyNames) && propertyNames.contains(conditionalOnPropertyKey)){
  return true;
 }
 return false;
 }
}

其次利用懒加载的思想,在使用条件注解bean时,使用形如下方法

Order order = (Order)
SpringContextUtils.getBean("order");

总结

本文主要介绍了常用的动态刷新,但本文的代码示例实现的功能不局限于此,本文的代码还实现如何通过自定义注解与apollo整合来实现一些业务操作,同时也实现了基于hystrix注解与apollo整合,实现基于线程隔离的动态熔断,感兴趣的朋友可以复制文末链接到浏览器,进行查看

apollo基本上是能满足我们日常的业务开发要求,但是对于一些需求,比如动态刷新线上数据库资源啥,我们还是得做一定的量的改造,好在携程也提供了apollo-use-cases,在里面可以找到常用的使用场景以及示例代码,其链接如下

https://github.com/ctripcorp/apollo-use-cases

感兴趣的朋友,可以查看下。

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-apollo

到此这篇关于apollo与springboot集成实现动态刷新配置的文章就介绍到这了,更多相关apollo与springboot集成动态刷新配置内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Springboot项目如何使用apollo配置中心

    这篇文章主要介绍了Springboot项目如何使用apollo配置中心,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1. 引入 apollo 配置依赖 <dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client</artifactId> <version>1.1.0<

  • springboot+idea热部署的实现方法(自动刷新)

    近来在使用idea做springboot的项目,但是发现每次修改之后我都需要重新将项目关闭再开启,这样比较繁琐,发现通过热部署的方式让我们可以一边修改我们的项目,然后在页面中直接通过刷新展示出来 spring为开发者提供了一个名为spring-boot-devtools的模块来使Spring Boot应用支持热部署,提高开发者的开发效率,无需手动重启Spring Boot应用. devtools的原理 深层原理是使用了两个ClassLoader,一个Classloader加载那些不会改变的类(第

  • SpringBoot配置Apollo代码实例

    这篇文章主要介绍了SpringBoot配置Apollo代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Windows环境安装下载,参考:https://github.com/ctripcorp/apollo 项目引用 <dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client</art

  • 基于springboot+jwt实现刷新token过程解析

    前一段时间讲过了springboot+jwt的整合,但是因为一些原因(个人比较懒)并没有更新关于token的刷新问题,今天跟别人闲聊,聊到了关于业务中token的刷新方式,所以在这里我把我知道的一些点记录一下,也希望能帮到一些有需要的朋友,同时也希望给我一些建议,话不多说,上代码! 1:这种方式为在线刷新,比方说设定的token有效期为30min,那么每次访问资源时,都会在拦截器中去判断一下token是否过期,如果没有过期就刷新token的时间为30min,反之则会重新登录,需要注意的是这种方式

  • apollo与springboot集成实现动态刷新配置的教程详解

    分布式apollo简介 Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境.不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限.流程治理等特性. 本文主要介绍如何使用apollo与springboot实现动态刷新配置,如果之前不了解apollo可以查看如下文档 https://github.com/ctripcorp/apollo 学习了解一下apollo,再来查看本文 正文 apollo与spring实现动态刷新配置本文主要演示2种刷新,一种

  • Spring Cloud 动态刷新配置信息教程详解

    有时候在配置中心有些参数是需要修改的,这时候如何不重启而达到实时生效的效果呢? 添加依赖 <dependencies> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> ... </dependencies>

  • SpringBoot 集成Kaptcha实现验证码功能实例详解

    在一个web应用中验证码是一个常见的元素.不管是防止机器人还是爬虫都有一定的作用,我们是自己编写生产验证码的工具类,也可以使用一些比较方便的验证码工具.在网上收集一些资料之后,今天给大家介绍一下kaptcha的和springboot一起使用的简单例子. 准备工作: 1.你要有一个springboot的hello world的工程,并能正常运行. 2.导入kaptcha的maven: <!-- https://mvnrepository.com/artifact/com.github.penggl

  • SpringBoot集成WebSocket长连接实际应用详解

    前言: 一.WebSocket之初出茅驴 官方定义:WebSocket是一种在单个TCP连接上进行全双工通信的协议.WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据.在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输.是真正的双向平等对话,属于服务器推送技术的一种. 太官方啦,还是博主过来翻译一下吧 :WebSocket技术只需要service和client建立一次连接,就能实现服

  • win10 DVWA下载安装配置图文教程详解(新手学渗透)

    电脑重装系统了,需要重新装一下渗透测试的学习环境DVWA,借此机会就跟大家讲一下DVWA的安装过程,因为不同的电脑配置.环境不同,在我的电脑上按照我这个安装教程是一次性就安装好了的.如果安装的时候遇到了什么问题,欢迎大家在评论区讨论,我每天都会查看博客,看到了能解决我就会回复. 安装过程总共分两步,phpstudy的下载以及dvwa的下载.下面正式进入安装教程: 1.1首先需要准备的是DVWA的环境,DVWA需要运行在有数据库/服务器等多种环境下,我们一般选用集成了这些环境的phpStudy,

  • SpringBoot实现多环境配置文件切换教程详解

    目录 背景 解决方案 一.新建配置文件 二. 服务调用测试 2.1 新建调用类 2.2 使用样例项目 三.扩展练习 3.1 使用注解标记配置,首先定义一个接口 3.2 分别定义俩个实现类来实现它 3.3 修改application.yml文件激活配置 3.4 新增查询方法 3.5 使用一个或多个配置文件及激活标记文件 3.6 切换日志文件 背景 很多时候,我们项目在开发环境和生成环境的环境配置是不一样的,例如,数据库配置,在开发的时候,我们一般用测试数据库,而在生产环境的时候,我们是用正式的数据

  • Mysql 5.7.19 免安装版配置方法教程详解(64位)

    官方网站下载mysql-5.7.19-winx64,注意对应系统64位或者32位,这里使用的是64位. 解压放置到本地磁盘.发现文件很大,大概是1.6G左右.删除lib文件夹下的.lib文件和debug文件夹下所有文件. 在主目录下创建my.ini文件,文件内容如下:(这里是简洁版,对应本机修改basedir和datadir的目录,根据需要可以自己扩充配置) [client] port=3306 default-character-set=utf8 [mysqld] basedir=D:\Jav

  • Winserver2012下mysql 5.7解压版(zip)配置安装教程详解

    一.安装 1.下载mysql zip版本mysql不需要运行可执行文件,解压即可,下载zip版本mysql msi版本mysql双击文件即可安装,相对简单,本文不介绍此版本安装 2.配置环境变量 打开环境变量配置页面(winserver服务器环境变量位置:服务器管理器->本地服务器->计算机名称->高级->环境变量),在系统变量path后面添加mysql bin文件路径,例如:;C:\mysql-5.7.17-winx64\bin 3.配置mysql mysql配置文件my-def

  • Node.js+Express配置入门教程详解

    Node.js是一个Javascript运行环境(runtime).实际上它是对Google V8引擎进行了封装.V8引 擎执行Javascript的速度非常快,性能非常好.Node.js对一些特殊用例进行了优化,提供了替代的API,使得V8在非浏览器环境下运行得更好.Node.js是一个基于Chrome JavaScript运行时建立的平台, 用于方便地搭建响应速度快.易于扩展的网络应用.Node.js 使用事件驱动, 非阻塞I/O 模型而得以轻量和高效,非常适合在分布式设备上运行的数据密集型

  • CentOS6.5下Tomcat7 Nginx Redis配置步骤教程详解

    所有配置均在一台机器上完成,部署拓扑信息如下: 注意:由于Redis配置对jar包和tomcat版本比较严格,请务必使用tomcat7和本文中提供的jar包. 下载地址: http://pan.baidu.com/s/1bO67Ky tomcat: tomcat1 localhost:8080 tomcat2 localhost:9080 nginx: localhost:1210 redis: localhost:6379 1. tomcat的安装和配置 1. 在server.xml文件中,修

随机推荐