深入Jetty源码之Servlet框架及实现(ServletContext)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
日志服务 SLS,月写入数据量 50GB 1个月
简介:

概述

Servlet是Server Applet的缩写,即在服务器端运行的小程序,而Servlet框架则是对HTTP服务器(Servlet Container)和用户小程序中间层的标准化和抽象。这一层抽象隔离了HTTP服务器的实现细节,而Servlet规范定义了各个类的行为,从而保证了这些“服务器端运行的小程序”对服务器实现的无关性(即提升了其可移植性)。
在Servlet规范有以下几个核心类(接口):
ServletContext:定义了一些可以和Servlet Container交互的方法。
Registration:实现Filter和Servlet的动态注册。
ServletRequest(HttpServletRequest):对HTTP请求消息的封装。
ServletResponse(HttpServletResponse):对HTTP响应消息的封装。
RequestDispatcher:将当前请求分发给另一个URL,甚至ServletContext以实现进一步的处理。
Servlet(HttpServlet):所有“服务器小程序”要实现了接口,这些“服务器小程序”重写doGet、doPost、doPut、doHead、doDelete、doOption、doTrace等方法(HttpServlet)以实现响应请求的相关逻辑。
Filter(FilterChain):在进入Servlet前以及出Servlet以后添加一些用户自定义的逻辑,以实现一些横切面相关的功能,如用户验证、日志打印等功能。
AsyncContext:实现异步请求处理。

ServletContext

Context在这里是指一个Web Application的上下文(Web Application是一个Server子URL下的Servlet和资源的集合),即它包含了这个Web Application级别的信息,如当前Web Application对应的根路径、使用的Servlet版本、使用的ClassLoader等,在一个JVM中的一个Web Application只能有一个Context(一个JVM可以包含多个Web Application,它们包含不同的根路径,即不同的Context路径,Context路径可以是空("/")即这个JVM只能包含一个Web Application)。ServletContext则是对这个Context的抽象,它还定义了一些和Servlet Container交互的方法,如获取文件的MINE Type、Dispatch请求到另一个URL或Context、将日志写入文件、根据提供的路径获取Resource实例、向这个ServletContext注册并获取Servlet或Filter、向这个ServletContext注册并获取Attribute或初始参数、向这个ServletContext注册或获取相关Listener等。对Distributed的Web Application来说,每个JVM下的Web Application都有一个独立的ServletContext,因而ServletContext不可以作为全局信息存储的地方,因而它并没有分布式信息同步的功能,即它只是本地的ServletContext。在Servlet中,使用ServletConfig实例可以获取ServletContext实例。
类图如下:

ServletContext的接口定义如下:
public   interface  ServletContext {
     // Servlet Container为当前Web Application设置临时目录,并将该临时目录的值存储到当前ServletContext的属性中使用的属性名。
    
// Jetty使用WebInfConfiguration(在preConfig ure()方法中)来设置该值,设置temp目录的规则:
     // 1. 如果存在WEB-INF/work目录,则temp目录的值为:WEB-INF/work/Jetty_<host>_<port>__<resourceBase>_<context>_<virtualhost+base36_hashcode_of_whole_string>
     // 2. 如果"javax.servlet.context.tempdir"已经在外部被设置,并且该目录存在且可写,则temp目录直接设置为该目录实例。
     // 3. 如果系统变量中"jetty.home"目录下存在"work"目录且可写,则设置temp目录的值为:${jetty.home}/work/ Jetty_<host>_<port>__<resourceBase>_<context>_<virtualhost+base36_hashcode_of_whole_string>
     // 4. 如果存在"org.eclipse.jetty.webapp.basetempdir"的属性,且该目录存在并可写,设置temp目录为:${ org.eclipse.jetty.webapp.basetempdir }/ Jetty_<host>_<port>__<resourceBase>_<context>_<virtualhost+base36_hashcode_of_whole_string>
     // 5. 如果以上条件都不成立,则设置temp目录为: ${ java.io.tmpdir }/ Jetty_<host>_<port>__<resourceBase>_<context>_<virtualhost+base36_hashcode_of_whole_string>,且删除已存在临时目录。
     // 注:对temp目录的父目录不是work,会注册在JVM退出时删除该temp目录,并在temp目录下创建.active目录。

