学习链接:https://www.liaoxuefeng.com/wiki/1252599548343744/1255945497738400

大部分内容来自此教程,以下只是我的学习笔记。

概念

JavaEE: Java Platform Enterprise Edition Java企业平台。它不是一个软件产品,是一种软件架构和设计思想。最核心的组件就是基于Servlet标准的Web服务器。

Web开发:编写服务器程序来处理客户端请求

HTTP协议

浏览器发出请求给web服务器,web服务器发送html网页给浏览器。发送的HTTP请求和响应格式例如:

  1. 请求
1
2
3
4
5
GET / HTTP/1.1
Host: www.sina.com.cn
User-Agent: Mozilla/5.0 xxx
Accept: */*
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8
  1. 响应
1
2
3
4
5
6
7
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 21932
Content-Encoding: gzip
Cache-Control: max-age=300

<html>...网页数据...

返回的响应码:

  • 1xx 服务器收到请求,需要请求者继续执行操作
  • 2xx 成功
  • 3xx 重定向
  • 4xx 客户端错误
  • 5xx 服务端错误

请求和响应报文处理细节:

HTTP请求和响应都由HTTP Header和HTTP Body构成,其中HTTP Header每行都以\r\n结束。如果遇到两个连续的\r\n,那么后面就是HTTP Body。浏览器读取HTTP Body,并根据Header信息中指示的Content-TypeContent-Encoding等解压后显示网页、图像或其他内容。

Servlet基础介绍

如果服务端要自己处理HTTP的请求,要包含很多动作,如识别请求头是否正确、复用TCP链接、异常处理等,非常繁琐。这些底层操作其他可以由JavaEE来操办,这样我们可以专注于实现我们真正的服务。

为了实现这一目的,JavaEE提供了Servlet API,我们使用Servlet API编写自己的Servlet来处理HTTP请求,Web服务器实现Servlet API接口,实现底层功能

这里的Servlet API是一个jar包,可以通过maven引入。

一个简单的代码例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 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()方法。我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequestHttpServletResponse就已经封装好了请求和响应。

如何运行我们的web应用

将我们servlet的web应用打包为war,需要将其放在支持Servlet API的Web服务器(例如开源免费的Tomcat)。这样就可以处理浏览器发送的请求。

传统的方式是:下载Tomcat,把生成的war放到webapps目录下,再启动。但是这种方法比较麻烦,不好调试。廖雪峰老师也介绍了更方便的方法:

  • maven引入Tomcat的jar包(Servlet API也包含在Tomcat的包里)
  • 编写自己的Servlet方法
  • 编写一个main方法,用于启动Tomcat服务器
  • 执行main方法即可

Servlet实现细节

注解

Servlet使用注解表示处理的路径。例如:

1
2
3
4
@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    ...
}

表示该Servlet能处理/hello路径的请求。一般是由Web Server统一接收请求,再dispatch(路径转发)到相应的Servlet去处理。

HttpServletRequest

封装的请求报文头。常用方法:

  • getMethod():返回请求方法,例如,"GET""POST"
  • getRequestURI():返回请求路径,但不包括请求参数,例如,"/hello"
  • getQueryString():返回请求参数,例如,"name=Bob&a=1&b=2"
  • getParameter(name):返回请求参数,GET请求从URL读取参数,POST请求从Body中读取参数;
  • getContentType():获取请求Body的类型,例如,"application/x-www-form-urlencoded"
  • getContextPath():获取当前Webapp挂载的路径,对于ROOT来说,总是返回空字符串""
  • getCookies():返回请求携带的所有Cookie;
  • getHeader(name):获取指定的Header,对Header名称不区分大小写;
  • getHeaderNames():返回所有Header名称;
  • getInputStream():如果该请求带有HTTP Body,该方法将打开一个输入流用于读取Body;
  • getReader():和getInputStream()类似,但打开的是Reader;
  • getRemoteAddr():返回客户端的IP地址;
  • getScheme():返回协议类型,例如,"http""https"

HttpServletResponse

封装的响应报文头。常用方法:

  • setStatus(sc):设置响应代码,默认是200
  • setContentType(type):设置Body的类型,例如,"text/html"
  • setCharacterEncoding(charset):设置字符编码,例如,"UTF-8"
  • setHeader(name, value):设置一个Header的值;
  • addCookie(cookie):给响应添加一个Cookie;
  • addHeader(name, value):给响应添加一个Header,因为HTTP协议允许有多个相同的Header;

注意:返回报文写入完毕后,不能用close(),要用flush()

多线程实现

一个Servlet类在服务器中只有一个实例,但对于每个HTTP请求,Web服务器会使用多线程执行请求。因此,一个Servlet的doGet()doPost()等处理请求的方法是多线程并发执行的。如果Servlet中定义了字段,要注意多线程并发访问的问题

重定向 Redirect

指的是服务器返回一个重定向指令,告诉浏览器地址已变,需要用新的url发送请求。虽然在用户的角度,是会自动跳转的(浏览器自动解析响应的location字段,并向新的url发送请求)。但是实际上浏览器请求了两次。

具体来说:

  1. 服务端在处理方法中,返回重定向响应
1
resp.sendRedirect(redirectToUrl);

2.浏览器接收到类似响应

1
2
HTTP/1.1 302 Found
Location: /hello

3.浏览器按照Location的新url发送新请求

重定向种类:

  • 永久重定向 301 (浏览器会缓存新旧url的关联,下次会直接请求到新的url)
  • 临时重定向 302

转发 Forward

指的是服务器内部转发。Servlet A 可以把请求转发给Servlet B来处理。与重定向的区别是,这种方式浏览器实际上只请求了一次。浏览器的地址栏上仍然是第一次请求的路径,也就是说浏览器其实并不知道发生了转发。

Session和Cookie

Session

基于唯一ID识别用户身份的机制称为Session。每个用户第一次访问服务器后,会自动获得一个Session ID。如果用户在一段时间内没有访问服务器,那么Session会自动失效。在Servlet中,可以使用HttpSession对象来维护。常见的场景就是记住用户登录后的身份。

使用Session时,由于服务器把所有用户的Session都存储在内存中,如果遇到内存不足的情况,就需要把部分不活动的Session序列化到磁盘上,这会大大降低服务器的运行效率,因此,放入Session的对象要小,通常我们放入一个简单的User对象就足够了

Cookie是一种用于在客户端(通常是Web浏览器)和服务器之间传递数据的小型文本文件。Cookie会被存储在用户的浏览器中,每次用户访问相关站点时,浏览器会将相应的Cookie信息包含在HTTP请求中发送到服务器。在Servlet中,可以使用Cookie对象来维护。

cookie的唯一性:Cookie的唯一性是由域名和名称共同决定的。在同一个域名下,每个Cookie的名称必须是唯一的。这意味着无论在哪个路径下创建,具有相同名称的Cookie都会被视为同一个Cookie。

举个例子,假设你在域名为"example.com"下创建了两个Cookie:

  1. 名称:“username”,路径:"/"
  2. 名称:“username”,路径:"/app"

尽管这两个Cookie的路径属性不同,但由于它们具有相同的名称和相同的域名,浏览器会将它们视为同一个Cookie。因此,在请求"/“路径下的资源时,浏览器会发送这两个Cookie,即使它们在不同的路径下创建。

这种特性使得在同一域名下的不同路径之间共享Cookie变得可能,但也需要注意确保不同路径下的Cookie名称不发生冲突,以避免不必要的问题。

JSP(Java Server Pages)

JSP的文件必须放到/src/main/webapp下,文件名以.jsp结尾。JSP和Servlet其实没有任何区别,因为JSP在执行前首先被编译成一个Servlet。

整个文件与HTML并无太大区别,但需要插入变量,或者动态输出的地方,使用特殊指令<% ... %>

JSP页面内置了几个变量:

  • out:表示HttpServletResponse的PrintWriter;
  • session:表示当前HttpSession对象;
  • request:表示HttpServletRequest对象。

MVC开发

初级阶段

MVC(Model-View-Controller) 设计模式 可以结合 Servlet和JSP 的优点。Servlet和JSP对比:

  • Servlet擅长逻辑处理,但是不适合复杂的HTML处理
  • JSP则相反

廖老师举了一个例子:

  1. 一个JavaBean对象,为User对象
  2. 一个Servlet对象,负责从数据库读取具体的user信息,并把读取到的JavaBean放到HttpServletRequest中,再通过forward()传给user.jsp处理
  3. 在user.jsp中,只负责展示获取到的JavaBean的信息。不需要处理复杂的逻辑。

也就是:在浏览器访问http://localhost:8080/user,请求首先由UserServlet处理,然后交给user.jsp渲染。在这个例子中:

  • Model = User对象
  • View = user.jsp
  • Controller = Servlet

进阶阶段

Spring MVC , 后面会学到

Filter 过滤器

在请求到达具体Servlet之前,可能还需要一些公共的处理逻辑等。因此,JavaEE的Servlet规范还提供了一组Filter组建。

它的作用是,在HTTP请求到达Servlet之前,可以被一个或多个Filter预处理,类似打印日志、登录检查等逻辑,完全可以放到Filter中。

具体的代码例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@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);
    }
}

@WebFilter注解表示了要过滤的URL。这种过滤类都需要implements Filter。并在doFilter中实现处理。要继续处理请求,必须调用chain.doFilter()(因此也可以不调用此方法实现中断后续的处理)。

修改请求和响应

有时候用到过滤器时,需要修改请求和响应对象。

Listener 监听器

Servlet定义了好几种监听器,其中最常用的是ServletContextListener。示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@WebListener
public class AppListener implements ServletContextListener {
    // 在此初始化WebApp,例如打开数据库连接池等:
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("WebApp initialized.");
    }

    // 在此清理WebApp,例如关闭数据库连接池等:
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("WebApp destroyed.");
    }
}

任何标注为@WebListener,且实现了特定接口的类会被Web服务器自动初始化。上述AppListener实现了ServletContextListener接口,它会在整个Web应用程序初始化完成后,以及Web应用程序关闭后获得回调通知。我们可以把初始化数据库连接池等工作放到contextInitialized()回调方法中,把清理资源的工作放到contextDestroyed()回调方法中,因为Web服务器保证在contextInitialized()执行后,才会接受用户的HTTP请求。

关于部署

一般一个具体的Web应用结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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

部署架构一般如下:

1
2
3
4
5
6
7
8
9
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐

             │  /static/*            │
┌───────┐      ┌──────────> file
│Browser├────┼─┤                     │    ┌ ─ ─ ─ ─ ─ ─ ┐
└───────┘      │/          proxy_pass
             │ └─────────────────────┼───>│  Web Server │
                       Nginx
             └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘    └ ─ ─ ─ ─ ─ ─ ┘

实现上述功能的Nginx配置文件如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;
    }
}