SpringBoot——配置嵌入式 Servlet容器

更多内容,前往 IT-BLOG

一、如何定制和修改Servlet容器的相关配置


 前言:SpringBoot 在Web 环境下,默认使用的是 Tomact 作为嵌入式的 Servlet容器;
 
【1】修改和 server相关的配置(ServerProperties 实现了 EmbeddedServletContainerCustomizer)例如:修改端口号

#通用的Servlet容器设置:修改端口号
server:
  port: 8081
  tomcat:  #设置Tomact的相关属性,例如编码格式
    uri-encoding: utf-8

  ☞ 我们也可以进入 port所属的对象中,发现其他可修改的参数等等,如下:

 1 @ConfigurationProperties(
 2     prefix = "server",
 3     ignoreUnknownFields = true
 4 )
 5 public class ServerProperties implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered {
 6     private Integer port;
 7     private InetAddress address;
 8     private String contextPath;
 9     private String displayName = "application";
10     ......

【2】编写一个 EmbeddedServletContainerCustomizer:嵌入式的 Servlet容器的定制器,来修改 Servlet容器的配置。其实1中的 ServerProperties也是实现了 EmbeddedServletContainerCustomizer。xxxCustomizer 帮组我们进行定制配置。

 1 @Configuration
 2 public class MyMvcConfig extends WebMvcConfigurerAdapter {
 3     @Bean
 4     public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
 5         return new EmbeddedServletContainerCustomizer() {
 6             @Override
 7             public void customize(ConfigurableEmbeddedServletContainer container) {
 8                 container.setPort(8082);
 9             }
10         };
11     }

二、注册Servlet三大组件【Servlet、Filter、Listener】


由于 SpringBoot默认是以 jar包的方式启动嵌入的 Servlet容器来启动 SpringBoot的 web应用,没有 web.xml文件。注册三大组件的方式如下:
【1】通过 ServletRegistrationBean 注册自定义的 Servlet。

 1 //首先创建一个Servlet
 2 public class MyServlet extends HttpServlet {
 3     @Override
 4     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 5         super.doGet(req, resp);
 6     }
 7 
 8     @Override
 9     protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
10         resp.getWriter().write("Hello MyServlet");
11         super.doPost(req, resp);
12     }
13 }
14 
15 //将创建的Servlet通过配置类注入到容器中,两个是不同的类。
16 @Configuration
17 public class MyMvcConfig extends WebMvcConfigurerAdapter {
18     @Bean
19     public ServletRegistrationBean myServlet(){
20         ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(), "/myServlet");
21         return registrationBean;
22     }