     public   static   final  String TEMPDIR = "javax.servlet.context.tempdir";
     / / Servlet 3.0中新引入的特性,即可以在WEB-INF/lib下的jar包中定义/META-INF/web-fragment配置响应的Servlet等。
     / / 如果在web.xml文件中定义了absolute-ordering,或者在jar包中存在/META-INF/web-fragment.xml文件,且定义了ordering,
     / / 则该属性的值即为根据规范中定义的规则计算出来的读取jar包中web-fragment.xml文件的jar包名顺序,它时一个List<String>类型,包含jar包名字。
     / / 在Jetty中使用Ordering来表示这种顺序,它有两个实现类:AbsoluteOrdering和RelativeOrdering用来分别表示在web.xml和web-fragment.xml中定义的absolute-ordering和ordering定义,
     / 并且将最终的解析结果汇总到Metadata类中,并根据规范中定义的规则以及metadata-complete的定义来计算实际的解析顺序
     / 而对这两种配置文件的解析由WebDescriptor和FragmentDescriptor来实现,它们都包含了metadata-complete解析,而真正的解析入口在WebXmlConfiguration和FragmentConfiguration中。
     / / 该规范的定义参考: https://blogs.oracle.com/swchan/entry/servlet_3_0_web_fragment
     public  static  final String ORDERED_LIBS = "javax.servlet.context.orderedLibs";

     // 返回当前Web Application的Context Path,即当前Web Application的根路径,Servlet Container根据该路径以及一个Request URL的匹配情况来选择一个Request应该交给那个Web Application处理该请求。
     // Context Path以"/"开始,但是不能以"/"结尾,对默认的根Context,它返回"",而不是"/"。该值在配置Jetty的ContextHandler中设置。
     // 有些Servlet Container支持多个Context Path指向同一个Context,此时可以使用HttpServletRequest中的getContextPath()来获取该Request实际对应的Context Path,此时这两个Context Path的值可能会不同,但是ServletContext中返回的Context Path值是主要的值。另外Jetty也不支持这种特性。
     public String getContextPath();

     // 通过给定一个Context Path以在当前 Servlet Container中找到其对应的ServletContext实例。
     // 可以通过该方法获取Servlet Container中定义的另一个Web Application的ServletContext实例,并获得其RequestDispatcher,并将当前请求Dispatch到那个Web Application中做进一步的处理。这里的uripath必须以"/"开头,且其路径相对于当前Server的根路径。出于安全考虑,该方法可能会返回null。
     // 在Jetty的实现中,这里uripath可以是一个具体的路径,并且支持查找最准确的匹配。如:对uripath为/foo/goo/abc.html,在Server中由以下Context Path定义:"/", "/foo", "/foo/goo",则最终查找到的ServletContext为"/foo/goo"作为Context Path对应的ServletContext实例。
     public ServletContext getContext(String uripath);
    
     / /返回Servlet规范的主版本,如3
     public  int getMajorVersion();
     / / 返回Servlet规范的次版本,如0
     public  int getMinorVersion();
     / / 返回当前Web Application基于的Servlet规范的主版本,如在web.xml文件中定义的version(<web-app version="..." ...>...</web-app>,即Jetty中的实现)
     public  int getEffectiveMajorVersion();
     / 返回当前Web Application基于的Servlet规范的次版本,如在web.xml文件中定义的version(<web-app version="..." ...>...</web-app>,即Jetty中的实现)
     public  int getEffectiveMinorVersion();


     // 返回给定file的MIME type,返回null如果无法计算出其MIME type。这个映射关系由Servlet Container定义或在web.xml文件中定义(mime-mapping, extension, mine-type)。
     // 常见的MIME type有:text/html, image/gif等。Jetty使用MimeTypes类来封装所有和MIME type相关的操作,MimeTypes类中定义了所有默认支持的MIME type以及编码类型,
     // 并且默认从org/eclipse/jetty/http/mime.properties文件中加载默认的映射,如css=text/css, doc=application/msword等,使用addMimeMapping()方法向该类注册web.xml中定义的文件名扩展名到MIME type的映射。
     //  而从org/eclipse/jetty/http/encoding.properties文件中加载MIME type的默认编码类型,如text/xml=UTF-8等。
     // 在使用文件名查找MIME type时,根据文件名的扩展名查找已注册或默认注册的MIME type。用户自定义的映射优先。用户定义的MIME type映射支持extension为"*",表示任意扩展名。
     public String getMimeType(String file);
    
