Jsp/Servlet整合Spring原理及源码分析

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】

表现层和业务层整合:

  1. Jsp/Servlet整合spring

  2. Spring MVC整合SPring;

  3. Struts2整合Spring;

本文主要介绍Jsp/Servlet整合Spring原理及源码分析。

一、整合过程

Spring&WEB整合,主要介绍的是Jsp/Servlet容器和Spring整合的过程,当然,这个过程是Spring MVC或Strugs2整合Spring的基础。

Spring和Jsp/Servlet整合操作很简单,使用也很简单,按部就班花不到2分钟就搞定了,本节只讲操作不讲原理,更多细节、原理及源码分析后续过程陆续涉及。

  1. 导入必须的jar包,本例spring-web-x.x.x.RELEASE.jar;

  2. 配置web.xml,本例示例如下;

[html]
view plain
copy
print
?
20191102100573\_1.png
20191102100573\_2.png

  1. <?**xmlversion=“1.0”encoding=“UTF-8”?>**
  2. <**web-appxmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”xmlns=“http://java.sun.com/xml/ns/javaee”xsi:schemaLocation=“http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app\_3\_0.xsd”id=“WebApp\_ID”version=“3.0>**
  3. <**display-name**>spring11</**display-name**>
  4. <**context-param**>
  5. <**param-name**>contextConfigLocation</**param-name**>
  6. <**param-value**>applicationContext.xml</**param-value**>
  7. </**context-param**>
  8. <**listener**>
  9. <**listener-class**>org.springframework.web.context.ContextLoaderListener</**listener-class**>
  10. </**listener**>
  11. <**welcome-file-list**>
  12. <**welcome-file**>index.jsp</**welcome-file**>
  13. </**welcome-file-list**>
  14. </**web-app**>

    只需要配置context-param和listener就够了。配置项的内容含义:ServletContext.setAttribute(“contextConfigLocation”, “applicationCOntext.xml”),而中配置的侦听器则来自于第一步中导入的spring-web-x.x.x.RELEASE.jar包。

    1. 就这么两步,Spring和Jsp/Servlet就已经整合好了,验证一下,启动tomcat不报错就可以了。

二、整合原理

原理:让Spring容器随着tomcat容器ServletContext的启动而启动,并且在初始化完成后放到整个应用都可以访问的范围

回想一下,在非web环境下,使用Spring是需要我们手动把ApplicationContext对象创建出来使用。在web环境下,使用Spring也是需要把ApplicationContext对象创建出来,不过这个步骤大可交给服务器来做,你不必自己再去写个单例类,web环境中单例对象多了去了,servlet是单例的,filter是单例的,listener也是单例,这三个,随便找一个在初始化的时候把ApplicationContext对象创建出来,然后放到整个应用都可以访问的ServletContext容器中就可以了。

总结一下:

  1. 让ApplicationContext随着服务器的启动而启动,可以借助与Servlet/Filter/Listener任何一个;

  2. 把创建好的ApplicationContext放到ServletContext中,整个应用范围,想怎访问就怎么访问;

    整合原理就这么多,你自己分分钟都可以整一个出来,至于代码健不健壮再说,先用再调嘛。但是大可不必这么麻烦,Spring已经把这一切做好了,你只要拿过来用就可以了。

    回顾一下第一节的内容,导入一个jar包spring-web-x.x.x.RELEASE.jar,配置了一个侦听器listener,没错,上面的原理都被这个jar包中的这个侦听器给实现了。早期的Spring整合web,支持servlet和listener,不过在Spring 3.x的时候,Spring官方访问已经明确不支持Servlet方式并且已经将相关源码移出web的jar包,所以现在Spring整合WEB,就一条路,listener。

    有了ApplicationContext对象,IOC/DI、AOP、Spring JDBC以及事务、国际化ResourceMessage、Spring事件机制、FactoryBean、Spring JNDI等等,想怎么用怎么用。

    再分析源码之前,介绍一个工具,org.springframework.web.context.support.WebApplicationContextUtils,这是Spring官方提供的一个web环境下便捷从ServletContext中获取ApplicationContext的工具类,使用示例如下所示。

[html]
view plain
copy
print
?
20191102100573\_3.png
20191102100573\_4.png

  1. WebApplicationContext applicationContext = WebApplicationContextUtils.
  2. getWebApplicationContext(this.getServletContext());

    你不用再去记忆ApplicationContext放入ServletContext中冗长的key,这个方法的源码也很简单,不过Spring写的又臭又长,原理如下所示。

[html]
view plain
copy
print
?
20191102100573\_5.png
20191102100573\_6.png

  1. /**
  2. * String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE =
  3. * WebApplicationContext.class.getName() + “.ROOT”;
  4. */
  5. WebApplicationContext context = (WebApplicationContext) this.getServletContext()
  6. .getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

    好了,有了以上的铺垫,并且你对Spring熟悉,那么在web环境下使用Spring对你来说已经毫无压力。

