Web
200 OK:表示成功;
301 Moved Permanently:表示该URL已经永久重定向;
302 Found:表示该URL需要临时重定向;
304 Not Modified:表示该资源没有修改,客户端可以使用本地缓存的版本;
400 Bad Request:表示客户端发送了一个错误的请求,例如参数无效;
401 Unauthorized:表示客户端因为身份未验证而不允许访问该URL;
403 Forbidden:表示服务器因为权限问题拒绝了客户端的请求;
404 Not Found:表示客户端请求了一个不存在的资源;
500 Internal Server Error:表示服务器处理时内部出错,例如因为无法连接数据库;
503 Service Unavailable:表示服务器此刻暂时无法处理请求。
从第二行开始,服务器每一行均返回一个HTTP头。服务器经常返回的HTTP Header包括:
Content-Type:表示该响应内容的类型,例如text/html,image/jpeg;
Content-Length:表示该响应内容的长度(字节数);
Content-Encoding:表示该响应压缩算法,例如gzip;
Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒。
HTTP请求和响应都由HTTP Header和HTTP Body构成,
其中HTTP Header每行都以\r\n结束。如果遇到两个连续的\r\n,
那么后面就是HTTP Body。浏览器读取HTTP Body,
并根据Header信息中指示的Content-Type、Content-Encoding等解压后显示网页、图像或其他内容。
通常浏览器获取的第一个资源是HTML网页,在网页中,
如果嵌入了JavaScript、CSS、图片、视频等其他资源,浏览器会根据资源的URL再次向服务器请求对应的资源。
Http版本
HTTP目前有多个版本,
1.0是早期版本,浏览器每次建立TCP连接后,只发送一个HTTP请求并接收一个HTTP响应,
然后就关闭TCP连接。由于创建TCP连接本身就需要消耗一定的时间,
因此,HTTP 1.1允许浏览器和服务器在同一个TCP连接上反复发送、接收多个HTTP请求和响应,这样就大大提高了传输效率。
我们注意到HTTP协议是一个请求-响应协议,它总是发送一个请求,然后接收一个响应。能不能一次性发送多个请求,
然后再接收多个响应呢?HTTP 2.0可以支持浏览器同时发出多个请求,
但每个请求需要唯一标识,服务器可以不按请求的顺序返回多个响应,由浏览器自己把收到的响应和请求对应起来。
可见,HTTP 2.0进一步提高了传输效率,因为浏览器发出一个请求后,不必等待响应,就可以继续发下一个请求。
HTTP 3.0为了进一步提高速度,将抛弃TCP协议,改为使用无需创建连接的UDP协议,目前HTTP 3.0仍然处于实验阶段。
Servlet
编写Web应用程序就是编写Servlet处理HTTP请求;
Servlet API提供了HttpServletRequest和HttpServletResponse两个高级接口来封装HTTP请求和响应;
Web应用程序必须按固定结构组织并打包为.war文件;
需要启动Web服务器来加载我们的war包来运行Servlet
JavaEE提供了Servlet API,
我们使用Servlet API编写自己的Servlet来处理HTTP请求,Web服务器实现Servlet API接口,实现底层功能:
┌───────────┐
│My Servlet │
├───────────┤
│Servlet API│
┌───────┐ HTTP ├───────────┤
│Browser│<──────>│Web Server │
└───────┘ └───────────┘
我们来实现一个最简单的Servlet:
// WebServlet注解表示这是一个Servlet,并映射到地址/:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 设置响应类型:
resp.setContentType("text/html");
// 获取输出流:
PrintWriter pw = resp.getWriter();
// 写入响应:
pw.write("<h1>Hello, world!</h1>");
// 最后不要忘记flush强制输出:
pw.flush();
}
}
一个Servlet总是继承自HttpServlet,
然后覆写doGet()或doPost()方法。
注意到doGet()方法传入了HttpServletRequest和HttpServletResponse两个对象,
分别代表HTTP请求和响应。我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,
因为HttpServletRequest和HttpServletResponse就已经封装好了请求和响应。以发送响应为例,
Servlet API是一个jar包,我们需要通过Maven来引入它,才能正常编译。编写pom.xml文件如下
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
注意到<scope>指定为provided,表示编译时使用,但不会打包到.war文件中,因为运行期Web服务器本身已经提供了Servlet API相关的jar包。
我们还需要在工程目录下创建一个web.xml描述文件,放到src/main/webapp/WEB-INF目录下(固定目录结构,不要修改路径,注意大小写)。
文件内容可以固定如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
整个工程结构如下:
web-servlet-hello
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── itranswarp
│ └── learnjava
│ └── servlet
│ └── HelloServlet.java
├── resources
└── webapp
└── WEB-INF
└── web.xml
运行Maven命令mvn clean package,在target目录下得到一个hello.war文件,
这个文件就是我们编译打包后的Web应用程序。
使用embedded的tomcat开发java
我们新建一个web-servlet-embedded工程,编写pom.xml如下:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>web-servlet-embedded</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
<tomcat.version>9.0.26</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
其中,<packaging>类型仍然为war,
引入依赖tomcat-embed-core和tomcat-embed-jasper,引入的Tomcat版本<tomcat.version>为9.0.26。
不必引入Servlet API,因为引入Tomcat依赖后自动引入了Servlet API。因此,我们可以正常编写Servlet如下:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
String name = req.getParameter("name");
if (name == null) {
name = "world";
}
PrintWriter pw = resp.getWriter();
pw.write("<h1>Hello, " + name + "!</h1>");
pw.flush();
}
}
public class Main {
public static void main(String[] args) throws Exception {
// 启动Tomcat:
Tomcat tomcat = new Tomcat();
tomcat.setPort(Integer.getInteger("port", 8080));
tomcat.getConnector();
// 创建webapp:
Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(
new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);
tomcat.start();
tomcat.getServer().await();
}
}
这样,我们直接运行main()方法,即可启动嵌入式Tomcat服务器,然后,
通过预设的tomcat.addWebapp("", new File("src/main/webapp"),Tomcat会自动加载当前工程作为根webapp,
可直接在浏览器访问http://localhost:8080/:
重定向和转发
转发和重定向的区别在于,转发是在Web服务器内部完成的,对浏览器来说,它只发出了一个HTTP请求:
// 构造重定向的路径:
String name = req.getParameter("name");
String redirectToUrl = "/hello" + (name == null ? "" : "?name=" + name);
// 发送重定向响应:
resp.sendRedirect(redirectToUrl);
转发forward
@WebServlet(urlPatterns = "/morning")
public class ForwardServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/hello").forward(req, resp);
}
}
Session和Cookie
JSESSIONID是由Servlet容器自动创建的,目的是维护一个浏览器会话,它和我们的登录逻辑没有关系;
登录和登出的业务逻辑是我们自己根据HttpSession是否存在一个"user"的Key判断的,登出后,Session ID并不会改变;
即使没有登录功能,仍然可以使用HttpSession追踪用户,例如,放入一些用户配置信息等。
除了使用Cookie机制可以实现Session外,还可以通过隐藏表单、URL末尾附加ID来追踪Session。这些机制很少使用,最常用的Session机制仍然是Cookie。
Servlet容器提供了Session机制以跟踪用户;
默认的Session机制是以Cookie形式实现的,Cookie名称为JSESSIONID;
通过读写Cookie可以在客户端设置用户偏好等
JSP是Java Server Pages
我们来编写一个hello.jsp,内容如下:
<html>
<head>
<title>Hello World - JSP</title>
</head>
<body>
<%-- JSP Comment --%>
<h1>Hello World!</h1>
<p>
<%
out.println("Your IP address is ");
%>
<span style="color:red">
<%= request.getRemoteAddr() %>
</span>
</p>
</body>
</html>
整个JSP的内容实际上是一个HTML,但是稍有不同:
包含在<%--和--%>之间的是JSP的注释,它们会被完全忽略;
包含在<%和%>之间的是Java代码,可以编写任意Java代码;
如果使用<%= xxx %>则可以快捷输出一个变量的值。
JSP页面内置了几个变量:
out:表示HttpServletResponse的PrintWriter;
session:表示当前HttpSession对象;
request:表示HttpServletRequest对象。
这几个变量可以直接使用。
jsp和servlet有什么区别
没有任何区别 本质jsp也是一个servlet 只不过无需映射路径
Web Server会根据路径查找对应的.jsp文件,
如果找到了,就自动编译成Servlet再执行。在服务器运行过程中,如果修改了JSP的内容,那么服务器会自动重新编译
JSP是一种在HTML中嵌入动态输出的文件,它和Servlet正好相反,Servlet是在Java代码中嵌入输出HTML;
JSP可以引入并使用JSP Tag,但由于其语法复杂,不推荐使用;
JSP本身目前已经很少使用,我们只需要了解其基本用法即可
MVC
Servlet适合编写Java代码,实现各种复杂的业务逻辑,但不适合输出复杂的HTML
JSP适合编写HTML,并在其中插入动态内容,但不适合编写复杂的Java代码
二者结合起来就是java版的php
怎么结合?
在UserServlet中,我们可以从数据库读取User、School等信息,然后,
把读取到的JavaBean先放到HttpServletRequest中,再通过forward()传给user.jsp处理:
@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 假装从数据库读取:
School school = new School("No.1 Middle School", "101 South Street");
User user = new User(123, "Bob", school);
// 放入Request中:
req.setAttribute("user", user);
// forward给user.jsp:
req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp);
}
}
在user.jsp中,我们只负责展示相关JavaBean的信息,不需要编写访问数据库等复杂逻辑:
需要展示的User被放入HttpServletRequest中以便传递给JSP,
因为一个请求对应一个HttpServletRequest,我们也无需清理它,
处理完该请求后HttpServletRequest实例将被丢弃;
把user.jsp放到/WEB-INF/目录下,是因为WEB-INF是一个特殊目录,
Web Server会阻止浏览器对WEB-INF目录下任何资源的访问,这样就防止用户通过/user.jsp路径直接访问到JSP页面;
JSP页面首先从request变量获取User实例,然后在页面中直接输出,此处未考虑HTML的转义问题,有潜在安全风险。
我们在浏览器访问http://localhost:8080/user,请求首先由UserServlet处理,然后交给user.jsp渲染:
简单说就是逻辑处理完,用forward到jsp渲染界面
简单介绍一下现在MVC的实现原理
现在我们都是xxxServlet来映射路径的
显然不友好
应该有Controller MoDel View
我们写Controller
@GetMapping("/signin")
public ModelAndView signin() {
...
}
在写model
ModelAndVIew
在写view
那这个Controller没有继承httpservlet是怎么实现的呢
其实是实现了一个统一的dispacther接口继承了httpServlet。
然后根据传入的路由的参数来匹配Controller来实现调用的
php里的框架也是这样实现的 在index.php里会匹配路由的然后再根据匹配到的路由去找controller处理逻辑的
Filter
有的界面是需要登录校验才能访问的
但是太多了总不能每一个都写一个校验把
为了把一些公用逻辑从各个Servlet中抽离出来,
JavaEE的Servlet规范还提供了一种Filter组件,即过滤器,
它的作用是,在HTTP请求到达Servlet之前,可以被一个或多个Filter预处理,类似打印日志、登录检查等逻辑,完全可以放到Filter中。
举例:对网站的统一编码进行设置
@WebFilter(urlPatterns = "/*")
public class EncodingFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("EncodingFilter:doFilter");
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}
编写Filter时,必须实现Filter接口,在doFilter()方法内部,
要继续处理请求,必须调用chain.doFilter()。最后,
用@WebFilter注解标注该Filter需要过滤的URL。这里的/*表示所有路径。
有些细心的童鞋会问,有多个Filter的时候,Filter的顺序如何指定?
多个Filter按不同顺序处理会造成处理结果不同吗?
答案是Filter的顺序确实对处理的结果有影响。但遗憾的是,
Servlet规范并没有对@WebFilter注解标注的Filter规定顺序。
如果一定要给每个Filter指定顺序,就必须在web.xml文件中对这些Filter再配置一遍。
注意到上述两个Filter的过滤路径都是/*,即它们会对所有请求进行过滤。也可以编写只对特定路径进行过滤的Filter,
例如AuthFilter:
@WebFilter("/user/*")
URL设置要合理,filere的逻辑会很清晰
Filter是一种对HTTP请求进行预处理的组件,它可以构成一个处理链,使得公共处理代码能集中到一起;
Filter适用于日志、登录检查、全局设置等;
设计合理的URL映射可以让Filter链更清晰
修改请求
使用filter是有弊端的,因为有的数据流被filter读取之后就没有了
例如上传文件
这里的原因是对HttpServletRequest进行读取时,只能读取一次。
如果Filter调用getInputStream()读取了一次数据,
借助HttpServletRequestWrapper,我们可以在Filter中实现对原始HttpServletRequest的修改
我们伪造一个httpservletRequest
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
chain.doFilter(new ReReadableHttpServletRequest(req, output.toByteArray()), response);
}
class ReReadableHttpServletRequest extends HttpServletRequestWrapper
在自己伪造的里边读取流保存起来再传出去
修改响应
请求可以修改继承HttpServletRequestWrapper 里边能修改
响应就能修改HttpServletResponseWrapper
Listener
通过Listener我们可以监听Web应用程序的生命周期,获取HttpSession等创建和销毁的事件;
ServletContext是一个WebApp运行期的全局唯一实例,可用于设置和共享配置信息
除了ServletContextListener外,还有几种Listener:
HttpSessionListener:监听HttpSession的创建和销毁事件;
ServletRequestListener:监听ServletRequest请求的创建和销毁事件;
ServletRequestAttributeListener:监听ServletRequest请求的属性变化事件(即调用ServletRequest.setAttribute()方法);
ServletContextAttributeListener:监听ServletContext的属性变化事件(即调用ServletContext.setAttribute()方法);
ServletContext
一个Web服务器可以运行一个或多个WebApp,对于每个WebApp,
Web服务器都会为其创建一个全局唯一的ServletContext实例,
我们在AppListener里面编写的两个回调方法实际上对应的就是ServletContext实例的创建和销毁:
public void contextInitialized(ServletContextEvent sce) {
System.out.println("WebApp initialized: ServletContext = " + sce.getServletContext());
}
ServletRequest、HttpSession等很多对象也提供getServletContext()方法获取到同一个ServletContext实例。
ServletContext实例最大的作用就是设置和共享全局信息。
此外,ServletContext还提供了动态添加Servlet、Filter、Listener等功能,它允许应用程序在运行期间动态添加一个组件,
虽然这个功能不是很常用。
部署
合理组织文件结构非常重要。我们以一个具体的Web应用程序为例:
webapp
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── itranswarp
│ └── learnjava
│ ├── Main.java
│ ├── filter
│ │ └── EncodingFilter.java
│ └── servlet
│ ├── FileServlet.java
│ └── HelloServlet.java
├── resources
└── webapp
├── WEB-INF
│ └── web.xml
├── favicon.ico
└── static
└── bootstrap.css
我们把所有的静态资源文件放入/static/目录,在开发阶段,有些Web服务器会自动为我们加一个专门负责处理静态文件的Servlet,
但如果IndexServlet映射路径为/,会屏蔽掉处理静态文件的Servlet映射。因此,我们需要自己编写一个处理静态文件的FileServlet
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ /static/* │
┌───────┐ ┌──────────> file
│Browser├────┼─┤ │ ┌ ─ ─ ─ ─ ─ ─ ┐
└───────┘ │/ proxy_pass
│ └─────────────────────┼───>│ Web Server │
Nginx
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘
实现上述功能的Nginx配置文件如下:
server {
listen 80;
server_name www.local.liaoxuefeng.com
# 静态文件根目录:
root /path/to/src/main/webapp;
access_log /var/log/nginx/webapp_access_log;
error_log /var/log/nginx/webapp_error_log;
# 处理静态文件请求:
location /static {
}
# 处理静态文件请求:
location /favicon.ico {
}
# 不允许请求/WEB-INF:
location /WEB-INF {
return 404;
}
# 其他请求转发给Tomcat:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
使用Nginx配合Tomcat服务器,可以充分发挥Nginx作为网关的优势,既可以高效处理静态文件,也可以把https、防火墙、限速、
反爬虫等功能放到Nginx中,使得我们自己的WebApp能专注于业务逻辑。