     / / 对于给定目录,返回该目录下所有的资源以及目录。path必须以"/"开头,如果它不是指向一个目录,则返回空的Set。所有返回的路径都相对当前Web Application的根目录,
     / 或对于/WEB-INF/lib中jar包中的/META-INF/resources/目录,如一个Web Application包含以下资源:/catalog/offers/music.html, /WEB-INF/web.xml, 
     / / 以及 /WEB-INF/lib/catalog.jar!/META-INF/resources/catalog/moreOffers/books.html,则getResourcePaths("/catalog")返回{"/catalog/offers/", /catalog/moreOffers/"}
     / / Jetty的实现中,在MetaInfConfiguration中,它会扫描WEB-INF/lib目录下所有的jar包,如果发现在某个jar包中存在META-INF/resources/目录,
     / 就会将该目录资源作为baseResource在WebInfConfiguration中注册到ContextHandler(WebAppContext)中。从而实现jar包中的META-INF/resources/目录作为根目录的查找。  
     public Set<String> getResourcePaths(String path);
    
     / / 返回给定path的URL,path必须以"/"开头,它相对当前Web Application的根目录或相对/WEB-INF/lib中jar包中的/META-INF/resources/目录。
     / 其中查找顺序前者优先于后者,但是在/WEB-INF/lib/目录下的jar包的查找顺序未定义。该方法不同于Class.getResource()在于它不使用ClassLoader,如果没有找到给定资源,返回null。
     / / 在WebAppContext实现中,它还支持Alias查找,并且如果其extractWAR的变量为false,给定的资源在WAR包中,则该URL返回WAR包中的URL。    
     public URL getResource(String path)  throws MalformedURLException;
    
     / / 参考getResource(path)的查找实现,如果其返回的URL为null,则该方法返回null,否则返回URL对应的InputStream。
     public InputStream getResourceAsStream(String path);

     // 创建一个RequestDispatcher用于将一个Request、Response分发到path对应的URL中,这里path必须以"/"开头,且它相对于当前Context Path。如果无法创建RequestDispatcher,返回null。
     //  path可以包含query数据用于传递参数:uriInContext?param1=abc&param2=123....该方法可以和getContext(uripath)一起使用,以将当前请求分发到另一个Web Application中。
     // 该方法的另一种用法是先有一个Servlet或Filter处理基本的逻辑,然后使用这个RequestDispatcher将当前请求forward到另一个URL中或include一个JSP文件 生成响应页面,如果在处理过程中出错,则将其当前请求分发到错误处理的流程中。
     // RequestDispatcher支持两种类型的分发:forward和include,唯一的区别是include只可以改变Response的内容,不可以改变其Header信息,forward则没有这种限制。
     public RequestDispatcher getRequestDispatcher(String path);

     / / 创建一个RequestDispatcher用于将一个Request、Response分发到name对应的Servlet(JSP)中。如果没能找到响应的Servlet,返回null。
     public RequestDispatcher getNamedDispatcher(String name);    

     / / 将msg打印到Servlet对应的log文件中,在Jetty中,使用INFO级别打印,logger名称为web.xml定义的display-name,或者context path。
     / Jetty默认使用SLF4J作为日志打印框架,可以使用"org.eclipse.jetty.util.log.class"系统属性改变其日志打印框架。
     public  void log(String msg);   

     / / 打印message和throwable到Servlet对应的log文件中,在Jetty中使用WARN级别打印该信息。
     public  void log(String message, Throwable throwable);

     / / 返回给定path在当前机器上操作系统对应的位置。对/META-INF/resources下的资源,除非他们已经解压到本地目录,否则对那些资源该方法返回null。
     / / 在Jetty实现中,使用getResource()方法相同的实现获取Resource实例,如果其getFile()返回不为null,则返回该File的canonical路径。
     public String getRealPath(String path);

     // 返回Servlet Container的名称和版本号,其格式为:<servername>/<versionnumber>,如:Jetty/8.1.9.v20130131。
     public String getServerInfo();

     / / ServletContext级别的初始参数操作,可以在web.xml中使用context-param定义,也可以手动设置。在get中如果找不到对应的项,返回null,在set时,如果已存在name,则返回false,并且不更新相应的值。
     public String getInitParameter(String name);
     public Enumeration<String> getInitParameterNames();
     public  boolean setInitParameter(String name, String value);

     // ServletContext级别的属性操作,其中属性名遵循包命名规则。在set中,如果object为null表示移除该属性,如果name以存在,则会替换原有的值,如果注册了ServletContextAttributeListener,则会出发相应的attributeRemoved、attributeReplaced、attributeAdded事件。在remove中,如果name存在且被移除了,则会触发attributeRemoved事件。
     // 在Jetty中使用ContextHandler中的Context内部类实现ServletContext,在ContextHandler中定义了两个相关字段:_attributes以及_contextAttributes,其中_attributes表示在Jetty内部通过ContextHandler设置的属性,而_contextAttributes表示用户设置的属性,但是在获取属性值时,两个字段的属性都会考虑进去,在移除属性时,如果是移除_attributes字段中的值,则不会触发attributeRemoved事件。
     public Object getAttribute(String name);
     public Enumeration<String> getAttributeNames();
     public  void setAttribute(String name, Object object);
     public  void removeAttribute(String name);

     // 返回当前Web Application在web.xml中的display-name定义的ServletContext名字,在Jetty实现中,如果该值为null,则返回context path。
     public String getServletContextName();

     // 该部分具体的使用可以参考: http://www.blogjava.net/yongboy/archive/2010/12/30/346209.html  

     // 动态的向ServletContext中注册Servlet,注册的Servlet还可以通过返回的ServletRegistration继续配置,如addMapping、setInitParameter等。
     //  在使用className实例话Servlet时,使用当前ServletContext相关联的ClassLoader。在创建Servlet实例时,会根据该类中定义的以下Annotation做相应的配置:
     //  javax.servlet.annotation.ServletSecurity、 javax.servlet.annotation.MultipartConfig、 javax.annotation.security.RunAs、 javax.annotation.security.DeclareRoles
     public ServletRegistration.Dynamic addServlet(String servletName, String className);
     public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet);
     public ServletRegistration.Dynamic addServlet(String servletName, Class <?  extends Servlet> servletClass);
     // 创建给定Servlet类的Servlet实例,并且 会根据该类中定义的以下Annotation做相应的配置:
     //  javax.servlet.annotation.ServletSecurity、 javax.servlet.annotation.MultipartConfig、 javax.annotation.security.RunAs、 javax.annotation.security.DeclareRoles
     // 在创建Servlet实例后,一般还要调用addServlet()方法将其注册到ServletContext中。
     public <T  extends Servlet> T createServlet(Class<T> clazz)  throws ServletException;
     // 根据servletName获取ServletRegistration实例
     public ServletRegistration getServletRegistration(String servletName);
     // 获取所有在ServletContext中注册的servletName到ServletRegistration映射的Map。所有动态注册和使用配置注册的映射。
     public Map<String, ?  extends ServletRegistration> getServletRegistrations();

