
先把Servlet的“底层逻辑”摸透,不然写代码像没根的草
你可能听过“JSP是Servlet的模板”,但具体是怎么回事?我当初学的时候也糊里糊涂,直到有次把JSP文件编译后的.class文件打开看——原来Tomcat会把JSP里的HTML代码转成out.write()
的语句,把Java代码直接保留,最后生成一个继承HttpServlet
的类。比如你写个index.jsp
,里面有,编译后会变成
index_jsp.java
,里面有个_jspService
方法,调用response.getWriter()
输出日期。哦,原来如此!那我们自己写Servlet,其实就是跳过JSP的模板,直接写那个被编译后的类——这样你就能完全控制请求怎么处理,响应怎么返回,比如处理用户登录、上传文件这些逻辑,比用JSP的脚本更干净。
那Servlet到底是什么?用Oracle Java EE文档的话说(链接:https://docs.oracle.com/javaee/7/tutorial/servlets001.htm rel=”nofollow”),Servlet是运行在服务器端的Java程序,用来处理客户端的HTTP请求并返回响应。但口语点说,Servlet就是“服务器上的小插件”,专门帮你处理HTTP请求:比如用户点了“登录”按钮,浏览器发一个POST请求到服务器,Servlet就接住这个请求,取出用户名密码,查数据库,然后告诉浏览器“登录成功,跳转到首页”或者“密码错了,回到登录页”。
为什么要自己写Servlet?比如你用JSP写登录逻辑,会有一堆的脚本,又乱又难维护;而用Servlet,你可以把业务逻辑放到Java类里,JSP只负责显示页面——这就是MVC模式(模型-视图-控制器)里的“C”(控制器)。我以前做一个电商项目,刚开始用JSP写所有逻辑,后来改造成Servlet+JSP的MVC,代码瞬间清爽了,bug也少了一半——这就是Servlet的好处:分离逻辑和视图,让代码更易维护。
从0到1写Servlet:步骤别乱,每一步都要“踩实”
现在进入实战环节,我拿“用户登录”这个常见场景当例子,带你写一个LoginServlet
,从写类到运行,每一步都讲清楚。
第一步:写Servlet类——核心是“继承+重写”
你得建一个Java类,继承HttpServlet
——这是Servlet的“基础模板”,已经帮你处理了HTTP请求的细节(比如GET、POST方法分流)。然后重写doGet
或doPost
方法——根据你要处理的请求方式来选。比如处理登录表单的POST请求,就重写doPost
:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
// 注解配置:对应URL路径是/login
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//
设置请求编码(防止中文乱码,必须放获取参数前)
request.setCharacterEncoding("UTF-8");
//
获取表单参数(name属性要和input的name一致)
String username = request.getParameter("username");
String password = request.getParameter("password");
//
简单的登录验证(实际要查数据库,这里模拟)
if ("admin".equals(username) && "123456".equals(password)) {
// 登录成功:转发到首页(request.getRequestDispatcher)
request.getRequestDispatcher("/index.jsp").forward(request, response);
} else {
// 登录失败:设置错误信息,转发回登录页
request.setAttribute("errorMsg", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
}
这里要敲黑板的几个点:
HttpServlet
? 因为GenericServlet
是通用Servlet类,而HttpServlet
专门处理HTTP请求——写web应用肯定用HttpServlet
更顺手。我以前试过直接继承GenericServlet
,结果要自己处理GET/POST方法的分流,麻烦得很,后来赶紧换成HttpServlet
,省了好多事。doPost
而不是service
? 因为HttpServlet
的service
方法会帮你“分流”:比如客户端发POST请求,service
就调用doPost
;发GET就调用doGet
。你要是重写了service
方法,反而会覆盖这个分流逻辑,所以不用管service
,只重写需要的doGet
或doPost
就行。我以前犯过这个错,重写了service
方法,结果doPost
没被调用,后来看了HttpServlet
的源码(service
方法里的逻辑:String method = req.getMethod(); if (method.equals("GET")) { ... } else if (method.equals("POST")) { ... }
)才明白,从那以后再也不重写service
了。request
的编码? 因为浏览器默认用ISO-8859-1
编码发送表单数据,要是不设置成UTF-8
,中文参数会乱码——比如输入“张三”,拿到的会是“å¼ ä¸”。我以前写Servlet的时候忘了这一步,结果用户输入中文用户名,查数据库总查不到,后来加上request.setCharacterEncoding("UTF-8")
,立马好了。对了,这个设置要放在获取参数之前,不然没用——我有次把它放在getParameter
后面,结果还是乱码,折腾了半小时才发现顺序错了。第二步:配置Servlet——两种方式,选适合你的
写好类之后,得告诉服务器“这个Servlet对应哪个URL”——比如用户访问http://localhost:8080/myapp/login
,服务器要知道找LoginServlet
来处理。配置有两种方式:传统的web.xml
和现代的注解(Servlet 3.0+支持)。
方式1:用web.xml
配置(适合老项目或统一管理)
如果你的项目用Servlet 3.0以前的版本,或者习惯用配置文件,就用web.xml
。配置代码长这样:
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-
注册Servlet:告诉服务器有这么个Servlet类 >
LoginServlet <!-
唯一名称 >
com.example.LoginServlet <!-
类的全路径 >
<!-
映射URL:告诉服务器这个Servlet对应哪个路径 >
LoginServlet <!-
和上面的名称一致 >
/login <!-
路径必须以/开头! >
这里要注意:
url-pattern
必须以/
开头!比如/login
是对的,login
是错的——因为这个路径是相对于web应用的根目录(比如myapp
的根目录是http://localhost:8080/myapp/
)。我朋友上次就是把url-pattern
写成login
,结果访问时服务器找的是http://localhost:8080/myapp/login/login
,当然404了,加了斜杠才通。servlet-name
要唯一!比如两个Servlet不能都叫“LoginServlet”,不然Tomcat启动时会报错。我以前复制粘贴配置忘了改名称,结果日志里抛异常,赶紧改了才解决。方式2:用注解配置(推荐,更简洁)
如果你的项目用Servlet 3.0或更高版本(比如Tomcat 7+、Java EE 6+),直接用@WebServlet
注解更方便——不用写web.xml
,直接在Servlet类上加注解就行,比如:
@WebServlet("/login") // 对应URL路径是/login
public class LoginServlet extends HttpServlet { ... }
是不是超简单?我现在做新项目都用注解,省得维护web.xml
的一堆配置。注解里还能加更多参数,比如:
@WebServlet(
urlPatterns = "/login", // 可以指定多个URL,比如{"/login", "/doLogin"}
loadOnStartup = 1, // 启动时加载Servlet,数字越小优先级越高
initParams = @WebInitParam(name = "encoding", value = "UTF-8") // 初始化参数
)
比如loadOnStartup=1
,意味着Tomcat启动时就创建这个Servlet的实例,而不是等第一次请求——这样可以提前加载资源(比如数据库驱动),提升第一次请求的速度。我以前做一个需要预加载字典数据的项目,就用了loadOnStartup=1
,把字典数据放到ServletContext
里,用户第一次访问时直接拿数据,快了好多。
第三步:部署运行——别慌,按步骤来就成
写好类、配置好之后,就可以部署到Tomcat运行了。这里分两种情况:用IDE(比如IDEA、Eclipse)或者手动部署。
用IDE部署(新手友好)
以IDEA为例:
war exploded
(解压后的war包,调试方便)。http://localhost:8080/myapp/login.jsp
(myapp
是项目名),输入“admin/123456”,点登录——如果跳转到index.jsp
,说明成功了。我以前用IDE部署时,忘了加Artifact,结果Tomcat启动了但项目没部署,访问一直404,后来看“Deployment”标签页才发现问题,加上后重启就好了。
手动部署(适合熟悉Tomcat的同学)
mvn package
,生成myapp.war
(在target
目录下)。myapp.war
复制到Tomcat的webapps
目录(比如D:apache-tomcat-9.0.60webapps
)。binstartup.bat
(Windows)或bin/startup.sh
(Linux),Tomcat会自动解压myapp.war
。http://localhost:8080/myapp/login.jsp
,操作和IDE部署一样。常见坑排雷:遇到错误先查这几点
我整理了新手常犯的错误和解决方法,做成了表格,你遇到问题直接查就行:
错误现象 | 常见原因 | 解决方法 |
---|---|---|
404(找不到资源) | url-pattern少斜杠;项目没部署;Servlet类没被扫描到 | 检查url-pattern是否以/开头;确认项目已部署到webapps;看注解或web.xml的类路径是否正确 |
500(服务器内部错误) | 代码逻辑错(空指针);方法参数顺序错;类没找到 | 看Tomcat日志的异常栈(定位错误行);检查doGet/doPost的参数顺序(HttpServletRequest在前);确认servlet-class的全路径正确 |
405(请求方式不支持) | 发POST但没重写doPost;发GET但没重写doGet | 重写对应的doGet/doPost;如果逻辑一样,可在doGet里调用doPost(如doGet() { doPost(req, resp); }) |
中文乱码 | 没设置request编码;没设置response的ContentType;浏览器编码不对 | 在获取参数前设置request.setCharacterEncoding(“UTF-8”);输出响应前设置response.setContentType(“text/html;charset=UTF-8”);检查浏览器编码是否为UTF-8 |
比如我以前遇到中文乱码,就是没设置response
的ContentType
,结果输出的HTML是ISO-8859-1
编码,加了response.setContentType("text/html;charset=UTF-8")
后立马好了。
如果你按这些步骤做,应该能顺利跑通第一个Servlet。我当年学的时候,就是这么一步步试的,从写HelloWorld
Servlet到复杂业务逻辑,现在想起那时候的场景还挺怀念——编程就是把复杂问题拆成小步骤,每一步搞懂,就不会慌了。
对了,遇到问题先看Tomcat的日志(logs/catalina.out
),日志会告诉你错误在哪一行、什么原因,比瞎猜管用多了。要是你试了这些方法,欢迎回来告诉我效果——比如成功跑通了,或者遇到新问题,我帮你一起看看~
比如啊,@WebServlet里的loadOnStartup参数,其实就是管Servlet啥时候“睡醒”的——默认值是-1,意思是等第一次有请求找它的时候,Tomcat才会慢悠悠地创建这个Servlet的实例。你想啊,第一次请求过来时,既要初始化实例又要处理逻辑,肯定会慢半拍——我之前做一个查地区字典的功能,一开始没改这个参数,用户第一次点“查地区”要等1.5秒,后面才快,就是因为第一次得加载字典数据到内存里。
后来我把loadOnStartup改成1了,哎,Tomcat启动的时候就“叫醒”这个Servlet了!我把加载字典的代码放到Servlet的init方法里,Tomcat一启动,就自动执行init,把字典表的数据读到ServletContext里——等用户第一次访问的时候,直接从内存里拿数据,根本不用等。对了,这个参数要是设成正整数,数字越小越“优先起床”:比如你有两个Servlet,一个要加载数据库连接池(设成1),一个要加载系统配置(设成2),Tomcat会先加载设1的那个,这样后面的Servlet要用数据库连接时,池已经建好了,不会出“找不到连接”的错。
再比如做电商项目的商品分类缓存,我把处理分类的Servlet设成loadOnStartup=1,启动时就把分类数据缓存好,用户点“分类”按钮直接显示,比默认情况快了差不多1秒——用户体验一下子就上去了。当然啦,要是你的Servlet没什么要提前准备的,比如只是处理个简单的留言提交,那用默认的-1也没事,毕竟不用占Tomcat启动的时间。
其实这个参数的核心就是“预加载”——把需要提前准备的资源,在服务器启动时就弄好,等用户来的时候直接用,不用让用户等。我做过的项目里,只要涉及到“第一次访问慢”的问题,首先就会想到改这个参数,百试百灵。
Servlet和JSP的核心区别是什么?
JSP本质是“Servlet的模板”,Tomcat会将JSP编译成继承HttpServlet的Java类(如index.jsp→index_jsp.java),侧重页面展示(视图);而Servlet是直接编写的Java类,侧重处理HTTP请求的业务逻辑(控制器)。简单说,JSP负责“显示什么”,Servlet负责“怎么处理请求”,两者配合实现MVC模式的逻辑与视图分离。
配置Servlet用注解还是web.xml更好?
新项目推荐用@WebServlet
注解(Servlet 3.0+支持,如Tomcat 7+),无需维护XML配置,更简洁;老项目或需要统一管理配置的场景,用web.xml更兼容。若需多个URL映射、启动预加载等高级配置,注解也能通过参数(如urlPatterns
、loadOnStartup
)实现。
访问Servlet时出现404错误,常见原因有哪些?
最常见原因包括:1)url-pattern
未以斜杠开头(如写成“login”而非“/login”);2)Servlet类的全路径配置错误(web.xml的servlet-class
或注解的类路径不对);3)项目未正确部署到Tomcat(如IDE未添加Artifact,或手动部署时war包未解压);4)请求URL与配置的路径不匹配(如配置的是“/login”,却访问“/doLogin”)。
Servlet处理请求时中文乱码怎么办?
需两步解决:1)处理请求参数:在获取参数前调用request.setCharacterEncoding("UTF-8")
(解决浏览器POST请求的编码问题);2)处理响应输出:在输出HTML前调用response.setContentType("text/html;charset=UTF-8")
(确保响应的内容编码为UTF-8)。同时检查浏览器编码是否为UTF-8,避免前端展示乱码。
@WebServlet的loadOnStartup参数有什么作用?
loadOnStartup
用于设置Servlet的加载时机:默认值为-1(第一次请求时加载);若设为1-10等正整数,Tomcat启动时会立即创建Servlet实例(数字越小优先级越高)。适用于需要预加载资源的场景(如初始化数据库连接、加载字典数据),可提升第一次请求的响应速度。