Servlet是一个运行在Web服务器中的Java小程序,Servlet会接收和响应来自Web客户端的请求,使用HTTP进行通信。除非某些过滤器提前中止了客户端的请求,否则所有的请求都将会发送到某些Servlet中。
大多情况下,Servlet都继承至GenericServlet,GenericServlet是一个不依赖具体协议的Servlet,它包含了一个抽象的service()方法以及几个辅助方法用于日志操作和从应用、Servlet配置中获取信息。
作为响应HTTP请求的HttpServlet,它继承了GenericServlet,并实现了只接受http请求的service()方法,然后它提供了响应每种http请求的方法的空实现。
import javax.servlet.http.HttpServlet;
public class HelloServlet extends HttpServlet {
}
这个Servlet已经可以接受任何HTTP请求,但它并没有重写HttpServlet中的方法,所以会返回一个HTTP状态405作为响应。现在我们重写doGet()方法,添加对HTTP GET方法的支持,如下:
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("Hello World");
}
}
@Override
public void init() throws ServletException {
System.out.println(this.getServletName() + "has started");
}
@Override
public void destroy() {
System.out.println(this.getServletName() + "has stopped");
}
如果将Servlet配置为在Web应用程序部署和启动时自动启动,那么它的init()方法也将会调用。否则,init()方法将在第一次请求访问Servlet时调用。
destroy()方法在Web应用程序被停止或卸载、关闭时立即调用,它不需要等待垃圾回收器进行
Archetype Created Web Application
hello
f1.chapter3.servlet.HelloServlet
1
hello
/getting
如果两个或多个Servlet的
如果tomcat没有设置为根目录的话,应该在getting前加上项目名,如:http://localhost:8080/xxx/getting
如果希望命中destroy的断点,需要在命令行关闭Tomcat,如果在IDE中关闭Tomcat,则Tomcat将在断点被命中之前断开与调试器的连接。
HttpServletRequest最重要的功能是从客户端发送的请求中获取参数。请求参数有两种不同的形式:查询参数(也称为URL参数)和post变量。
//请求url为http://localhost:8080/getting?name=alex&name=zhangsan&age=25,请求类型为get
String name = req.getParameter("name");//当参数有多个值时只返回第一个值
System.out.println(name);//alex
String[] names = req.getParameterValues("name");//返回参数值的数组
System.out.println(names[0] + "==" + names[1]);//alex==zhangsan
Map map = req.getParameterMap();//返回参数和值的key-value
String[] s = (String[]) map.get("name");//alex==zhangsan
System.out.println(s[0] + "==" + s[1]);
Enumeration names1 = req.getParameterNames();//返回参数名字的枚举
Object o1 = names1.nextElement();
Object o2 = names1.nextElement();
System.out.println(o1 + "==" + o2);//name==age
resp.getWriter().print("Hello World");
//请求内容为age=8,请求类型为post,Request Body为Empty
String contentType = req.getContentType();//返回请求的MIME内容类型(多用途互联网邮件扩展)
System.out.println(contentType);//application/x-www-form-urlencoded; charset=GBK
int length = req.getContentLength();//返回请求正文的长度,以字节为单位
System.out.println(length);//5
String encoding = req.getCharacterEncoding();//返回请求内容的字符编码
System.out.println(encoding);//GBK
resp.getWriter().print("Hello World");
//请求内容为zhangsan,请求类型为post,Request Body为Text
String contentType = req.getContentType();//返回请求的MIME内容类型(多用途互联网邮件扩展)
System.out.println(contentType);//*/*; charset=UTF-8
int length = req.getContentLength();//返回请求正文的长度,以字节为单位
System.out.println(length);//8
String encoding = req.getCharacterEncoding();//返回请求内容的字符编码
System.out.println(encoding);//UTF-8
BufferedReader reader = req.getReader();
StringBuffer buffer = new StringBuffer();
char[] c = new char[100];
int len = 0;
while ((len = reader.read(c)) != -1) {
buffer.append(c, 0, len);
}
System.out.println(buffer.toString());//zhangsan
如果请求内容是字符编码的,那么使用BufferedReader是最简单的方式。
如果请求格式是二进制的,那么必须使用ServletInputStream,这样才可以访问字节格式的请求内容。
以上的两种方式永远不要在同一请求上同时使用这两种方法,否则会触发IllegalStateException异常。
//post请求
StringBuffer requestURL = req.getRequestURL();
System.out.println(requestURL);//http://localhost:8080/getting
String requestURI = req.getRequestURI();
System.out.println(requestURI);///getting
String servletPath = req.getServletPath();
System.out.println(servletPath);///getting
String contenttype = req.getHeader("contenttype");
System.out.println(contentType);//*/*; charset=UTF-8
String name = req.getParameter("name");
resp.setContentType("application/json;charset=utf-8");
//resp.setCharacterEncoding("utf-8");
resp.getWriter().print("你好" + name);//你好alex
package f1.chapter3.servlet;
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;
import java.io.PrintWriter;
//使用servlet注解
@WebServlet(
name = "helloServlet",
urlPatterns = {"/getting"},
loadOnStartup = 1
)
public class HelloServlet1 extends HttpServlet{
private static final String USER_NAME="Tom";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
if(null==username){
username=HelloServlet1.USER_NAME;
}
resp.setContentType("text/html");
resp.setCharacterEncoding("utf-8");
PrintWriter writer = resp.getWriter();
writer.append("")
.append("")
.append("测试 ")
.append("")
.append("Hello ").append(username+"
")
.append("Enter your name:
")
.append("");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req,resp); //将请求委托给doGet方法
}
}
浏览器请求结果如下:
输入参数然后点击提交可以请求post方法,然后将请求消息委托给get访求处理,并返回结果。
package f1.chapter3.servlet;
import javax.servlet.ServletContext;
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;
import java.io.PrintWriter;
@WebServlet(
name = "contextParamServlet",
urlPatterns = {"/contextParam"},
loadOnStartup = 1
)
public class ContextParamServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = req.getServletContext();
PrintWriter writer = resp.getWriter();
writer.print(context.getInitParameter("username"));//Tom
}
}
配置的上下文初始化参数是可以在多个servlet中共享的,所以需要需要配置部署描述符中的
username
Tom
package f1.chapter3.servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(
name = "servletParamServlet",
urlPatterns = {"/servletParam"},
initParams = {@WebInitParam(name = "password", value = "Tom123")},
loadOnStartup = 1
)
public class ServletParamServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletConfig servletConfig = this.getServletConfig();//获取当前的Servlet配置
PrintWriter writer = resp.getWriter();
writer.print(servletConfig.getInitParameter("password"));
}
}
注解配置servlet的初始化参数之后,必须重新编译应用程序。当然也可以配置在部署描述符中,只需更改xml代码并生重启应用程序即可,并不需要重新编译,如下:
servletParamServlet
f1.chapter3.servlet.ServletParamServlet
password
Tom123
servletParamServlet
/servletParam
下面是演示一个简单的servlet项目,用于处理票据业务
Attachment是代表附件信息
package com.wrox;
public class Attachment
{
private String name;
private byte[] contents;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public byte[] getContents()
{
return contents;
}
public void setContents(byte[] contents)
{
this.contents = contents;
}
}
Ticket代表票据
package f1.chapter3.pojo;
import java.util.LinkedHashMap;
import java.util.Map;
public class Ticket {
private String customerName;
private String subject;
private String body;
private Map attachments = new LinkedHashMap<>();
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public Map getAttachments() {
return attachments;
}
public void setAttachments(Map attachments) {
this.attachments = attachments;
}
}
package f1.chapter3.servlet;
import f1.chapter3.pojo.Attachment;
import f1.chapter3.pojo.Ticket;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;
@WebServlet(
name = "ticketServlet",
urlPatterns = {"/tickets"},
loadOnStartup = 1
)
@MultipartConfig(//告诉web容器为servlet提供上传文件的支持
fileSizeThreshold = 5_242_880,//5MB
/**
* 告诉web容器文件必须达到多大才能写入到临时目录中(这里小于5MB的将保存在内存中,请求完成后将由垃圾回收器回收,
* 大于5MB的将保存在location指向的目录中,服务器会有一个默认的location目录,请求完成后,容器将从磁盘中删除此文件)
*/
maxFileSize = 20_971_520L,//20MB,禁止上传文件大小超过20MB的文件
maxRequestSize = 41_943_040L//40MB,禁止大小超过40MB的请求
)
public class TicketServlet extends HttpServlet {
private volatile int TICKET_ID_SEQUENCE = 0;//使用volatile关键字,可以保证其他线程始终可以读取变量修改后的最终值
private Map ticketDB = new LinkedHashMap<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String action = req.getParameter("action");
if (null == action) {
action = "list";
}
switch (action) {
case "create":
this.showTicketForm(resp);
break;
case "view":
this.viewTicket(req, resp);
break;
case "download":
this.downloadAttachment(req, resp);
break;
case "list":
default:
this.showTicketList(resp);
break;
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String action = req.getParameter("action");
if (null == action) {
action = "list";
}
switch (action) {
case "create":
this.createTicket(req, resp);
break;
case "list":
default:
resp.sendRedirect("tickets");
break;
}
}
/**
* 下载attachment
*
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
private void downloadAttachment(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
String idString = request.getParameter("ticketId");
Ticket ticket = this.getTicket(idString, response);
if (ticket == null) {
return;
}
String name = request.getParameter("attachment");
if (name == null) {
response.sendRedirect("tickets?action=view&ticketId=" + idString);
return;
}
Attachment attachment = ticket.getAttachments().get(name);
if (attachment == null) {
response.sendRedirect("tickets?action=view&ticketId=" + idString);
return;
}
response.setHeader("Content-Disposition",//强制浏览器询问是保存还是下载文件,而不是在线打开文件
"attachment; filename=" + attachment.getName());
response.setContentType("application/octet-stream");
ServletOutputStream stream = response.getOutputStream();
stream.write(attachment.getContents());//如果是下载大文件,则不应该将数据存在内存中,而是应该经常刷新ResponseOutputStream,这样数据才会不断的发送到浏览器
}
/**
* 根据ticketID显示查找对应的ticket并显示
*
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
private void viewTicket(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String ticketId = request.getParameter("ticketId");
Ticket ticket = this.getTicket(ticketId, response);
if (null == ticket) {
return;
}
PrintWriter writer = this.writerHeader(response);
writer.append("Ticket #").append(ticketId)
.append(": ").append(ticket.getSubject()).append("
\r\n");
writer.append("Customer Name - ").append(ticket.getCustomerName())
.append("
\r\n");
writer.append(ticket.getBody()).append("
\r\n");
if (ticket.getAttachments().size() > 0) {
int i = 0;
for (Attachment attachment : ticket.getAttachments().values()) {
if (i++ > 0) {
writer.append(", ");
}
writer.append("")
.append(attachment.getName()).append("");
}
writer.append("
\r\n");
}
writer.append("Return to list tickets\r\n");
this.writerFooter(writer);
}
/**
* 根据key值从tikcetDB中获取ticket
*
* @param idString
* @param response
* @return
* @throws IOException
*/
public Ticket getTicket(String idString, HttpServletResponse response) throws IOException {
if (null == idString && idString.length() == 0) {
response.sendRedirect("tickets");
return null;
}
try {
Ticket ticket = this.ticketDB.get(Integer.parseInt(idString));
if (null == ticket) {
response.sendRedirect("tickets");
return null;
}
return ticket;
} catch (Exception e) {
response.sendRedirect("tickets");
return null;
}
}
/**
* 创建ticket并转发到ticket页面
*
* @param request
* @param response
* @throws IOException
* @throws ServletException
*/
private void createTicket(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
Ticket ticket = new Ticket();
ticket.setCustomerName(request.getParameter("customerName"));
ticket.setSubject(request.getParameter("subject"));
ticket.setBody(request.getParameter("body"));
Part filePart = request.getPart("file1");
//如果filePart不为空,就向ticket中添加attachment
if (null != filePart && filePart.getSize() > 0) {
Attachment attachment = this.getAttachment(filePart);
if (null != attachment) {
ticket.getAttachments().put(attachment.getName(), attachment);
}
}
int id;
synchronized (this) {//这两个都是servlet的实例变量,这意味着多个线程可以同时访问他们,将这些操作添加到同步代码块中,可以保证其他线程无法同时执行这两行代码
id = this.TICKET_ID_SEQUENCE++;
this.ticketDB.put(id, ticket);
}
//转发到ticket页面
response.sendRedirect("tickets?action=view&ticketId=" + id);
}
/**
* 通过filePart来获取attachment
*
* @param filePart
* @return
* @throws IOException
*/
private Attachment getAttachment(Part filePart) throws IOException {
InputStream inputStream = filePart.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int read;
byte[] bytes = new byte[1024];
while ((read = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, read);
}
Attachment attachment = new Attachment();
attachment.setName(filePart.getSubmittedFileName());//获取文件上传之前的原始名称
attachment.setContents(outputStream.toByteArray());
return attachment;
}
/**
* 创建ticket时显示此表单,让客户填写相关信息
*
* @param response
* @throws IOException
*/
public void showTicketForm(HttpServletResponse response) throws IOException {
PrintWriter writer = this.writerHeader(response);
writer.append("Create a Ticket
\r\n");
writer.append("\r\n");
this.writerFooter(writer);
}
/**
* 显示ticket列表,如果没有显示提示信息
*
* @param response
* @throws IOException
*/
public void showTicketList(HttpServletResponse response) throws IOException {
PrintWriter writer = this.writerHeader(response);
writer.append("Tickets
")
.append("Create Ticket
");//可点击执行创建ticket操作
if (this.ticketDB.size() == 0) {
writer.append("There are no tickets in the system.\r\n");
} else {
for (int id : this.ticketDB.keySet()) {
Ticket ticket = this.ticketDB.get(id);
String idStr = Integer.toString(id);
writer.append("Ticket #").append(idStr)
.append(": ").append(ticket.getSubject())
.append(" (customer: ").append(ticket.getCustomerName())
.append(")
");
}
}
this.writerFooter(writer);
}
/**
* html头部标签
*
* @param response
* @throws IOException
*/
private PrintWriter writerHeader(HttpServletResponse response) throws IOException {
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.append("")
.append("")
.append("测试 ")
.append("")
.append("");
return writer;
}
/**
* html底部标签
*
* @param writer
*/
private void writerFooter(PrintWriter writer) {
writer.append("")
.append("");
}
}
Web容器通常会包括某种类型的线程池(连接池),当容器接收到请求时,它将在池中寻找可用的线程。如果找不到可用的线程,并用线程池已经达到了最大线程数,那么请求将被放入一个队列中,等待获得可用的线程。一旦出现可用线程,浏览器将从线程池中借出线程,并将请求传递给线程,让线程进行处理。只要请求正在由应用程序代码处理,该线程就只属于这个请求。只有在请求完成,响应内容已经发送到客户端后,该线程才会变成可用状态,并返回到线程池,用于处理下一个请求。
创建和销毁线程会产生许多开销,这可能会降低应用程序的运行速度,所以使用线程池可以减少这种开销,提升性能。Tomat中最大线程数默认为200。
方法中创建的对象和变量在方法执行的过程中都是安全的,其他线程无法访问。但servlet中静态变量和实例变量都可以同志被多个线程访问,因此需要对共享资源进行同步。