     // 动态的向ServletContext中注册Filter,注册的Filter可以通过返回的FilterRegistration进一步配置,如addMappingForUrlPatterns、setInitParameter等
     public FilterRegistration.Dynamic addFilter(String filterName, String className);
     public FilterRegistration.Dynamic addFilter(String filterName, Filter filter);
     public FilterRegistration.Dynamic addFilter(String filterName, Class <?  extends Filter> filterClass);
     / / 创建给定Filter类实例的Filter实例,一般都会后继调用addFilter将该实例注册到ServletContext中。
     public <T  extends Filter> T createFilter(Class<T> clazz)  throws ServletException;
     // 根据filterName获取FilterRegistration实例。
     public FilterRegistration getFilterRegistration(String filterName);
     / / 返回所有filterName到FilterRegistration映射的Map,包括所有动态注册和使用配置注册的映射。
     public Map<String, ?  extends FilterRegistration> getFilterRegistrations();

     // 返回SessionCookieConfig实例,用于session tracking的cookie属性,多次调用该方法返回相同的实例。
     public SessionCookieConfig getSessionCookieConfig();
     // 设置session tracking模式,可以是URL、COOKIE、SSL。Jetty8只支持URL和COOKIE。
     public  void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes);
     // 返回当前ServletContext默认支持的session tracking模式。
     public Set<SessionTrackingMode> getDefaultSessionTrackingModes();
     // 返回当前ServletContext目前使用的session tracking模式。
     public Set<SessionTrackingMode> getEffectiveSessionTrackingModes();

     // 向ServletContext动态的注册Listener,该Listener类或实例必须实现以下的一个或多个接口:
    // 
