引言
Servlet是一门很古老的技术了,网上很多人说没必要学了,可以直接学习Springboot+vue进行开发。但Servlet作为Java EE规范的一部分,是这些框架的基础。掌握Servlet对于理解现代Web开发框架至关重要。当然每个人有自己独特的见解,写这篇文章方便自己回顾,也希望能帮助到他人。如文章中有描述错误,欢迎大家指正!
相关资源下载
mysql驱动jar包官网下载: https://dev.mysql.com/downloads/connector/j/
mysql驱动jar包下载教程: https://blog.51cto.com/u_15858035/11005824
tomcat官网下载: https://tomcat.apache.org/
tomcat下载教程: https://blog.csdn.net/qq_42257666/article/details/105701914
jakartaee帮助文档官网下载: https://jakarta.ee/zh/specifications/
jakartaee下载教程: https://blog.csdn.net/yuanhong55/article/details/133868639
什么是Servlet?(以下内容来自百度百科)
Servlet(Server Applet)是Java Servlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态Web内容。
狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。Servlet运行于支持Java的应用服务器中。从原理上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。
最早支持Servlet标准的是JavaSoft的Java Web Server,此后,一些其它的基于Java的Web服务器开始支持标准的Servlet。
快速入门(从0到1搭建)
-
新建项目
-
新建模块
-
为模块添加web框架支持
-
创建工件
-
为模块添加依赖(刚才下载的tomcat中有这个依赖)
-
idea配置tomcat
-
部署工件
-
实现Servlet接口
-
在service方法中编写响应代码
-
在web.xml中编写Servlet配置信息
-
启动项目在浏览器访问
到这里,第一个demo程序便写完了。
了解Servlet接口
Servlet接口有五个方法
void init(ServletConfig config) throws ServletException; 由servlet容器调用,向servlet指示servlet正在投入服务。void service(ServletRequest request, ServletResponse response) throws ServletException, IOException; 由servlet容器调用,以允许servlet响应请求。ServletConfig getServletConfig(); 返回一个ServletConfig对象,其中包含此servlet的初始化和启动参数。String getServletInfo(); 返回有关servlet的信息,如作者、版本和版权。void destroy(); 由servlet容器调用,向servlet指示servlet正在停止服务。
方法执行顺序测试
多次访问demo然后结束程序,控制台输出结果
由此结果可得结论:
- init方法最先执行,且只执行一次
- service方法执行次数与用户访问次数有关
- 当程序结束之前,tomcat会调用这个servlet的destroy方法
既然Demo这整个Servlet都由tomcat服务器来调用, 那这个demo对象也是由tomcat来创建,我们能不能自己创建创建一个?
浏览器访问
控制台输出
当浏览器发送请求后,才出现了以上控制台输出,说明:
- tomcat默认不会创建servlet对象,而且servlet对象只会创建一次,是单例的,但是我们可以手动创建servlet对象,所以是一种假单例。
- 执行顺序(无参构造、init、service)
- 我们可以创建servlet对象,但是似乎没什么用,而且还有一种风险
- 当我们写了一个有参构造函数,不写无参,项目会启动失败(tomcat要调用无参构造函数)
所以我们应该避免自己去写构造函数,如果有初始化参数,可以放在init方法中(init方法先于service方法执行)
那么假设我们有一个Servlet需要连接mysql数据库,有一些初始化参数,我们可以怎么做呢?
这个时候我们可以使用ServletConfig这个对象了。一个Servlet对应一个ServletConfig。
在<init-param></init-param>标签中配置的参数可以被ServletConfig获取到
使用ServletConfig获取初始化信息
了解ServletConfig接口
ServletConfig中有四个方法
String getServletName(); 返回此servlet实例的名称。ServletContext getServletContext(); 返回对调用方正在其中执行的ServletContext的引用。String getInitParameter(String key); 获取具有给定名称的初始化参数的值。Enumeration<String> getInitParameterNames(); 返回servlet初始化参数的名称,作为String对象的枚举,如果servlet没有初始化参数,则返回空枚举。
实践
由于init方法和destroy方法只会执行一次, 那么我们应该重点关注service方法,因为每发一次请求,就会调用这个方法。
void service(ServletRequest request, ServletResponse response) throws ServletException, IOException;
那么既然我们着重于service方法,而init方法是按需使用,我们能不能专注于service方法的编写呢?
当然可以。
Servlet接口有一个适配器GenericServlet,该类实现了Servlet、ServletConfig、Serializable接口。它将service方法设为抽象方法,我们只需要继承该抽象类,然后重写service方法便可以进行web开发。
那么以后如果有初始化参数,我们可以按需重写init方法
重写时我们发现有两个init方法,这是怎么回事,看看源码
public void init(ServletConfig config) throws ServletException {this.config = config;this.init();
}public void init() throws ServletException {
}
那么哪个是我们需要重写的呢?
可以看出上面的那个init方法是GenericServlet实现Servlet接口中的方法。
里面将形参config交给了成员变量config来保管,在源码下面我们发现
public ServletConfig getServletConfig() {return this.config;
}
这个方法提供了ServletConfig对象,这表明GenericServlet将config保存了下来,并且可以供子类使用。
如果我们重写这个有参的init的方法,便破坏了GenericServlet结构,而有参的init方法之后调用了无参的init方法,所以我们重写无参的init方法也是能实现效果的。
所以如果重写init方法,我们应该重写GenericServlet中无参的init方法。
继承GenericServlet进行web开发
import java.io.IOException;
import java.io.PrintWriter;public class Servlet01 extends GenericServlet {@Overridepublic void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {response.setContentType("text/html;charset=UTF-8"); //设置响应信息格式PrintWriter out = response.getWriter();out.print("基于GenericServlet进行web开发");}
}
编写servlet配置信息
<servlet><servlet-name>servlet01</servlet-name><servlet-class>com.hzh.linux.web.Servlet01</servlet-class>
</servlet>
<servlet-mapping><servlet-name>servlet01</servlet-name><url-pattern>/servlet01</url-pattern>
</servlet-mapping>
启动项目,浏览器访问
由此可见, 继承GenericServlet抽象类来开发确实是我们更专注于service方法的编写。
那么有没有更简单的开发方式呢?我们是基于http协议的web程序
当然。tomcat中有针对http协议的类(HttpServlet)
HttpServlet继承了GenericServlet,实现了里面的service方法。
看看源码:
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {HttpServletRequest request;HttpServletResponse response;try {request = (HttpServletRequest)req;response = (HttpServletResponse)res;} catch (ClassCastException var6) {throw new ServletException(lStrings.getString("http.non_http"));}this.service(request, response);
}
在源码中我们可以看到 ServletRequest 被强转成了 HttpServletRequest , ServletResponse 被强制成了 HttpServletResponse。
查看相关源码,可知 HttpServletRequest 继承了 ServletRequest
HttpServletResponse 继承了 ServletResponse。
那么为什么可以直接强转供后续使用呢?
因为我们做的就是基于http协议的web开发。
该方法最后调用了自己的service方法,再看看源码:
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {String method = req.getMethod();long lastModified;if (method.equals("GET")) {lastModified = this.getLastModified(req);if (lastModified == -1L) {this.doGet(req, resp);} else {long ifModifiedSince;try {ifModifiedSince = req.getDateHeader("If-Modified-Since");} catch (IllegalArgumentException var9) {ifModifiedSince = -1L;}if (ifModifiedSince < lastModified / 1000L * 1000L) {this.maybeSetLastModified(resp, lastModified);this.doGet(req, resp);} else {resp.setStatus(304);}}} else if (method.equals("HEAD")) {lastModified = this.getLastModified(req);this.maybeSetLastModified(resp, lastModified);this.doHead(req, resp);} else if (method.equals("POST")) {this.doPost(req, resp);} else if (method.equals("PUT")) {this.doPut(req, resp);} else if (method.equals("DELETE")) {this.doDelete(req, resp);} else if (method.equals("OPTIONS")) {this.doOptions(req, resp);} else if (method.equals("TRACE")) {this.doTrace(req, resp);} else {String errMsg = lStrings.getString("http.method_not_implemented");Object[] errArgs = new Object[]{method};errMsg = MessageFormat.format(errMsg, errArgs);resp.sendError(501, errMsg);}}
从源码我们大概可以看出,这个service方法首先获取了请求方式,然后根据不同的请求方式执行不同的方法。并且没匹配上会向前端响应错误
HttpServlet是一个抽象类,并未存在抽象方法,那么我们继承之后应该怎么做呢?
- 可以直接重写 void service(HttpServletRequest req, HttpServletResponse resp) 这个方法
- 但是这样可以享受不到一些报错信息,需要自己给浏览器响应相关报错信息
- 我们可以直接重写里面对应不同请求方式所执行的函数。如: doGet、doPost等。
测试
我们编写Servlet02继承HttpServlet进行开发
Servlet02.java
package com.hzh.linux.web;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;
import java.io.PrintWriter;public class Servlet02 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {response.setContentType("text/html;charset=UTF-8");PrintWriter out = response.getWriter();out.print("继承HttpServlet进行web开发");}
}
web.xml中相关配置
<servlet><servlet-name>servlet02</servlet-name><servlet-class>com.hzh.linux.web.Servlet02</servlet-class>
</servlet>
<servlet-mapping><servlet-name>servlet02</servlet-name><url-pattern>/servlet02</url-pattern>
</servlet-mapping>
浏览器访问
如何实现多Servlet数据共享
其实我们在前面多次看到了 ServletContext 这个类,这个类相当于web.xml,可以实现多Servlet数据共享。
在 web.xml 中配置如下信息:
<context-param><param-name>username</param-name><param-value>root</param-value>
</context-param>
<context-param><param-name>password</param-name><param-value>123456</param-value>
</context-param>
在Servlet01和Servlet02中获取全局信息
Servlet01.java
package com.hzh.linux.web;import jakarta.servlet.*;import java.io.IOException;
import java.io.PrintWriter;public class Servlet01 extends GenericServlet {@Overridepublic void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {response.setContentType("text/html;charset=UTF-8");ServletContext application = this.getServletContext();String username = application.getInitParameter("username");String password = application.getInitParameter("password");PrintWriter out = response.getWriter();out.print(username + ": " + password);}
}
Servlet02.java
package com.hzh.linux.web;import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;
import java.io.PrintWriter;public class Servlet02 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {response.setContentType("text/html;charset=UTF-8");ServletContext application = this.getServletContext();String username = application.getInitParameter("username");String password = application.getInitParameter("password");PrintWriter out = response.getWriter();out.print(username + ": " + password);}
}
浏览器访问结果:
注意:servletcontext中保存的信息应尽量少且不经常改变
因为ServletContext只有一个,在多线程下对同一数据进行修改存在线程安全问题。
小试牛刀
学到这里,我们可以尝试实现一个登录功能。
设计并创建数据库表 t_user
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (`id` int NOT NULL AUTO_INCREMENT,`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;INSERT INTO `t_user` VALUES (1, 'root', '123456');
INSERT INTO `t_user` VALUES (2, 'linux', '123456');SET FOREIGN_KEY_CHECKS = 1;
该表有三个字段:
id: 主键, 自增
username: 用户名
password: 密码
现在插入了两条数据
设计页面原型
login.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>login page</title>
</head><body><form action="/oa/login" method="post">用户名: <input name="username" /><br />密码: <input name="password" type="password" /><br /><input type="submit" value="登录" /></form>
</body></html>
新建一个应用根路径为 /oa 的模块
将login.html放到web目录下,并创建LoginServlet
在web.xml中配置欢迎页与LoginServlet信息
配置完后目录结构如下:
启动项目,访问 http://localhost:8080/oa/login.html
导入mysql的驱动jar包:在WEB-INF目录下新建lib目录,将mysql的驱动jar包复制到此处
编写LoginServlet.java
package com.hzh.linux.web;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;
import java.io.PrintWriter;
import java.sql.*;public class LoginServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String username = request.getParameter("username");String password = request.getParameter("password");response.setContentType("text/html;charset=UTF-8");PrintWriter out = response.getWriter();String driver = "com.mysql.cj.jdbc.Driver";String url = "jdbc:mysql://localhost:3306/servlet?characterEncoding=utf-8&serverTimezone=Asia/Shanghai";String user = "root"; // 数据库账号String pwd = "123456"; // 数据库密码Connection conn = null;PreparedStatement ps = null;ResultSet rs = null;try {Class.forName(driver);conn = DriverManager.getConnection(url, user, pwd);String sql = "select * from t_user where username = ? and password = ? ";ps = conn.prepareStatement(sql);ps.setString(1, username);ps.setString(2, password);rs = ps.executeQuery();if(rs.next()){// 登录成功out.print("登录成功");}else {//登录失败out.print("登录失败");}} catch (ClassNotFoundException e) {throw new RuntimeException(e);} catch (SQLException e) {e.printStackTrace();} finally {if(rs != null){try {rs.close();} catch (SQLException e) {e.printStackTrace();}}if(ps != null){try {rs.close();} catch (SQLException e) {e.printStackTrace();}}if(conn != null){try {rs.close();} catch (SQLException e) {e.printStackTrace();}}}}
}
启动项目,输入正确的账号密码,浏览器显示登录成功
模拟学生信息管理功能,查看当前系统有啥问题
编写StudentManagerServlet
package com.hzh.linux.web;import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;
import java.io.PrintWriter;public class StudentsManagerServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {response.setContentType("text/html;charset=UTF-8");PrintWriter out = response.getWriter();out.print("姓名:张三 年龄:16<br/>姓名:李四 年龄:17");}
}
将LoginServlet中登录成功后的代码改为
if(rs.next()){// 登录成功response.sendRedirect("students");
}else {// 登录失败out.print("登录失败");
}
在web.xml中配置相关信息
<servlet><servlet-name>studentsManagerServlet</servlet-name><servlet-class>com.hzh.linux.web.StudentsManagerServlet</servlet-class>
</servlet>
<servlet-mapping><servlet-name>studentsManagerServlet</servlet-name><url-pattern>/students</url-pattern>
</servlet-mapping>
浏览器中登录成功后跳转成功
当前问题
正常逻辑应该是后台用户管理员登录成功后才可以访问学生信息管理页面。
但是我们发现不登录也能访问到。
解决方案 —— 使用HttpSession保持会话状态
http是一种无状态协议, 我们可以利用session来保持会话状态。
LoginServlet.java中修改代码如下
if(rs.next()){// 登录成功HttpSession session = request.getSession();session.setAttribute("username", username);response.sendRedirect("students");}else {//登录失败out.print("登录失败");
}
StudentsManagerServlet.java中doGet方法添加如下代码
HttpSession session = request.getSession();
if(session == null || session.getAttribute("username") == null){response.sendRedirect("login");
}
这里我们没有设置session的过期时间,那有效时间是多少呢?
查看tomcat中conf目录下的web.xml文件
所以可知session默认30分钟
在项目web.xml中手动配置
<session-config><session-timeout>60</session-timeout>
</session-config>
这样便增加了会话保持时间
现在便实现了用户只有登录后才能访问资源的效果,但仍存在问题。
假设是一个多功能的复杂系统,难道每次都要手写上述session相关代码吗?
当然不用,我们可以使用过滤器。
在web.xml中配置过滤器
<filter><filter-name>myfilter</filter-name><filter-class>com.hzh.linux.web.MyFilter</filter-class>
</filter>
<filter-mapping><filter-name>myfilter</filter-name><url-pattern>/*</url-pattern>
</filter-mapping>
编写MyFilter.java
package com.hzh.linux.web;import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;import java.io.IOException;public class MyFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;String path = request.getServletPath();if(!(path.equals("/login") || path.equals("/login.html"))){HttpSession session = request.getSession();if(session == null || session.getAttribute("username") == null){response.sendRedirect("login.html");}}// 以上访问Servlet之前的操作filterChain.doFilter(request, response);// 以下响应浏览器之前的操作}
}
接下来删除StudentsManagerServlet.java中相关session判断代码
至此,我们便完成了这个小系统的开发。
Servlet的注解式开发
@WebServlet注解源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package jakarta.servlet.annotation;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {String name() default ""; // 相当于<servlet-name></servlet-name>String[] value() default {}; // 相当于<url-pattern></url-pattern>String[] urlPatterns() default {}; // 相当于<url-pattern></url-pattern>int loadOnStartup() default -1; // 相当于<load-on-startup></load-on-startup>WebInitParam[] initParams() default {}; // 相当于多个<init-param></init-param>boolean asyncSupported() default false; // 相当于<async-supported></async-supported>String smallIcon() default ""; // 该Servlet的大图标String largeIcon() default ""; // 该Servlet的大图标String description() default ""; // 相当于<description></description>String displayName() default ""; // 相当于<display-name></display-name>
}
@WebFilter注解源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package jakarta.servlet.annotation;import jakarta.servlet.DispatcherType;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebFilter {String description() default ""; // description 属性用于提供过滤器的描述信息。String displayName() default ""; // 相当于<display-name></display-name>WebInitParam[] initParams() default {}; // 相当于多个<init-param></init-param>String filterName() default ""; // 相当于<filter-name></filter-name>String smallIcon() default ""; // 该过滤器的小图标String largeIcon() default ""; // 该过滤器的大图标String[] servletNames() default {}; // servletNames 属性用于指定过滤器将应用于哪些 Servlet。String[] value() default {}; // value 属性用于指定过滤器将应用到哪些 URL 模式或 Servlet 上。String[] urlPatterns() default {}; // 相当于<url-pattern></url-pattern>DispatcherType[] dispatcherTypes() default {DispatcherType.REQUEST}; // 用于指定过滤器应用的请求分发类型。boolean asyncSupported() default false; // 相当于<async-supported></async-supported>
}
改造上面那个小案例
@WebServlet("/students")
public class StudentsManagerServlet extends HttpServlet {...}@WebServlet("/login")
public class LoginServlet extends HttpServlet {...}@WebFilter("/*")
public class MyFilter implements Filter {}
使用上诉注解后便可以将web.xml中关于servlet相关配置和filter相关配置删除
不过建议在使用过滤器时不使用注解式开发。这样在修改过滤器路径时就不用重新编译整个项目了。而且一般过滤器的数量不会很多。