三、源码分析

1. contextInitialized

ServletContextListener侦听器是Jsp/Servlet规范中八大侦听器之一,用来监听ServletContext的创建和销毁。第一节中配置的侦听器ContextLoaderListener就实现了该接口。

[html]
view plain
copy
print
?
20191102100573\_7.png
20191102100573\_8.png

  1. package org.springframework.web.context;
  2. import javax.servlet.ServletContextEvent;
  3. import javax.servlet.ServletContextListener;
  4. public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
  5. public ContextLoaderListener() {}
  6. public ContextLoaderListener(WebApplicationContext context) {
  7. super(context);
  8. }
  9. /**
  10. * 该方法实现与ServletContextListener接口
  11. * 当ServletContext被创建之后,服务器会自动调用该方法
  12. * 而该方法就是Spring整合WEB的入口
  13. */
  14. public void contextInitialized(ServletContextEvent event) {
  15. initWebApplicationContext(event.getServletContext());
  16. }
  17. /**
  18. * 该方法实现与ServletContextListener接口
  19. * 当ServletContext销毁的时候,服务器会自动调用该方法
  20. * 最后一节详解该方法
  21. */
  22. public void contextDestroyed(ServletContextEvent event) {
  23. closeWebApplicationContext(event.getServletContext());
  24. ContextCleanupListener.cleanupAttributes(event.getServletContext());
  25. }
  26. }

    随着服务器的启动,ContextLoaderListener侦听器对象会被实例化并且调用contextInitialized(ServletContextEvent event)方法,再在该方法中调用包裹了初始化所有逻辑的initWebApplicationContext(event.getServletContext())方法,深入一下这个方法。

1.1 initWebApplicationContext

这个方法可大发了,ApplicationContext就在这里被创建完成,并且调用refresh()方法。refresh()方法对于Spring有多重要我就不说了,BeanFactory的创建以及配置文件applicationContext加载解析成BeanDefinition以及单例Bean的实例化和缓存、完成国际化机制的初始化、完成Spring事件机制的初始化、发布Spring容器刷新事件等等,这个方法成功调用返回标志着Spring容器启动成功,可以对外提供服务。

简单总结一下该方法的内容:

  1. 创建WebApplicationContext对象;

  2. 将配置的contextConfigLocation参数(即Spring配置文件applicationContext.xml)传入ApplicationContext对象并调用refresh()方法刷新Spring容器,完成Spring容器的创建和启动;

  3. 将创建好的ApplicationContext放入ServletContext中;

  4. 将ApplicationContext和当前线程的类加载器作为KV存入到ContextLoaderListener中;