ServletContextAttributeListener、 ServletRequestListener、 ServletRequestAttributeListener、 HttpSessionListener、 HttpSessionAttributeListener}</tt>
    // 如果这个ServletContext传入ServletContainerInitializer的onStartup方法,那么这个Listener类或实例也可以实现ServletContextListener接口。
    // 注:动态注册的ServletContextListener中的contextInitialized方法中不可以调用Servlet 3.0中定义的这些动态注册Servlet、Filter、Listener等方法,不然会抛出UnsupportedOperationException,看起来像是出于一致性、安全性或是兼容性的考虑,但是具体是什么原因一直想不出来。而且在Jetty实现中,它在注册EventListener实例是确取消了这种限制,而对注册EventListener类实例和类名确有这种限制,不知道这是Jetty的bug还是其他什么原因。。。。。

    // 对于调用顺序按定义顺序来的EventListener(如ServletRequestListener、ServletContextListener、HttpSessionListener),那这个新的EventListener会添加到相应列表末尾。

     public  void addListener(String className);
     public <T  extends EventListener>  void addListener(T t);
     public  void addListener(Class <?  extends EventListener> listenerClass);
     // 创建clazz对应的EventListener实例,一般这个新创建的EventListener会之后注册到ServletContext中。
     public <T  extends EventListener> T createListener(Class<T> clazz)  throws ServletException; 

     // 返回web.xml和web-fragment.xml配置文件中定义<jsp-config>的汇总,或返回null如果没有相关配置。看起来像Jetty并没有实现该方法。
     public JspConfigDescriptor getJspConfigDescriptor();

     // 返回当前Web Application对应的ClassLoader实例。
     public ClassLoader getClassLoader();

     // 定义角色名。角色名在ServletRegistration.Dynamic.setServletSecurity()和ServletRegistration.Dynamic.setRunAsRole()中默认定义,因而不需要重新使用这个方法定义。
     public  void declareRoles(String  roleNames);
}

ServletContext初始化

ServletContext的初始化从ContextHandler的doStart()方法开始,在其startContext()方法快结束时,会调用注册的ServletContextListener中的contextInitialized()方法,因而这里是用户对ServletContext初始化时做一些自定义逻辑的扩展点。

在Servlet 3.0中还引入了ServletContainerInitializer接口,它定义了onStartup()方法,该方法会在WebAppContext中的startContext方法中的configure方法中通过ContainerInitializerConfiguration.configure()中被调用,该方法的调用要早于所有ServletContextListener .contextInitialized()事件的触发。
public  interface ServletContainerInitializer {
     // 当ServletContext对应的Web Application初始化时,该方法会被调用。其中c参数是所有继承、实现在ServletContainerInitializer实现类定义的HandlesTypes注解中定义的类,
    // 如果该注解定义的类数组中有注解,那么c参数还包含所有存在这个注解的类。如果实现ServletContainerInitializer接口的类在WEB-INF/lib的jar包中绑定,
    // 那么该方法只会在对应的Web Application初始化被调用一次,如果实现ServletContainerInitializer接口的类在WEB-INF/lib以外定义,但是还可以被Servlet Container找到,
    // 那么意味这这个jar包是多个Web Application共享的,因而该方法会在每个Web Application初始化时被调用。如果ServletContainerInitializer实现类没有定义HandlesTypes注解,
    // 那么c参数为null。ServletContainerInitializer实现类的查找使用运行时service provider机制,然而对于定义在fragment jar包中的ServletContainerInitializer实现类,
    // 但是该jar在absolute ordering中被exclude了,那么该jar包会被忽略,即在web.xml中的absolute-ordering中没有包含相应的fragment名。
     // 在查找满足HandlesTypes注解中定义的类数组的类实例集合时,由于有些JAR包是可选的,因而在加载Class时有时会遇到问题,此时Servlet Container可以选择忽略该错误,
     //  但是需要提供配置已让Servlet Container决定是否要将这些错误打印到日志文件中。 
     public  void onStartup(Set<Class<?>> c, ServletContext ctx)  throws ServletException; 
}
要自定义ServletContainerInitializer逻辑,首先需要META-INF/services/目录下建立一个 javax.servlet.ServletContainerInitializer文件,在该文件内部写入用户在定义的ServletContainerInitializer类,如:x.y.z.MyServletContainerInitializer,该MyServletContainerInitializer类必须实现ServletContainerInitializer接口,如果它有一些特定感兴趣的类,可以向其定义HandlesTypes注解,该注解的值可以是类实例、接口实例、注解实例,它表示所有继承自注解中定义的类、实现注解中定义的接口、有注解中定义的注解注解的类(注解在类、字段、方法中)都会收集成一个Set<Class<?>>类型,并传入onStartup()方法中,如果没有HandlesTypes注解,其onStartup()中Set<Class<?>>参数为null。

