SpringBoot集成gRPC微服务工程搭建实践的方法

前言

本文将使用Maven、gRPC、Protocol buffers、Docker、Envoy等工具构建一个简单微服务工程,笔者所使用的示例工程是以前写的一个Java后端工程,因为最近都在 学习微服务相关的知识,所以利用起来慢慢的把这个工程做成微服务化应用。在实践过程踩过很多坑,主要是经验不足对微服务还是停留在萌新阶段,通过本文 记录创建微服务工程碰到一些问题,此次实践主要是解决以下问题:

  • 如何解决、统一服务工程依赖管理
  • SpringBoot集成gRPC
  • 管理Protocol buffers文件
  • 使用Envoy代理访问gRPC
  • 部署到Docker

本文假设读者已经了解以下相关知识:

  • Maven
  • Envoy
  • gRPC
  • Protocol buffers
  • SpringBoot
  • Docker

由于是初步实现微服务,不会考虑过多的细节,现阶段只需要能够使用gRPC正常通信,后续计划会发布到k8s中,使用istio实现来服务网格。

使用Maven

现在比较流行的构建工具有Maven和Gradle,现阶段后端开发大多数都是用的Maven所以本工程也使用Maven来构建项目,当然使用Gradle也可以两者概念大都想通,不同的地方大多是实现和配置方式不一致。

使用项目继承

根据Maven的POM文件继承特性,将工程分不同的模块,所有的模块都继承父pom.xml的依赖、插件等内容,这样就可以实现统一管理,并方便以后管理、维护。先看一下大概的项目结构:

AppBubbleBackend   (1)
├── AppBubbleCommon
├── AppBubbleSmsService  (2)
├── AppBubbleUserService
├── docker-compose.yaml  (3)
├── pom.xml
├── protos     (4)
│ ├── sms
│ └── user
└── scripts     (5)
 ├── docker
 ├── envoy
 ├── gateway
 └── sql

以下是各个目录的用处简述,详细的用处文章后面都会提到,先在这里列出个大概:

  1. 工程主目录
  2. 单个服务工程目录(模块)
  3. docker-compose发布文件
  4. 存放.proto文件
  5. 发布、编译时用到的脚本文件

知道大概的项目工程结构后我们创建一个父pom.xml文件,放在AppBubbleBackend目录下面:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.1.2.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
 </parent>

 <groupId>com.bubble</groupId>
 <artifactId>bubble</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>pom</packaging>

 <modules>
  <module>AppBubbleSmsService</module>
  <module>AppBubbleCommon</module>
  <module>AppBubbleUserService</module>
 </modules>

 <!-- 省略其他部分 -->
</project>

因为使用SpringBoot构架,所以主pom.xml文件继承自SpringBoot的POM文件。 有了主pom.xml后然后使每个模块的pom.xml都继承自 主pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <parent>
 <groupId>com.bubble</groupId>
 <artifactId>bubble</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 </parent>
 <artifactId>sms</artifactId>
 <version>0.0.1-SNAPSHOT</version>

 <!-- 省略其他部分 -->
</project>

经过上面的配置后,所有的模块都会继承AppBubbleBackend中的pom.xml文件,这样可以很方便的更改依赖、配置等信息。

依赖管理

Maven提供依赖中心化的管理机制,通过项目继承特性所有对AppBubbleBackend/pom.xml所做的更改都会对其他模块产生影响,详细的依赖管理内容可查看官方文档。

 <dependencyManagement>
  <dependencies>
   <!-- gRPC -->
   <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty-shaded</artifactId>
    <version>${grpc.version}</version>
   </dependency>
  </dependencies>
</dependencyManagement>

通过dependencyManagement标签来配置依赖,这样可以就可以实现统一依赖的管理,并且还可以添加公共依赖。

插件管理

使用pluginManagement可以非常方便的配置插件,因为项目中使用了Protocol buffers需要集成相应的插件来生成Java源文件:

<pluginManagement>
   <plugins>
    <plugin>
     <groupId>org.xolstice.maven.plugins</groupId>
     <artifactId>protobuf-maven-plugin</artifactId>
     <version>0.5.1</version>
     <executions>
      <execution>
       <goals>
        <goal>compile</goal>
        <goal>compile-custom</goal>
       </goals>
      </execution>
     </executions>
    </plugin>
   </plugins>
</pluginManagement>

Protocol buffers插件的完整配置参数,可以这这里找到。

Profile

使用Profile的目的是为了区分生成Docker镜像时的一些特殊配置,示例工程只配置了一个docker-build的profile:

 <profiles>
  <profile>
   <id>docker-build</id>
   <properties>
    <jarName>app</jarName>
   </properties>
  </profile>
 </profiles>

  <properties>
  <jarName>${project.artifactId}-${project.version}</jarName>
 </properties>

 <build>
  <finalName>${jarName}</finalName>
 </build>

如果使用mvn package -P docker-build命令生成jar包时,相应的输出文件名是app.jar这样可以方便在Dockerfile中引用文件,而不需要使用${project.artifactId}-${project.version}的形式来查找输出的jar这样可以省去了解析pom.xml文件。如果还需要特殊的参数可以或者不同的行为,可以添加多个Profile,这样配置起来非常灵活。

Protocol buffers文件管理

因为是使用微服务开发,而且RPC通信框架是使用的gRPC,所以每个服务工程都会使用.proto文件。服务工程之间又会有使用同一份.proto文件的需求,比如在进行RPC通信时服务提供方返回的消息Test定义在a.proto文件中,那么在使用方在解析消息时也同样需要a.proto文件来将接收到的消息转换成Test消息,因此管理.proto文件也有一些小麻烦。关于Protocol buffers的使用可参考 官方文档。

Protocol buffers文件管理规约

在我们的示例项目中使用集中管理的方式,即将所有的.proto文件放置在同一个目录(AppBubbleBackend/protos)下并按服务名称来划分:

├── sms
│ ├── SmsMessage.proto
│ └── SmsService.proto
└── user
 └── UserMessage.proto

还可以将整个目录放置在一个单独的git仓库中,然后在项目中使用git subtree来管理文件。

Protocol buffers 插件配置

有了上面的目录结构后,就需要配置一下Protocol buffers的编译插件来支持这种.proto文件的组织结构。在讲解如何配置插件解决.proto文件的编译问题之前,推荐读者了解一下插件的配置文档:Xolstice Maven Plugins。在我们的工程中使用如下配置:

<plugin>
  <groupId>org.xolstice.maven.plugins</groupId>
  <artifactId>protobuf-maven-plugin</artifactId>
  <version>0.5.1</version>
  <configuration >
   <protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact>
   <pluginId>grpc-java</pluginId>
   <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.17.1:exe:${os.detected.classifier}</pluginArtifact>
   <additionalProtoPathElements combine.children="append" combine.self="append">
    <additionalProtoPathElement>${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis</additionalProtoPathElement>
    <additionalProtoPathElement>${GOPATH}/src</additionalProtoPathElement>
   </additionalProtoPathElements>
   <protoSourceRoot>${protos.basedir}</protoSourceRoot>
   <writeDescriptorSet>true</writeDescriptorSet>
   <includeDependenciesInDescriptorSet>true</includeDependenciesInDescriptorSet>
  </configuration>
  <!-- ... -->
 </plugin>

首先上面的插件配置使用protoSourceRoot标签将Protocol buffers的源文件目录更改成AppBubbleBackend/protos目录,因为工程中使用了googleapis来定义服务接口,所以需要使用添加additionalProtoPathElement标签添加额外的依赖文件。注意这个插件的配置是在AppBubbleBackend/pom.xml文件中的,服务工程都是继承此文件的。在父POM文件配置好以后,再看一下服务工程的插件配置:

<plugins>
 <plugin>
 <groupId>org.xolstice.maven.plugins</groupId>
 <artifactId>protobuf-maven-plugin</artifactId>
 <configuration>
  <includes>
  <include>${project.artifactId}/*.proto</include>
  <include>user/*.proto</include>
  </includes>
 </configuration>
 </plugin>
 </plugins>

服务工程主要使用includes标签,将需要的.proto文件包含在编译脚本中,includes标签中的include只是一个指定匹配.proto文件的匹配模式,<include>${project.artifactId}/*.proto</include>意思是AppBubbleBackend/protos/${project.artifactId}目录下的所有以.proto文件结尾的文件,如果服务工程有多个依赖可以将需要依赖的文件也添加到编译服务中,如上面的<include>user/*.proto</include>就将AppBubbleBackend/protos/user中的.proto文件添加进来,然后进行整体的编译。

gRPC

gRPC是由Google开源的RPC通信框架,gRPC使用Protocol buffers定义服务接口并自动生成gRPC相关代码,有了这些代码后就可以非常方便的实现gRPC服务端和gPRC客户端,过多的细节就不细说了先看一下如何使用在SpringBoot中使用gRPC。

运行gRPC服务

利用ApplicationRunner接口,在SprintBoot中运行gRPC服非常方便,只需要像下面代码一样就可以运行一个简单的gRPC服务。

package com.bubble.sms.grpc;

@Component
public class GrpcServerInitializer implements ApplicationRunner {

 @Autowired
 private List<BindableService> services;

 @Value("${grpc.server.port:8090}")
 private int port;

 @Override
 public void run(ApplicationArguments args) throws Exception {

  ServerBuilder serverBuilder = ServerBuilder
    .forPort(port);

  if (services != null && !services.isEmpty()) {
   for (BindableService bindableService : services) {
    serverBuilder.addService(bindableService);
   }
  }
  Server server = serverBuilder.build();
  serverBuilder.intercept(TransmitStatusRuntimeExceptionInterceptor.instance());
  server.start();
  startDaemonAwaitThread(server);
 }

 private void startDaemonAwaitThread(Server server) {
  Thread awaitThread = new Thread(() -> {
   try {
    server.awaitTermination();
   } catch (InterruptedException ignore) {

   }
  });
  awaitThread.setDaemon(false);
  awaitThread.start();
 }
}

Envoy代理

gRPC服务运行起来后就需要进行调试了,比如使用curl、chrome等工具向gRPC服务发起Restful请求,实际上gRPC的调试并没有那么简单。一开始的方案是使用了gRPC-gateway,为每个服务都启动一个网关将Http 1.x请求转换并发送到gRPC服务。然而gRPC-gateway只有go语言的版本,并没有Java语言的版本,所有在编译和使用中比较困难,后来发现了Envoy提供了envoy.grpc_json_transcoder这个http过滤器,可以很方便的将RESTful JSON API转换成gRPC请求并发送给gRPC服务器。

envoy的相关配置都放置在AppBubbleBackend/scripts/envoy目录中,里面的envoy.yaml是一份简单的配置文件:

static_resources:
 listeners:
 - name: grpc-8090
 address:
  socket_address: { address: 0.0.0.0, port_value: 8090 }
 filter_chains:
 - filters:
  - name: envoy.http_connection_manager
  config:
   stat_prefix: sms_http
   codec_type: AUTO
   # 省略部分配置
   http_filters:
   - name: envoy.grpc_json_transcoder
   config:
    proto_descriptor: "/app/app.protobin"
    services: ["sms.SmsService"]
    match_incoming_request_route: true
    print_options:
    add_whitespace: true
    always_print_primitive_fields: true
    always_print_enums_as_ints: false
    preserve_proto_field_names: false
# 省略部分配置    

使用envoy.grpc_json_transcoder过滤器的主要配置是proto_descriptor选项,该选项指向一个proto descriptor set文件。AppBubbleBackend/scripts/envoy/compile-descriptor.sh是编译proto descriptor set的脚本文件, 运行脚本文件会在脚本目录下生成一个app.protobin的文件,将此文件设置到envoy.grpc_json_transcoder就可大致完成了envoy的代理配置。

使用Docker发布

经过上面的一系统准备工作之后,我们就可以将服务发布到docker中了,Docker相关的文件都放置中AppBubbleBackend/scripts/docker和一个AppBubbleBackend/docker-compose.yaml文件。在发布时使用单个Dockerfile文件来制作服务镜像:

FROM rcntech/ubuntu-grpc:v0.0.5
EXPOSE 8080
EXPOSE 8090

#将当前目录添加文件到/bubble
ARG APP_PROJECT_NAME
#复制父pom.xml
ADD /pom.xml /app/pom.xml
ADD /protos /app/protos
ADD $APP_PROJECT_NAME /app/$APP_PROJECT_NAME
ADD scripts/gateway /app/gateway
ADD scripts/docker/entrypoint.sh /app/entrypoint.sh
RUN chmod u+x /app/entrypoint.sh

ENTRYPOINT ["/app/entrypoint.sh"]

有了Dockerfile文件后,在docker-compose.yaml里面做一些配置就能将服务打包成镜像:

 sms:
 build:
 context: ./
 dockerfile: scripts/docker/Dockerfile
 args:
  APP_PROJECT_NAME: "AppBubbleSmsService"
 environment:
 APOLLO_META: "http://apollo-configservice-dev:8080"
 APP_PROJECT_NAME: "AppBubbleSmsService"
 ENV: dev

同时编写了一个通用的entrypoint.sh脚本文件来启动服务器:

#!/bin/bash

export GOPATH=${HOME}/go
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin

rootProjectDir="/app"
projectDir="${rootProjectDir}/${APP_PROJECT_NAME}"

cd ${rootProjectDir}/AppBubbleCommon
./mvnw install

cd $projectDir
#打包app.jar
./mvnw package -DskipTests -P docker-build
#编译proto文件
./mvnw protobuf:compile protobuf:compile-custom -P docker-build

# Run service
java -jar ${projectDir}/target/app.jar

entrypoint.sh脚本中将服务工程编译成app.jar包再运行服务。还有envoy代理也要启动起来这样我们就可以使用curl或其他工具直接进行测试了。

总结

搭建这个工程大概摸索了一周的时间,主要的时间是花在了Protocol buffers文件的管理与使用Envoy作为代理调试gRPC服务上。文章中的示例工程已经传到了GitHub: AppBubbleBackend 后面会打算慢慢的完善这个应用,这是个简单的短视屏应用除了服务器还包含了Android和iOS端,等到将后端微服务化为开源出来供学习交流使用。

参考引用

gRPC官方文档
Protocol buffers Maven 插件文档
Protocol buffers官方文档
gRPC 官方文档中文版
gRPC-JSON transcoder

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

时间: 2019-01-18

配置SpringBoot方便的切换jar和war的方法示例

网上关于如何切换,其实说的很明确,本文主要通过profile进行快速切换已实现在不同场合下,用不同的打包方式. jar到war修改步骤 pom文件修改 packaging配置由jar改为war 排除tomcat等容器的依赖 配置web.xml或者无web.xml打包处理 入口类修改 添加ServletInitializer 特别注意:当改成war包的时候,application.properties配置的server.port和server.servlet.context-path就无效了,遵从

spring中的BeanFactory与FactoryBean的讲解

1.BeanFactory 1.1Spring提供了IOC容器的两种实现方式 ① BeanFactory:IOC容器的基本实现,是Spring内部的基础设施,是面向Spring本身的,不是提供给开发人员使用的. ② ApplicationContext:BeanFactory的子接口,提供了更多高级特性.面向Spring的使用者,几乎所有场合都使用ApplicationContext而不是底层的BeanFactory. 1.2 ApplicationContext的主要实现类 ClassPath

Spring多对象引入方法

在以前使用xml配置注入的时候, 可以通过name名称注入, 也可以使用type类型注入. 在SpringBoot中, 可以使用@Resource和@Autowried注解进行注入. @Resource 默认会使用名称进行注入,  如果找不到, 会使用自动使用类型进行注入. @Autowried, 则会在容器中寻找匹配的对象, 如果找到则注入成功, 如果没找到或者找到多个, 则会报错. 但是, 如果有多个的情况下, 可以使用@Qualifier("别名") 进行约束. 1.创建Bean

详解Spring 参数验证@Validated和@Valid的区别

Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303 规范,是标准 JSR-303 的一个变种),javax提供了@Valid(标准JSR-303规范),配合 BindingResult 可以直接提供参数验证结果.其中对于字段的特定验证注解比如 @NotNull 等网上到处都有,这里不详述 在检验 Controller 的入参是否符合规范时,使用 @Validated 或者 @Valid 在基本验证功能上没有太多区别.但是在分组.

SpringBoot AOP使用笔记

1. 启用AOP a. 在类上添加@Aspect注解 b. 注入该类, 可以使用@Component进行注入到Spring容器中 2. 通过PointCut对象创建切入点 a. 在某个方法使用类似下面的方法进行注入 @Pointcut("execution(* com.sguess.service.IAOPService.*(..))") private void pointcut() { } i. 其中,execution表达式为 execution(modifiers-patter

spring中bean的生命周期详解

1.Spring IOC容器可以管理bean的生命周期,Spring允许在bean生命周期内特定的时间点执行指定的任务. 2.Spring IOC容器对bean的生命周期进行管理的过程: ① 通过构造器或工厂方法创建bean实例 ② 为bean的属性设置值和对其他bean的引用 ③ 调用bean的初始化方法 ④ bean可以使用了 ⑤ 当容器关闭时,调用bean的销毁方法 3.在配置bean时,通过init-method和destroy-method 属性为bean指定初始化和销毁方法 4.be

Spring Boot和Docker实现微服务部署的方法

Spring boot 开发轻巧的微服务提供了便利,Docker 的发展又极大的方便了微服务的部署.这篇文章介绍一下如果借助 maven 来快速的生成微服务的镜像以及快速启动服务. 其实将 Spring Boot 项目用 Docker 部署也不是什么多么神秘的技术,也要先生成镜像,再用镜像启动容器,如果说有什么方便的地方,也就是一些工具可以帮助我们节省手动操作的过程. 知识背景: 掌握 docker 的安装以及基本的操作,熟悉 Dockerfile 文件创建镜像的方法. 创建 Spring bo

在SpringBoot中通过jasypt进行加密解密的方法

1.用途 在SpringBoot中,通过jasypt可以进行加密解密. 这个是双向的, 且可以配置密钥. 2.使用: 2.1通过UT创建工具类,并认识jasypt import org.jasypt.util.text.BasicTextEncryptor; import org.junit.Test; public class UtilTests { @Test public void jasyptTest() { BasicTextEncryptor encryptor = new Basi

springmvc中RequestMappingHandlerAdapter与HttpMessageConverter的装配讲解

一.DispatcherServlet 默认装配 RequestMappingHandlerAdapter ,而 RequestMappingHandlerAdapter 默认装配如下 HttpMessageConverter HttpMessageConverter: 2)加入jackson jar 包后,RequestMappingHandlerAdapter 装配的HttpMessageConverter 如下: 二. HttpMessageConverter 1)HttpMessageC

SpringBoot项目访问任意接口出现401错误的解决方案

之前搭建了一个SpringBoot项目用于测试集成Redis和MyBatis以及Freemarker,搭建完成测通之后就没有再打开过.今天打开之后想要测试一个问题,发现在这个项目下无论请求哪个接口,浏览器都会跳转到一个登录页面,而且这个页面不是我写的,如下图: 地址栏里的login也是在我输入了自己的接口之后,自动跳转到了login 于是用Postman测试,得到401响应: 当时一脸蒙蔽,心想我代码里面没有写拦截器啊,而且拦截之后的页面也不是我写的.刚开始认为可能和端口有关,后来发现不是.于是

springboot项目访问静态资源的配置代码实例

这篇文章主要介绍了springboot项目访问静态资源的配置代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 这里只是简单记录当上传图片不是放在tomcat其他服务器中时,只是放在磁盘中便可以这样配置,在项目启动后可以访问到磁盘中的资源. @Configuration public class SystemConfigurer implements WebMvcConfigurer { @Value("${jeewx.path.uploa

Springboot项目因为kackson版本问题启动报错解决方案

问题现象 org.springframework.context.ApplicationContextException: Unable to start embedded container; nested exception is org.springframework.boot.context.embedded.EmbeddedServletContainerException: Unable to start embedded Tomcat     at org.springframew

SpringBoot项目修改访问端口和访问路径的方法

创建SpringBoot项目,启动后,默认的访问路径即主机IP+默认端口号8080:http://localhost:8080/ 此时,我们就可以访问Controller层的接口了,如:http://localhost:8080/hello package com.springboot.test; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.a

SpringBoot如何使用feign实现远程接口调用和错误熔断

这篇文章主要介绍了SpringBoot如何使用feign实现远程接口调用和错误熔断,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.第一步,新建两个简单的springboot项目并创建rest接口 demo系统的rest接口 plus系统的调用接口 2.在项目pom文件里导入feign和hystrix的pom依赖包 <properties> <java.version>1.8</java.version> <s

SpringBoot项目如何访问jsp页面的示例代码

最近在接支付项目,从官方下了个及时到款的demo,想在springBoot项目中运行起来,发现访问jsp的时候直接会访问到jsp页面的源文件. 如何在springBoot项目中访问到jsp页面? 1.添加pom依赖 首先在原来的pom文件基础上加上这两个配置 <!-- tomcat 的支持.--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring

Docker 部署 SpringBoot 项目整合 Redis 镜像做访问计数示例代码

最终效果如下 大概就几个步骤 1.安装 Docker CE 2.运行 Redis 镜像 3.Java 环境准备 4.项目准备 5.编写 Dockerfile 6.发布项目 7.测试服务 环境准备 系统:Ubuntu 17.04 x64 Docker 17.12.0-ce IP:45.32.31.101 一.安装 Docker CE 国内不建议使用:"脚本进行安装",会下载安装很慢,使用步骤 1 安装,看下面的链接:常规安装方式 1.常规安装方式 Ubuntu 17.04 x64 安装

springBoot项目启动类启动无法访问的解决方法

网上也查了一些资料,我这里总结.下不来虚的,也不废话. 解决办法: 1.若是maven项目,则找到右边Maven Projects --->Plugins--->run(利用maven启动)则可以加载到webapp资源 2.上面方法治标不治本.在项目的pom文件中添加<bulid>标签标注路径即可,pom.xml后部分代码如下: 刷新maven加载,重启项目.若还是无法访问,重新导入项目 <dependencies> xxxxxxxxxxxx </dependen

详解springboot项目带Tomcat和不带Tomcat的两种打包方式

1,带Tomcat的打包方式 1.1, 在pom.xml文件添加以下配置(目的:自定main入口和跳过Junit代码) <build> <plugins> <!--打包为jar时指定main入口--> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <

Tomcat启动springboot项目war包报错:启动子级时出错的问题

今天公司springboot项目准备部署到测试服务器上进行测试,打包好war后放到tomcat里面启动后,前端文件能访问到,但是接口请求一直是404,一直找了很久的原因,tomcat启动是成功的,war打包的时候也提示build success了,tomcat启动日志发现报错: java.lang.IllegalStateException: 启动子级时出错   at org.apache.catalina.core.ContainerBase.addChildInternal(Containe