【2】通过 FilterRegistrationBean 注册拦截器 Filter。

 1 //自定义一个filter实现servlet.Filter接口
 2 public class myFilter implements Filter {
 3     @Override
 4     public void init(FilterConfig filterConfig) throws ServletException {
 5 
 6     }
 7 
 8     @Override
 9     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
10         System.out.printf("myFilter");
11         filterChain.doFilter(servletRequest,servletResponse);
12     }
13 
14     @Override
15     public void destroy() {
16 
17     }
18 }
19 
20 //通过配置类注入自定义的Filter
21 @Configuration
22 public class MyMvcConfig extends WebMvcConfigurerAdapter {
23     @Bean
24     public FilterRegistrationBean myFilter(){
25         FilterRegistrationBean registrationBean = new FilterRegistrationBean();
26         registrationBean.setFilter(new myFilter());
27         registrationBean.setUrlPatterns(Arrays.asList("/hello","/myFilter"));
28         return registrationBean;
29     }

【3】通过 ServletListenerRegistrationBean 注册自定义的 Listener。

 1 //创建自定义的Listener监听
 2 public class myListener implements ServletContextListener {
 3     @Override
 4     public void contextInitialized(ServletContextEvent servletContextEvent) {
 5         System.out.printf("服务启动");
 6     }
 7 
 8     @Override
 9     public void contextDestroyed(ServletContextEvent servletContextEvent) {
10         System.out.printf("服务销毁");
11     }
12 }
13 
14 //通过配置类注入自定义的listener
15 @Configuration
16 public class MyMvcConfig extends WebMvcConfigurerAdapter {
17 
18     public ServletListenerRegistrationBean myListener(){
19         ServletListenerRegistrationBean<MyListener> servletListenerRegistrationBean = new ServletListenerRegistrationBean<>(new MyListener());
20         return servletListenerRegistrationBean;
21     }

三、使用其他 Servlet容器:Jetty(长连接引用)、Undertow(不支持JSP)


【1】我们在定制嵌入式的 Servlet容器时,会传入 ConfigurableEmbeddedServletContainer类,我们通过 Ctrl+T查看此可配置嵌入式类容器中可以配置 Tomcat、Jetty和Undertow。

 1 //ConfigurableEmbeddedServletContainer 
 2 @Bean
 3 public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
 4     return new EmbeddedServletContainerCustomizer() {
 5         @Override
 6         public void customize(ConfigurableEmbeddedServletContainer container) {
 7             container.setPort(8082);
 8         }
 9     };
10 }

 
【2】默认使用 Tomcat,因为 starter-web 引入的是 Tomcat的 starter。我们排除 Tomcat的依赖,引入 Jetty的依赖即可。

 1 <dependency>
 2     <groupId>org.springframework.boot</groupId>
 3     <artifactId>spring-boot-starter-web</artifactId>
 4     <exclusions>
 5         <exclusion>
 6             <artifactId>spring-boot-starter-tomcat</artifactId>
 7             <groupId>org.springframework.boot</groupId>
 8         </exclusion>
 9     </exclusions>
10 </dependency>
11 
12 <dependency>
13     <artifactId>spring-boot-starter-Jetty</artifactId>
14     <groupId>org.springframework.boot</groupId>
15 </dependency>

四、嵌入式 Servlet容器自动配置原理


【1】EmbeddedServletContainerAutoConfiguration 类主要用来自动配置嵌入式的 Servlet容器。

 1 @AutoConfigureOrder(-2147483648)
 2 @Configuration
 3 @ConditionalOnWebApplication
 4  //导入BeanPostProcessorsRegistrar:后置处理器:在bean初始化前后,执行(刚创建完对象,还没属性赋值)初始化工作.
 5  //给容器中导入一些组件,导入了embeddedServletContainerCustomizerBeanPostProcessor
 6 @Import({EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar.class})
 7 public class EmbeddedServletContainerAutoConfiguration {
 8     @Configuration
 9     @ConditionalOnClass({Servlet.class, Tomcat.class})//判断当前Servlet中是否引入的Tomcat依赖
10     @ConditionalOnMissingBean(
11         value = {EmbeddedServletContainerFactory.class},
12         search = SearchStrategy.CURRENT
13     )//判断当前容器中,没有用户自定义的EmbeddedServletContainerFactory嵌入式的Servlet容器工厂,
14     //作用:创建嵌入式的servlet容器。
15     public static class EmbeddedTomcat {
16         public EmbeddedTomcat() {
17         }
18 
19         @Bean
20         public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
21             return new TomcatEmbeddedServletContainerFactory();
22         }
23     }
24 
25     @Configuration
26     @ConditionalOnClass({Servlet.class, Undertow.class, SslClientAuthMode.class})
27     @ConditionalOnMissingBean(
28         value = {EmbeddedServletContainerFactory.class},
29         search = SearchStrategy.CURRENT
30     )
31     public static class EmbeddedUndertow {
32         public EmbeddedUndertow() {
33         }
34 
35         @Bean
36         public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
37             return new UndertowEmbeddedServletContainerFactory();
38         }
39     }
40 
41     @Configuration
42     @ConditionalOnClass({Servlet.class, Server.class, Loader.class, WebAppContext.class})
43     @ConditionalOnMissingBean(
44         value = {EmbeddedServletContainerFactory.class},
45         search = SearchStrategy.CURRENT
46     )
47     public static class EmbeddedJetty {
48         public EmbeddedJetty() {
49         }
50 
51         @Bean
52         public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
53             return new JettyEmbeddedServletContainerFactory();
54         }
55     }
56 }

【2】嵌入式的容器工厂:EmbeddedServletContainerFactory ,用来创建嵌入式的 Servlet容器。

1 public interface EmbeddedServletContainerFactory {
2      //获取嵌入式的Servlet容器
3      EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... var1); 
4 }

  ☛ SpringBoot 再带了三种嵌入式的容器工厂,如下:

【3】EmbeddedServletContainer:嵌入式的容器,SpringBoot 为我们提供了三种不同的嵌入式容器,与工厂相互对应,如下:
 