[html]
view plain
copy
print
?
20191102100573\_9.png
20191102100573\_10.png

  1. public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
  2. //如果ServletContext中已经放了一个ApplicationContext,抛异常
  3. if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
  4. throw new IllegalStateException(
  5. “Cannot initialize context because there is already a root application context present – ” +
  6. “check whether you have multiple ContextLoader* definitions in your web.xml!”);
  7. }
  8. Log logger = LogFactory.getLog(ContextLoader.class);
  9. servletContext.log(“Initializing Spring root WebApplicationContext”);
  10. if (logger.isInfoEnabled()) {
  11. logger.info(“Root WebApplicationContext: initialization started”);
  12. }
  13. long startTime = System.currentTimeMillis();
  14. try {
  15. //1. 创建WebApplicationContext,这是一个很重要的方法,可以控制初始化的ApplicationContext类型,下文详解
  16. if (this.context == null) {
  17. this.context = createWebApplicationContext(servletContext);
  18. }
  19. //将上文创建的WebApplicationContext强转成子类ConfigurableWebApplicationContext
  20. if (this.context instanceof ConfigurableWebApplicationContext) {
  21. ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
  22. if (!cwac.isActive()) {
  23. // The context has not yet been refreshed –> provide services such as
  24. // setting the parent context, setting the application context id, etc
  25. if (cwac.getParent() == null) {
  26. // The context instance was injected without an explicit parent –>
  27. // determine parent for root web application context, if any.
  28. ApplicationContext parent = loadParentContext(servletContext);
  29. cwac.setParent(parent);
  30. }
  31. //2. 整个Spring整合Web的灵魂所在,在里面调用了refresh()方法,完成Spring容器的启动,下文详解详解详解
  32. configureAndRefreshWebApplicationContext(cwac, servletContext);
  33. }
  34. }
  35. //3. 将ApplicationContext放入ServletContext中
  36. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
  37. //4. 将ApplicationContext根据类加载器放入ContextLoaderListener的ConcurrentHashMap的逻辑
  38. ClassLoader ccl = Thread.currentThread().getContextClassLoader();
  39. if (ccl == ContextLoader.class.getClassLoader()) {
  40. currentContext = this.context;
  41. }
  42. else if (ccl != null) {
  43. currentContextPerThread.put(ccl, this.context);
  44. }
  45. if (logger.isDebugEnabled()) {
  46. logger.debug(“Published root WebApplicationContext as ServletContext attribute with name [” +
  47. WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + “]”);
  48. }
  49. if (logger.isInfoEnabled()) {
  50. long elapsedTime = System.currentTimeMillis() – startTime;
  51. logger.info(“Root WebApplicationContext: initialization completed in ” + elapsedTime + ” ms”);
  52. }
  53. //refresh()方法调用完成,标志着Spring对外可以提供服务
  54. return this.context;
  55. } catch (RuntimeException ex) {
  56. logger.error(“Context initialization failed”, ex);
  57. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
  58. throw ex;
  59. } catch (Error err) {
  60. logger.error(“Context initialization failed”, err);
  61. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
  62. throw err;
  63. }
  64. }

    上面都是些包来包去的方法,下面才是精华。上面错过就算了,下面错过就亏了。

1.1.1 createWebApplicationContext

在调用createWebApplicationContext(servletContext)方法的时候,会判断ContextLoaderListener的this.context是否为null,不为null才创建新的对象,之所以这样判断,是因为ContextLoaderListener的构造方法有两种,含参和不含参,另外,如果不是在启动服务时触发refresh方法,而是在运行中手动触发refresh方法的话,就不必在重新创建ApplicationContext对象,而只需要把这个对象后面的逻辑刷新一下就可以了。

[html]
view plain
copy
print
?
20191102100573\_11.png
20191102100573\_12.png

  1. protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
  2. //1. 获得ApplicationContext类型的字节码,这里可以自定义ApplicationContext的类型
  3. //默认是XmlApplicationContext
  4. //在web.xml中通过设置参数,你可以在web中使用Spring 3.0的AnnotationConfigWebApplicationContext
  5. //或者是其他任意类型的ApplicationContext
  6. Class<?**>**contextClass = determineContextClass(sc);
  7. if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {// 这个判断决定了自定义ApplicationContext的范围,下文会有一个自定义的举例
  8. throw new ApplicationContextException(“Custom context class [” + contextClass.getName() +
  9. “] is not of type [” + ConfigurableWebApplicationContext.class.getName() + “]”);
  10. }
  11. //2. 利用反射来实例化ApplicationContext
  12. //这里的逻辑是通过反射获取默认构造函数,然后newInstance();
  13. return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
  14. }
1.1.1.1 determineContextClass

[html]
view plain
copy
print
?
20191102100573\_13.png
20191102100573\_14.png

  1. // 默认WebApplicationContext初始化对象的配置文件
  2. private static final String DEFAULT_STRATEGIES_PATH = “ContextLoader.properties”;
  3. // 配置文件工具
  4. private static final Properties defaultStrategies;
  5. protected Class<?**>** determineContextClass(ServletContext servletContext) {
  6. // 从web.xml中获取配置的ApplicationContext类的全量路径
  7. // public static final String CONTEXT_CLASS_PARAM = “contextClass”;
  8. String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
  9. if (contextClassName != null) {
  10. try {
  11. return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
  12. }
  13. catch (ClassNotFoundException ex) {
  14. throw new ApplicationContextException(
  15. “Failed to load custom context class [” + contextClassName + “]”, ex);
  16. }
  17. } else {
  18. // 如果没有配置contextClass,那么默认就创建XmlWebApplicationContext
  19. // 这个参数在spring-web-x.x.x.RELEASE.jar的配置文件中
  20. // /org/springframework/web/context/ContextLoader.properties
  21. contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
  22. try {
  23. return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
  24. }
  25. catch (ClassNotFoundException ex) {
  26. throw new ApplicationContextException(
  27. “Failed to load default context class [” + contextClassName + “]”, ex);
  28. }
  29. }
  30. }

    先看ContextLoader.properties配置文件中的内容。