在Jetty实现中,AnnotationConfiguration会查找到所有jar包中在META-INF/services/javax.servlet.ServletContainerInitializer中定义的ServletContainerInitializer(排除那些被absolute-ordering排除在外的fragment jar包中的定义),使用这些查找到的ServletContainerInitializer创建ContainerInitializer实例,使用HandlesTypes注解中定义的Class数组初始化InterestedTypes字段,如果这个Class数组中有注解类型,则将所有存在这个注解类型的类(这个注解可以注解在该类的类、字段、方法中)添加到ContainerInitializer实例中的_annotatedTypeNames集合中(使用ContainerInitializerAnnotationHandler实现);最后将他们注册到Context的一个属性中。同时在AnnotationConfiguration中还会向Context中注册一个属性,其值是Map,它包含了所有类对应的其子类或实现类。然后在ContainerInitializerConfiguration中,对每个在Context中注册的ContainerInitializer实例,对所有注册的_annotatedTypeNames,将该类以及该类的子类、实现类注册到_applicableTypeNames集合中;对所有注册的非注解类型的_interestedTypes,将其所有的子类、实现类注册到_applicableTypeNames集合中(在Jetty当前版本的实现中没有包含_interestedTypes中的类实例本身,在ServletContainerInitializer的注释中确实也没有说明要包含这些类本身,感觉这个有点不合理。。。);最后调用ContainerInitializer中的callStartup()方法,它加载_applicableTypeNames集合中的所有类,并将其传入ServletContainerInitializer的onStartup()方法中(这里没有根据规范忽略不能加载成功的类实例)。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
7月前
Could not open ServletContext resource [/WEB-INF/springmvc-servlet.xml]【解决方案】
Could not open ServletContext resource [/WEB-INF/springmvc-servlet.xml]【解决方案】
|
2月前
|
缓存 Java Spring
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
文章比较了在Servlet和Spring Boot中获取Cookie、Session和Header的方法,并提供了相应的代码实例,展示了两种方式在实际应用中的异同。
203 3
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
|
2月前
|
Java 应用服务中间件 Spring
【终极解决方案】Could not open ServletContext resource [/WEB-INF/dispatcher-servlet.xml]
【终极解决方案】Could not open ServletContext resource [/WEB-INF/dispatcher-servlet.xml]
38 0
|
7月前
|
Java 计算机视觉
java实现人脸识别源码【含测试效果图】——Servlet层(FaceServlet)
java实现人脸识别源码【含测试效果图】——Servlet层(FaceServlet)
|
7月前
|
Java 应用服务中间件 数据库连接
Spring5源码(51)-Servlet知识点回顾以及SpringMVC分析入口
Spring5源码(51)-Servlet知识点回顾以及SpringMVC分析入口
73 0
|
7月前
|
前端开发 Java BI
Servlet+Jsp+JDBC实现房屋租赁管理系统(源码+数据库+论文+系统详细配置指导+ppt)
Servlet+Jsp+JDBC实现房屋租赁管理系统(源码+数据库+论文+系统详细配置指导+ppt)
|
7月前
|
前端开发 JavaScript Java
servlet+jsp实现小区门户网站后台管理系统(源码+数据库+文档)
servlet+jsp实现小区门户网站后台管理系统(源码+数据库+文档)
|
7月前
|
前端开发 JavaScript Java
基于servlet实现的日记本管理系统(源码+数据库+文档)
基于servlet实现的日记本管理系统(源码+数据库+文档)
|
7月前
|
Java 关系型数据库 MySQL
基于jsp+servlet+mysql框架的旅游管理系统【源码+数据库+报告】
基于jsp+servlet+mysql框架的旅游管理系统【源码+数据库+报告】
235 0
|
7月前
|
前端开发 JavaScript Java
基于servlet+jsp+mysql实现的工资管理系统【源码+数据库】
基于servlet+jsp+mysql实现的工资管理系统【源码+数据库】
137 0