【4】我们进入工厂类 TomcatEmbeddedServletContainerFactory发现,其实也是创建一个 Tomcat并配置其基本属性。

 1 public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
 2     //创建Tomcat
 3     Tomcat tomcat = new Tomcat();
 4 
 5     //配置Tomcat的基本环境
 6     File baseDir = this.baseDirectory != null?this.baseDirectory:this.createTempDir("tomcat");
 7     tomcat.setBaseDir(baseDir.getAbsolutePath());
 8     Connector connector = new Connector(this.protocol);
 9     tomcat.getService().addConnector(connector);
10     this.customizeConnector(connector);
11     tomcat.setConnector(connector);
12     tomcat.getHost().setAutoDeploy(false);
13     this.configureEngine(tomcat.getEngine());
14     Iterator var5 = this.additionalTomcatConnectors.iterator();
15 
16     while(var5.hasNext()) {
17         Connector additionalConnector = (Connector)var5.next();
18         tomcat.getService().addConnector(additionalConnector);
19     }
20 
21     this.prepareContext(tomcat.getHost(), initializers);
22     //将配置好的Tomcat传入,并启动Tomcat,Tomcat.start()
23     return this.getTomcatEmbeddedServletContainer(tomcat);
24 }

【5】用户自定义的 Servlet容器配置类和 SpringBoot 默认的 ServerProperties 配置类,都实现了EmbeddedServletContainerCustomizer 接口。到底是怎么实现的哪?其实是 SpringBoot 自动配置类中引入了后置处理器,如下:

//与用户自定义的Servlet容器实现的接口名很类似,有一定的命名规则。
embeddedServletContainerCustomizerBeanPostProcessor

  ☛ 进入后置处理器类中,重点看如下代码:

 1 //初始化之前执行
 2 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
 3     //如果当前初始化的是当前ConfigurableEmbeddedServletContainer类型的组件
 4     if(bean instanceof ConfigurableEmbeddedServletContainer) {
 5         this.postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer)bean);
 6     }
 7 
 8     return bean;
 9 }
10 
11 //上面的postProcessBeforeInitialization方法:
12 private void postProcessBeforeInitialization(ConfigurableEmbeddedServletContainer bean) {
13     Iterator var2 = this.getCustomizers().iterator();
14 
15     while(var2.hasNext()) {
16         //获取所有的定制器,调用每一个定制器的customize方法来给servlet属性赋值。
17         EmbeddedServletContainerCustomizer customizer = (EmbeddedServletContainerCustomizer)var2.next();
18         customizer.customize(bean);
19     }
20 
21 private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
22     if(this.customizers == null) {
23         //this.beanFactory.xx表示从容器中获取XXCustomizer自定义类型的组件
24         this.customizers = new ArrayList(this.beanFactory.getBeansOfType(EmbeddedServletContainerCustomizer.class, false, false).values());
25         Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
26         this.customizers = Collections.unmodifiableList(this.customizers);
27     }
28 
29     return this.customizers;
30 }

整理下步骤:【1】、SpringBoot根据pom.xml中导入的依赖,给容器中添加其对应的嵌入式的服务容器工厂类,例如默认的Tomcat工厂:EmbeddedServletContainerFactory【TomcatEmbeddedServletContainerFactory】
【2】、给容器中某个组件要创建对象就会触发后置处理器EmbeddedServletContainerCustomizerBeanPostProcessor,只要是嵌入式的Servlet容器工厂,后置处理器就会工作(默认的ServerProperties也是实现了此类接口的,所以肯定存在相关配置类)
【3】、后置处理器从容器中获取所有的EmbeddedServletContainerCustomizer,调用定制器的定制方法。

五、嵌入式Servlet容器启动原理


根据上述的流程,我们要研究Servlet容器的启动原理。其实就是研究什么时候创建嵌入式的容器工厂和何时获取嵌入式的容器并启动Tomcat。获取嵌入式的Servlet容器工厂的过程(在new TomcatEmbeddedServletContainerFactory()时打一个断电,查看过程):
【1】SpringBoot 应用启动运行 run() 方法。
【2】this.refreshContext(context) 方法:用来初始化 IOC容器,既创建 IOC容器对象并初始化IOC容器中的每一个组件。

 1 protected ConfigurableApplicationContext createApplicationContext() {
 2     Class<?> contextClass = this.applicationContextClass;
 3     if(contextClass == null) {
 4         try {
 5             //判断是不是web环境,是Web环境引入AnnotationConfigEmbeddedWebApplicationContext,否则引入AnnotationConfigApplicationContext
 6             contextClass = Class.forName(this.webEnvironment
 7             ?"org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext"
 8             :"org.springframework.context.annotation.AnnotationConfigApplicationContext");
 9         } catch (ClassNotFoundException var3) {
10             throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
11         }
12     }
13 
14     return (ConfigurableApplicationContext)BeanUtils.instantiate(contextClass);
15 }