[html]
view plain
copy
print
?
20191102100573\_15.png
20191102100573\_16.png

  1. # Default WebApplicationContext implementation class for ContextLoader.
  2. # Used as fallback when no explicit context implementation has been specified as context-param.
  3. # Not meant to be customized by application developers.
  4. org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

    如果没有在web.xml中没有配置contextClass,那么默认加载的就是XmlWebApplicationContext,如果配置了,那么就加载配置的ApplicationContext类型,但是有一个条件,即该类必须是ConfigurableWebApplicationContext或其子类。一个简单的配置例子,web容器启动的时候,加载AnnotationConfigWebApplicationContext类。

[html]
view plain
copy
print
?
20191102100573\_17.png
20191102100573\_18.png

  1. <?**xmlversion=“1.0”encoding=“UTF-8”?>**
  2. <**web-appxmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”xmlns=“http://java.sun.com/xml/ns/javaee”xsi:schemaLocation=“http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app\_3\_0.xsd”id=“WebApp\_ID”version=“3.0>**
  3. <**display-name**>spring11</**display-name**>
  4. <**context-param**>
  5. <**param-name**>contextClass</**param-name**>
  6. <**param-value**>org.springframework.context.annotation.AnnotationConfigWebApplicationContext</**param-value**>
  7. </**context-param**>
  8. <**context-param**>
  9. <**param-name**>contextConfigLocation</**param-name**>
  10. <!– 这里不再加载applicationContext.xml,而是加载自定义的配置类 –>
  11. <**param-value**>cn.wxy.configuration.AnnoBeanConfiguation</**param-value**>
  12. </**context-param**>
  13. <**listener**>
  14. <**listener-class**>org.springframework.web.context.ContextLoaderListener</**listener-class**>
  15. </**listener**>
  16. <**welcome-file-list**>
  17. <**welcome-file**>index.jsp</**welcome-file**>
  18. </**welcome-file-list**>
  19. </**web-app**>
1.1.1.2 BeanUtils.instantiateClass(contextClass)

在成功加载ApplicationContext的字节码并获得Class对象之后,利用反射获取ApplicationContext实例,源码很简单,核心就就一句话clazz.getDeclaredConstructor().newInstance(),反射获取默认构造器,然后实例化。

1.1.2 configureAndRefreshWebApplicationContext

[html]
view plain
copy
print
?
20191102100573\_19.png
20191102100573\_20.png

  1. protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
  2. // 从web.xml中获取context-param配置参数contextId,没有就采用默认
  3. // contextId这个不知道用来干什么,不涉及主要逻辑,此处不予关注
  4. if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
  5. // The application context id is still set to its original default value
  6. // –> assign a more useful id based on available information
  7. String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
  8. if (idParam != null) {
  9. wac.setId(idParam);
  10. }
  11. else {
  12. // Generate default id…
  13. wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
  14. ObjectUtils.getDisplayString(sc.getContextPath()));
  15. }
  16. }
  17. // 从context-param中获取Spring配置文件applicationContext.xml路径
  18. // 参数contextConfigLocation为第一节中web.xml中配置的参数
  19. wac.setServletContext(sc);
  20. String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
  21. if (configLocationParam != null) {
  22. wac.setConfigLocation(configLocationParam);
  23. }
  24. //初始化web环境
  25. // The wac environment’s #initPropertySources will be called in any case when the context
  26. // is refreshed; do it eagerly here to ensure servlet property sources are in place for
  27. // use in any post-processing or initialization that occurs below prior to #refresh
  28. ConfigurableEnvironment env = wac.getEnvironment();
  29. if (env instanceof ConfigurableWebEnvironment) {
  30. ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
  31. }
  32. customizeContext(sc, wac);
  33. // 刷新Spring容器
  34. // 该方法调用完成,标志着Spring容器可以对外提供服务
  35. wac.refresh();
  36. }

这个方法主要是将Spring配置文件路径关联到ApplicationContext,并没有进行解析,解析过程在refresh中获取BeanFactory的时候完成。

最重要的方法就是refresh()方法,它包含了Spring所有的启动逻辑,这里只是简单的列了一下,并没有深入,深入就太多了,写不完,可以参看《Spring:源码解读Spring IOC原理》

[html]
view plain
copy
print
?
20191102100573\_21.png
20191102100573\_22.png

  1. public void refresh() throws BeansException, IllegalStateException {
  2. synchronized (this.startupShutdownMonitor) {
  3. //调用容器准备刷新的方法,获取容器的当时时间,同时给容器设置同步标识
  4. prepareRefresh();
  5. //告诉子类启动refreshBeanFactory()方法,Bean定义资源文件的载入从
  6. //子类的refreshBeanFactory()方法启动
  7. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  8. //为BeanFactory配置容器特性,例如类加载器、事件处理器等
  9. prepareBeanFactory(beanFactory);
  10. try {
  11. //为容器的某些子类指定特殊的BeanPost事件处理器
  12. postProcessBeanFactory(beanFactory);
  13. //调用所有注册的BeanFactoryPostProcessor的Bean
  14. invokeBeanFactoryPostProcessors(beanFactory);
  15. //为BeanFactory注册BeanPost事件处理器.
  16. //BeanPostProcessor是Bean后置处理器,用于监听容器触发的事件
  17. registerBeanPostProcessors(beanFactory);
  18. //初始化信息源,和国际化相关.
  19. initMessageSource();
  20. //初始化容器事件传播器.
  21. initApplicationEventMulticaster();
  22. //调用子类的某些特殊Bean初始化方法
  23. onRefresh();
  24. //为事件传播器注册事件监听器.
  25. registerListeners();
  26. //初始化所有剩余的单态Bean.
  27. finishBeanFactoryInitialization(beanFactory);
  28. //初始化容器的生命周期事件处理器,并发布容器的生命周期事件
  29. finishRefresh();
  30. }
  31. catch (BeansException ex) {
  32. //销毁以创建的单态Bean
  33. destroyBeans();
  34. //取消refresh操作,重置容器的同步标识.
  35. cancelRefresh(ex);
  36. throw ex;
  37. }
  38. }
  39. }

1.1.3 收尾工作

  1. 接下来将创建好的XmlWebApplicationContext放入ServletContext中;

  2. 以当前线程的类加载器为key,XmlWebApplicationContext为value,放入ContextLoaderListener的HashMap中;

  3. 将XmlApplicationContext和ContextLoaderListener的this.currentContext绑定;

2. contextDestroyed

[html]
view plain
copy
print
?
20191102100573\_23.png
20191102100573\_24.png

  1. public void contextDestroyed(ServletContextEvent event) {
  2. // 关闭WebApplicationContext
  3. closeWebApplicationContext(event.getServletContext());
  4. // 将ServletContext中Spring相关的对象清理
  5. ContextCleanupListener.cleanupAttributes(event.getServletContext());
  6. }

    清理逻辑就很简单了,源码简单注释如下。

[html]
view plain
copy
print
?
20191102100573\_25.png
20191102100573\_26.png

  1. public void closeWebApplicationContext(ServletContext servletContext) {
  2. servletContext.log(“Closing Spring root WebApplicationContext”);
  3. try {
  4. // 关闭Spring容器,此操作会调用Bean生命周期中destory-method和注解preDestory清理资源:销毁单例bean、销毁BeanFacotry,删除JVM关闭钩子;
  5. if (this.context instanceof ConfigurableWebApplicationContext) {
  6. ((ConfigurableWebApplicationContext) this.context).close();
  7. }
  8. } finally {
  9. // 清理ContextLoaderListener
  10. ClassLoader ccl = Thread.currentThread().getContextClassLoader();
  11. if (ccl == ContextLoader.class.getClassLoader()) {
  12. currentContext = null;
  13. } else if (ccl != null) {
  14. currentContextPerThread.remove(ccl);
  15. }
  16. // 清理放入ServletContext中的ApplicationContext
  17. servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
  18. if (this.parentContextRef != null) {
  19. this.parentContextRef.release();
  20. }
  21. }
  22. }

[html]
view plain
copy
print
?
20191102100573\_27.png
20191102100573\_28.png

  1. // 把Servlet中Spring相关的内容清掉
  2. static void cleanupAttributes(ServletContext sc) {
  3. Enumeration<**String**>attrNames = sc.getAttributeNames();
  4. while (attrNames.hasMoreElements()) {
  5. String attrName = attrNames.nextElement();
  6. if (attrName.startsWith(“org.springframework.”)) {
  7. Object attrValue = sc.getAttribute(attrName);
  8. if (attrValue instanceof DisposableBean) {
  9. try {
  10. ((DisposableBean) attrValue).destroy();
  11. }
  12. catch (Throwable ex) {
  13. logger.error(“Couldn’t invoke destroy method of attribute with name ‘” + attrName + “‘”, ex);
  14. }
  15. }
  16. }
  17. }
  18. }

    至此,结束,谢谢观赏!

附注:

本文如有错漏,烦请不吝指正,谢谢!


来源:http://ddrv.cn

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » Jsp/Servlet整合Spring原理及源码分析

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