【3】this.refresh(context):刷新刚才创建好的 IOC容器。
【4】this.onRefresh():web 的 IoC 容器重写了 onRefresh() 方法。

 1 protected void onRefresh() {
 2     super.onRefresh();
 3 
 4     try {
 5         //重点是创建了嵌入式的Servlet容器
 6         this.createEmbeddedServletContainer();
 7     } catch (Throwable var2) {
 8         throw new ApplicationContextException("Unable to start embedded container", var2);
 9     }
10 }

【5】this.createEmbeddedServletContainer():web的IOC容器会创建嵌入式的Servlet容器。

 1 private void createEmbeddedServletContainer() {
 2     EmbeddedServletContainer localContainer = this.embeddedServletContainer;
 3     ServletContext localServletContext = this.getServletContext();
 4     if(localContainer == null && localServletContext == null) {
 5         // 1、获取嵌入式的Servlet嵌入式的工厂
 6         EmbeddedServletContainerFactory containerFactory = this.getEmbeddedServletContainerFactory();
 7         this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(
 8                 new ServletContextInitializer[]{this.getSelfInitializer()});
 9     } 
10 }

【6】获取嵌入式工厂后,便可从容器中获取 EmbeddedServletContainerFactory 的组件tomcatEmbeddedServletContainerFactory 来创建 Tomcat 对象,后置处理器就会触发获取所有的定制器来确定 Servlet容器的相关配置。
【7】通过嵌入式工厂获取嵌入式容器,如下:

this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(
                    new ServletContextInitializer[]{this.getSelfInitializer()});

  ● 嵌入式的Servlet容器创建并启动对象:

1 public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
2     //创建对象
3     Tomcat tomcat = new Tomcat();
4 
5     //启动对象
6     this.tomcat.start();

  ● 先启动嵌入式的Servlet容器,再将IOC容器中剩下没有创建的对象进行初始化,如下:

1     this.onRefresh();
2     //启动完嵌入式容器后,后续还有其他对象的初始化工作
3     this.registerListeners();
4     this.finishBeanFactoryInitialization(beanFactory);
5     this.finishRefresh();
6 } catch (BeansException var9) {

六、使用外置的Servlet容器


嵌入式Servlet容器的缺点:默认不支持JSP、优化和定制比较复杂。
外置Servlet容器:安装外部的Tomcat,步骤如下:
1)、必须创建一个war项目,需要手动创建目录(利用Idea快速创建)如下:

2)、将嵌入式的Tomcat指定为provide(Idea创建完后,会自动帮我们完成,但我们需要了解)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

3)、需要编写一个SpringBootServletInitializer的子类,并调用configure方法:

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SpringBootWebApplication.class);
    }
}

4)、配置本地的Tomcat,并启动Tomcat即可。(此项目运行run()方法是不能启动项目的):需要设置名称和本地Tomcat的路径即可使用外部Servlet。   

七、外置服务器的使用原理


 ☞  jar包:执行SpringBoot主类的main方法,启动并初始化IOC容器且创建嵌入式的Servlet容器。
 ☞  war包:启动服务器后调用SpringBootServletInitializer中的configure()方法,加载我们的SpringBoot应用并启动。
Servlet3.0规则:1)、服务器启动后,会创建当前web应用中包含的每个jar内的ServletContainerInitializer实例。
   2)、ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下(javax.servlet.ServletContainerInitializer:内容就是ServletContainerInitializer的全类名)
  3)、可以使用@handlesTypes注解,在应用启动时加载我们需要的类。
流程:1)、启动Tomcat后,获取servlet.ServletContainerInitializer文件如下:其中的内容同下:

#文件中的内容
org.springframework.web.SpringServletContainerInitializer

  2)、进入SpringServletContainerInitializer发现此类将@HandlesTypes({WebApplicationInitializer.class})标注的所有这个类型的类都传入到onStartup方法中的Set<Class<?>>,并为这些类创建实例。

 1 @HandlesTypes({WebApplicationInitializer.class})
 2 public class SpringServletContainerInitializer implements ServletContainerInitializer {
 3     //onStartup方法,用来实例化感兴趣的对象
 4     public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
 5         if(webAppInitializerClasses != null) {
 6             var4 = webAppInitializerClasses.iterator();
 7 
 8             while(var4.hasNext()) {
 9                 Class<?> waiClass = (Class)var4.next();
10                 if(!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
11                     try {
12                             //实例化
13                        initializers.add((WebApplicationInitializer)waiClass.newInstance());
14                     } catch (Throwable var7) {
15                         throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
16                     }
17                 }
18             }
19         }

  3)、每一个WebApplicationInitializer都调用自己的onStartup()方法。

while(var4.hasNext()) {
      WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
      //onStartup()方法
      initializer.onStartup(servletContext);
 }

4)、WebApplicationInitializer只是一个借口,其实现类主要有以下三个:SpringBootServletInitalizer正是SpringBoot给我们创建好的启动类,会被创建对象,并启动自身的onStartup()方法。

5)、执行onStartup()方法时,会调用createRootApplicationContext()方法来创建容器

 1 public void onStartup(ServletContext servletContext) throws ServletException {
 2         this.logger = LogFactory.getLog(this.getClass());
 3         //创建容器
 4         WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
 5         if(rootAppContext != null) {
 6             servletContext.addListener(new ContextLoaderListener(rootAppContext) {
 7                 public void contextInitialized(ServletContextEvent event) {
 8                 }
 9             });
10 
11     //容器的具体调用实现
12     protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
13         //创建Spring应用的构建器
14         SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
15         //设置主类
16         builder.main(this.getClass());
17         //创建一些环境
18         ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext);
19         if(parent != null) {
20             this.logger.info("Root context already created (using as parent).");
21             servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, (Object)null);
22             builder.initializers(new ApplicationContextInitializer[]{new ParentContextApplicationContextInitializer(parent)});
23         }
24 
25         builder.initializers(new ApplicationContextInitializer[]{new ServletContextApplicationContextInitializer(servletContext)});
26         builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
27 
28         //重要:子类中重写了此方法,子类出入了应用的主程序类
29         builder = this.configure(builder);
30         builder.listeners(new ApplicationListener[]{new SpringBootServletInitializer.WebEnvironmentPropertySourceInitializer(servletContext, null)});
31         //使用build()创建一个Spring应用
32         SpringApplication application = builder.build();
33         if(application.getAllSources().isEmpty() && AnnotationUtils.findAnnotation(this.getClass(), Configuration.class) != null) {
34             application.addPrimarySources(Collections.singleton(this.getClass()));
35         }
36 
37         Assert.state(!application.getAllSources().isEmpty(), "No SpringApplication sources have been defined. Either override the configure method or add an @Configuration annotation");
38         if(this.registerErrorPageFilter) {
39             application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
40         }
41         //启动应用
42         return this.run(application);
43     }

6)、执行应用的run()方法,来启动Spring应用并创建IOC容器。

 1 public ConfigurableApplicationContext run(String... args) {
 2     StopWatch stopWatch = new StopWatch();
 3     stopWatch.start();
 4     ConfigurableApplicationContext context = null;
 5     Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
 6     this.configureHeadlessProperty();
 7     SpringApplicationRunListeners listeners = this.getRunListeners(args);
 8     listeners.starting();
 9 
10     Collection exceptionReporters;
11     try {
12         ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
13         ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
14         this.configureIgnoreBeanInfo(environment);
15         Banner printedBanner = this.printBanner(environment);
16         context = this.createApplicationContext();
17         exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, new Object[]{context});
18         this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
19 
20         //刷新IOC容器
21         this.refreshContext(context);
22         this.afterRefresh(context, applicationArguments);
23         stopWatch.stop();
24         if(this.logStartupInfo) {
25             (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
26         }
27 
28         listeners.started(context);
29         this.callRunners(context, applicationArguments);
30     } catch (Throwable var10) {
31         this.handleRunFailure(context, var10, exceptionReporters, listeners);
32         throw new IllegalStateException(var10);
33     }
34 
35     try {
36         listeners.running(context);
37         return context;
38     } catch (Throwable var9) {
39         this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
40         throw new IllegalStateException(var9);
41     }
42 }


 ----关注公众号,获取更多内容----

posted @ 2020-11-22 00:40  Java程序员进阶  阅读(97)  评论(0编辑  收藏  举报