javaweb

Posted by zangxin on February 10, 2024

javaweb

XML

定义

  • 扩展标记语言

功能

  • 能存储复杂的数据结构

  • 可以作为在程序之间传输数据格式(被JSON取代)

  • 作为配置文件(主要: spring,maven等等)

  • 充当小型数据库

语法

  • 1.xml声明放在xml文档第一行

  • 2.xml声明包含version(基本不变为1.0), 和字符编码一般用utf-8

    1
    
    <?xml version="1.0" encoding="UTF-8"?>
    
  • 3.元素语法要求

    • 每个xml文档有且只有一个根元素

    • 根元素包括其他所有元素

    • 标签分为有标签体和没有标签体的

      • 有标签体

        1
        
        <a>www.google.com</a>
        
      • 无标签体

        1
        
        <a></a>简写为<a/>
        
    • 元素,标签(tag),节点术语指定是同一个东西

  • 4.属性

    • 属性值用双引号或者单引号分隔

    • 一个元素可以有多个属性, 基本格式为<元素名 属性名="属性值">

    • 属性名在一个标签中不能重复

    • 属性值不能包括&符号

  • CDATA节

    • CDATA节中所有字符都会被当做简单文本,而不是xml标记

      1
      
      <![CDATA[需要的内容]]>
      
  • 转义字符

    1
    
    &lt; &gt; &amp;(&) &quot;(") &apos;(')
    

xml解析

  • 原理,xml和html这类标记文档,可以通过w3c组织制定的dom技术解析xml获取dom对象

  • dom4j解析

    • 获取dom对象的3种方式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      // 从文件中解析
      SAXReader reader = new SAXReader();
      Document document = reader.read(new File("src/xml/students.xml"));
      // 从文本中解析
      String text = "<name>mary</name>";
      document = DocumentHelper.parseText(text);
      // 创建dom对象
      document = DocumentHelper.createDocument();
      document.addElement("name");
      
      • QName是标签名字
    • 遍历

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
      // ============遍历====================
      Element rootElement = document.getRootElement();
      // 获取root的两个直接子元素
      List<Element> elements = rootElement.elements();
      for (Element student : elements) {
      // 获取student的属性id
      String id = student.attributeValue("id");
      // 获取student子节点
      Element name = student.element("name");
      Element age = student.element("age");
      Element gender = student.element("gender");
      System.out.printf("%s,%s,%s,%s\n",
            id, name.getText(), age.getText(), gender.getText());
      }
      
    • 查找

      1
      2
      
      //=========== 获取第二个学生===========
      Element stu = (Element) rootElement.elements("student").get(1);
      
    • 增删改

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        
        //=========== 添加一个新学生=============
        Element newStu = DocumentHelper.createElement("student");
        Element newStuName = DocumentHelper.createElement("name");
        // 如何给元素添加属性
        newStu.addAttribute("id", "300");
        // 给元素添加Text
        newStuName.addText("tom");
        // 把子节点添加到父节点中
        newStu.add(newStuName);
        // 把学生节点加到根节点中
        rootElement.add(newStu);
        
      • 1
        2
        3
        
        // ============删除第三个学生=========================
        Element student = (Element) rootElement.elements("student").get(2);
        student.getParent().remove(student);
        
      • 1
        2
        3
        
        //==========更新第一个学生, 把学生的年龄+3============
        Element newAge = (Element) ((Element) (rootElement.elements("student").get(0))).elements("age").get(0);
        newAge.setText(Integer.parseInt(newAge.getText()) + 3 + "");
        
    • 保存修改到文件中

      1
      2
      3
      4
      5
      6
      
      // ========保存xml文件=========
      OutputFormat out = OutputFormat.createPrettyPrint();
      out.setEncoding("utf-8");
      XMLWriter xmlWriter = new XMLWriter(new FileOutputStream("src/main/java/xml/students.xml"), out);
      xmlWriter.write(document);
      xmlWriter.close();
      
  • 历史

    • 早期JDK提供两种xml解析技术DOM和Sax(Simple API for XML),现在二者已经过时

    • 第三方解析技术, jdom对dom封装, dom4j对jdom进行了封装

HTTP协议

命令

1
curl -i 'www.baidu.com'

请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /hi HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Cookie: Idea-3873738e=839fe1ca-6bc2-4055-bb09-70a4cd05d6f5
Host: localhost:43999
Pragma: no-cache
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
空行\r\n
请求体
  • 请求头

    • Host请求的主机端口

    • User-Agent: 浏览器信息

    • Accept: 浏览器可以接受的数据格式

    • Accept-Language:浏览器可以接受的语言,zh-CN,en-Us

    • Accept-Encoding:浏览器可以接受数据压缩方式

    • Connection: 告诉服务器处理连接的方式, keep-alive长连接, closed,立即关闭连接

    • Referer: 表示这个请求是从哪里来的(防止盗链)

    • Cookie, 浏览器每次请求时都会带上cookie

    • Content-Type:表示提交数据的格式 application/x-www-form-urlencoded 表示表单数据

    • content-length表示数据长度

    • Origin:表示请求是从那个主机发出的

响应

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
HTTP/1.1 200 
Content-Length: 321
Accept-Ranges: bytes
Connection: keep-alive
Content-Type: text/html
Date: Fri, 21 Feb 2025 06:19:23 GMT
Etag: W/"321-1740107657325"
Keep-Alive: timeout=4
Last-Modified: Fri, 21 Feb 2025 03:14:17 GMT
Proxy-Connection: keep-alive

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>Wel come hello</h1>
        <a href="ganyu-game.html">玩游戏吧</a>
        <br>
        <a href="hi">hi</a> <br>
        <a href="register.html">去注册</a>
    </body>
</html>
  • 响应头

    • Server: 服务器 Accept-Range: byte字节–>支持断点续传 ETag: 对资源的标识(类似一个token) Last-Modified: 返回资源的上次修改时间,可以用来判断是否使用缓存 Content-Type:表示返回资源类型 Content-Length:返回资源的字节长度,浏览器根据这个来判断文件是否结束了,不设置的话, 浏览器会一直转圈 Date: 服务器响应时间
  • 状态码

    • 200OK 302 重定向 304 Not Modified 404
    • 302, 有个location响应头, 表示重定向的资源url

重定向会发送至少两次请求

304, 当没有禁用浏览器缓存时, 响应头会添加if-modified-since:时间, 表示资源的最近修改时间,服务器会比较时间, 如果服务器的资源更新, 就会返回该资源, 如果没有修改, 就返回304状态码, 但是不会返回该资源

发送请求数量案例

请求了三次

1
2
3
index.html
<img src="a.jpg">
<img src="b.jpg">

get和post请求分别有哪些

  • get

    • 表单方法指定get, form, method=get a标签 link标签,引入css script标签,引入js脚本 img标签 iframe引入html页面 浏览器地址敲回车
  • post

    • 表单中方法指定post
  • 什么情况下使用post

    • get传输数据量小 get的密码会显示在url上

    • 查询使用get OK 修改/更新/删除可以使用post请求

MIME类型

  • MIME类型是HTTP协议中数据类型,在响应头中的Content-Type中指定

  • 常用的MIMIE类型

    • text/html text/plain image/gif application/json …

Tomcat+Servlet

Tomcat

  • 介绍

    tomcat也是java网络程序的一部分, 他负责处理sokcet和浏览器通信的过程, 我们负责业务问题, 把用户的请求入库写入数据库或者把数据库中的数据返回给tomcat,tomcat再返回给浏览器.

  • 配置

    • Tomcat的目录

      • bin: 存放启动和关闭服务的命令

      • conf 存放Tomcat服务的各种配置文件

        • server.xml 配置tomcat的基本设置,启动端口,关闭端口,主机名

        • web.xml 用来指定tomcat运行配置(比如servlet)

      • lib tomcat自己要用到的jar包

      • logs tomcat运行日志

      • webapps

        • web应用所在目录, 默认访问ROOT应用项目

        • web应用

          • web应用组成: html,图片,文本,声音,视频等,css,js,动态web页面, java程序(servlet), jar包,配置文件等

          • web应用目录规范

            • webapp目录

              • 静态文件,html/css/js等

              • WEB-INF目录

                • web.xml

                • lib

          • 部署方式

            • 方式1,将web工程目录copy到webapps目录下

            • 方式2,通过配置文件来部署(idea就是这样用这种方式的), 在tomcat/conf/Catalina/localhost/下,配置文件

          • 访问方式

            • http://ip:port/web应用目录名/请求的文件路径
      • work: tomcat的工作目录,存放解析后的jsp文件

  • 使用

    • 配置JAVA_HOME环境变量

    • 在idea中配置tomcat

      • 1.创建web项目

      • 2.edit configuration, 选择tomcat server

      • 3.maven打包方式选择war包

      • 4.rebuild项目后生成artifacts

      • 5,在第2步中选择deployment中选择artifacts

      • 6.直接执行或者拷贝artifacts到tomcat的webapps目录下

      • 注意, 编辑区的源码, 和运行时源码不是一个东西 编辑区的源码经过rebuild/build后才能生成可以运行的artifacts

  • tomcat底层原理

    • http请求过程

      • 启动tomcat, 完成web.xml配置Servlet,启动socketServer监听

        • http Request 开启一个线程去处理请求

          • 静态资源 直接返回静态资源

          • 请求Servlet ->调用Servlet的service方法

    • 启动tomcat, socket监听, 请求过来开启多线程 dom4j, 解析web.xml, 利用反射创建Servlet,

    • 拼接http请求的字符串

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      String content = """
      HTTP/1.1 200
      Content-Type: text/html;charset=utf-8
      Content-Length: %d
                    
      %s
      """;
      String s = "<h1>你好, Hello, Tomcat</h1>";
      content = String.format(content, s.getBytes().length, s);
      

实现简易版tomcat

核心类-加载配置-创建/调用Servlet

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.xxx.tomcat_;

import com.xxx.tomcat_.myservlet.MyHttpRequest;
import com.xxx.tomcat_.myservlet.MyHttpResponse;
import com.xxx.tomcat_.myservlet.MyHttpServlet;
import com.xxx.tomcat_.myservlet.MyServlet;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author zangxin
 * @version 1.0
 * @date 2025/2/22
 *
 *创建,调用,从xml读取Servlet配置信息
 */
public class ManageServlet {
    // url --- obj
    private static final Map<String, MyServlet> URL_SERVLET_MAP = new ConcurrentHashMap<>();
    public static final MyServlet NULLOBJ = new MyHttpServlet();
    // url --- class
    private static final Map<String, String> URL_CLASS_MAP = new HashMap<>();

    private static void doServlet() {

    }

    public static void loadServlet() {
        System.out.println("load servlet start ....");
        // 从web.xml文件中读取
        SAXReader saxReader = new SAXReader();
        try {
            Document document = saxReader.read(ManageServlet.class.getResourceAsStream("myservlet/web.xml"));
            Element rootElement = document.getRootElement();
            List<Element> elements = rootElement.elements("servlet-mapping");
            for (Element servletMapping : elements) {
                Element element = servletMapping.element("url-pattern");
                String urlPattern = element.getText();
                Element servletName = servletMapping.element("servlet-name");
                URL_CLASS_MAP.put(servletName.getText(), urlPattern);
            }
            List<Element> servlets = rootElement.elements("servlet");
            for (Element servlet : servlets) {
                String servletName = servlet.element("servlet-name").getText();
                String servletClass = servlet.element("servlet-class").getText();
                String urlPattern = URL_CLASS_MAP.get(servletName);
                URL_CLASS_MAP.put(urlPattern, servletClass);
                URL_CLASS_MAP.remove(servletName);
                URL_SERVLET_MAP.put(urlPattern, NULLOBJ);
            }
        } catch (DocumentException e) {
            throw new RuntimeException(e);
        }
        System.out.println("load servlet end, " + "load " + URL_SERVLET_MAP.size() + " servlets.");
    }

    public static void createServlet(String url) {
        String className = URL_CLASS_MAP.get(url);
        try {
            Class<?> clazz = Class.forName(className);
            Object o = clazz.newInstance();
            URL_SERVLET_MAP.put(url, (MyServlet) o);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static MyServlet getServlet(String url) {
        return URL_SERVLET_MAP.get(url);
    }

    public static boolean containsUrl(String url) {
        return URL_SERVLET_MAP.containsKey(url);
    }

    public static void doService(String url, MyHttpRequest request, MyHttpResponse response) {
        String className = URL_CLASS_MAP.get(url);
        try {
            Class<?> clazz = Class.forName(className);
            Method service = clazz.getMethod("service", MyHttpRequest.class, MyHttpResponse.class);
            service.invoke(URL_SERVLET_MAP.get(url), request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

核心类-http通信

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.xxx.tomcat_;

import com.xxx.tomcat_.myservlet.CalTwoSumServlet;
import com.xxx.tomcat_.myservlet.MyHttpRequest;
import com.xxx.tomcat_.myservlet.MyHttpResponse;
import com.xxx.tomcat_.myservlet.MyServlet;
import com.xxx.util.StreamUtils;

import java.net.ServerSocket;
import java.net.Socket;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.xxx.tomcat_.ManageServlet.NULLOBJ;

/**
 * @author zangxin
 * @version 1.0
 * @date 2025/2/22
 * 和2一样, 这里要支持MyServlet系列接口
 */
public class MyTomcat3 {
    public static void main(String[] args) throws Exception {
        System.out.println("tomcat 在43999监听");
        ServerSocket serverSocket = new ServerSocket(43999);
        // 加载Servlet从xml文件或者注解中
        ManageServlet.loadServlet();
        while (true) {
            Socket socket = serverSocket.accept();
            new HttpRequestThread3(socket).start();
        }
    }
}

class HttpRequestThread3 extends Thread {
    private Socket socket;

    public HttpRequestThread3(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            MyHttpRequest request = new MyHttpRequest(socket.getInputStream());
            MyHttpResponse response = new MyHttpResponse(socket.getOutputStream());

            // index页面
            String url = request.getUrl();
            if (url.equals("/")) {
                response.write(StreamUtils.streamToString(CalTwoSumServlet.class.getResourceAsStream("index.html")));
            }
            // 查找静态资源 html
            // 匹配文件扩展名
            Matcher matcher = Pattern.compile("(/)(\\w+.\\w+$)").matcher(url);
            if (matcher.find()) {
                String resource = matcher.group(2);
                System.out.println(resource);
                if (resource.substring(resource.lastIndexOf(".") + 1, resource.length()).equals("html")) {
                    response.write(StreamUtils.streamToString(CalTwoSumServlet.class.getResourceAsStream(resource)));
                }
            }
            // 根据url查找Servlet
            if (ManageServlet.containsUrl(url)) {
                MyServlet servlet = ManageServlet.getServlet(url);
                if (servlet == NULLOBJ) {
                    ManageServlet.createServlet(url);
                }
                ManageServlet.doService(url, request, response);
            }
            response.write("404 NOT FOUND");

            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

request

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.xxx.tomcat_.myservlet;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;

/**
 * @author zangxin
 * @version 1.0
 * @date 2025/2/22
 */
public class MyHttpRequest {
    private String url;
    private String method;
    private InputStream in;
    private HashMap<String, String> paramMap = new HashMap<>();

    public MyHttpRequest(InputStream inputStream) {
        // 封装请求
        this.in = inputStream;
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        try {
            // 解析请求行
            String requestLine = br.readLine();
            // 请求行
            // GET /?name=ggstar HTTP/1.1
            String[] lines = requestLine.split(" ");
            method = lines[0];
            url = lines[1];
            if (url.contains("?")) {
                url = url.substring(0, url.indexOf("?"));
                // 获取参数
                String pls = lines[1].substring(lines[1].indexOf("?")+1);
                String[] parameters = pls.split("&");
                for (String parameter : parameters) {
                    String[] kv = parameter.split("=");
                    if (kv.length == 2)
                        paramMap.put(kv[0], kv[1]);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }


    public String getMethod() {
        return method;
    }


    public String getParameter(String name) {
        return paramMap.get(name);
    }

    public String getUrl() {
        return url;
    }
}

response

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
34
35
36
37
38
39
40
41
42
43
44
45
package com.xxx.tomcat_.myservlet;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

/**
 * @author zangxin
 * @version 1.0
 * @date 2025/2/22
 */
public class MyHttpResponse {

    private OutputStream out;

    public MyHttpResponse(OutputStream out) {
        this.out = out;
    }

    public OutputStream getOut() {
        return out;
    }

    String httpFormat = """
            HTTP/1.1 200
            Content-Type: text/html;charset=utf-8
            Content-Length: %d
            KeepAlive: 3600
                      
            %s
            """;

    public String write(String content) {
        String format = String.format(httpFormat, content.getBytes().length, content);
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
        try {
            writer.write(format);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return format;
    }
}

servlet接口

1
2
3
4
5
public interface MyServlet {
    void init();
    void service(MyHttpRequest request, MyHttpResponse response);
    void destroy();
}

servlet实现类

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
public class MyHttpServlet implements MyServlet {
    @Override
    public void init() {

    }

    @Override
    public void service(MyHttpRequest request, MyHttpResponse response) {
        if (request.getMethod().equals("GET")) {
            doGet(request, response);
        } else if (request.getMethod().equals("POST")) {
            doPost(request, response);
        }
    }

    public void doPost(MyHttpRequest request, MyHttpResponse response) {

    }

    public void doGet(MyHttpRequest request, MyHttpResponse response) {

    }

    @Override
    public void destroy() {

    }
}

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <servlet>
        <servlet-name>CalTwoSumServlet</servlet-name>
        <servlet-class>com.xxx.tomcat_.myservlet.CalTwoSumServlet</servlet-class>
        <load-on-startup>99</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>CalTwoSumServlet</servlet-name>
        <url-pattern>/cal</url-pattern>
    </servlet-mapping>
    <servlet>
        <servlet-name>test</servlet-name>
        <servlet-class>com.xxx.tomcat_.myservlet.CalTwoSumServlet</servlet-class>
        <load-on-startup>99</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>test</servlet-name>
        <url-pattern>/ff</url-pattern>
    </servlet-mapping>
</web-app>

servlet

  • 定义:Servlet是javaEE规范, 是一套接口 由服务器厂商实现该接口

    • servlet和tomcat的关系: tomcat实现了Servlet接口

    • 使用时,在Servlet实现业务功能

  • Servlet接口

    • 生命周期方法

      • init

        请求url时,Servlet如果存在, Tomcat就创建实例,再会调用该方法, 只调用一次(Servlet是懒加载的,只有在有请求时, 才创建Servlet对象)

      • service

        从创建开始,服务直到销毁, 可被多次调用 可以处理get和post请求

        1.每收到一个请求, 服务器就会产生一个新的线程去处理 2.创建一个用于封装HTTP请求消息的ServletRquest和HTTP响应对象ServletResponse 3.然后调用service方法,并将请求和响应对象作为参数传递

      • destroy

        • 销毁时调用,只调用一次

        • 使用kill -9 时不会调用

  • 使用方法

    • 开发方式

      • 配置文件web.xml

        1
        2
        3
        4
        5
        6
        7
        8
        9
        
        <servlet>
         <servlet-name>
         <servlet-class>
          设置tomcat启动时加载Servlet, 对抗懒加载
          <load-on-startup>99</load-on-startup>
              
        <servlet-mapping>
        	<servlet-name>
          <url-pattern>
        

        在第二次请求时: 1.查询web.xml, url–>从map中查询获取Servlet对象–>拿到对象,调用service方法,为用户服务

        • 请求过程, 如果是第一次请求 1.查询web.xml 2.查看请求的资源/helloServlet, 在web.xml配置url-pattern 3.如果找到类url-pattern, 就得到了HelloServlet对象 4.Tomcat维护了一个大的HashMap<id, Servlet> 查询该Map,看看是否有Servlet实例 5.如果没有查询到Servlet对应id, 即Servlet还不存在 6.就根据servlet-name去得到servlet-class的类路径 7.反射实例化servlet,调用init()方法, 在放入到map中, 便于以后查询

        • servlet默认是单例的, 从刚才的hashmap维护url —> Servlet对象可以看出来
      • 注解开发

        • 在我们定义Servlet类上面写上下面的注解

          1
          2
          3
          4
          
          @WebServlet(urlPatterns = {"/as", "/as1","/as2"})
          // 或者
          // name和value不能重复(和其他Servlet一样)
          @WebServlet(name = "helloServlet", value = "/hello-servlet") 
          
        • 原理

          包扫描, 某个类上面有WebServlet注解时就认为它是一个Servlet—>放入hashmap中

          注解开发方式和web.xml一样, 相当于web.xml的语法糖

    • 共通线

      1.引入servlet-api和jsp-api依赖

      2.写Servlet

      写一个类继承HttpServlet类

      或者通过idea自动生成

      3.实现doGet方法和doPost方法

      • 父类的HttpServlet.service被我们的Servlet方法继承了, 在调用时并不直接调用service方法, 而是调用service方法, 判断是get还是post等请求, 然后if判断后再去调用对应doGet方法和doPost方法(动态绑定机制)

      • 路径匹配

        • 精准匹配

          • /ok 只匹配/ok这一个url
        • 目录匹配

          • /ok/* 匹配前缀是/ok/的
        • 扩展名匹配

          • *.action 后缀是.action, 比如/ab/c/e/gg.action
        • 任意匹配

          • /和/*

            • 匹配所有请求

            • 连静态资源html,css,js,图片等静态资源都会匹配😅

            • 会覆盖了tomcat的web.xml中配置的DefaultServlet, 导致处理不了静态资源, 建议不要使用

        • 优先级: 精准路径>前缀>后缀>任意匹配

        • 😅不要发明新匹配, /*/a.html, //b.html, /*/ab_.html, 都不生效, 只要上面四种有效

  • Repsonse

    • 响应中文乱码时,加上配置

      • response.setContentType(“text/html;utf-8”); response.setCharacterEncoding(“utf8”);
    • 常用方法

      • // 添加content-Type请求头,告诉浏览器用html方式来解析 response.setContentType(“text/html;utf-8”);

      • // 设置状态码 setStatus

      • // 设置响应头 setHeader

      • // 获取字符流对象用来给client返回数据 getWriter

  • 两个流在同一时间只能使用一个

  • // 获取字节流对用来给client返回二进制文件 getOutputStream

  • Request

    • 请求参数值乱码时,加上配置

      • request.setCharacterEncoding(“utf8”);
    • HttpServletRequest表示客户端的请求, http请求头中的信息都封装在这个对象,通过对象的方法可以获取客户端的信息

    • 常用方法

      • getRequestURI : /req

      • getRequestURL: http://localhost:43999/req

      • getRemoteAddr

        • 客户端地址
      • getHeader(“referer”)

        • 获取请求头
      • getParameter(“username”)

        • 获取请求参数(表单中的,和url后面都可以)
      • getCookies

        • 获取cookie
      • getParameterValues(“爱好”)

        • 获取checkbox复选框中多值数据
      • getMethod

        • 获取请求方法
      • setAttribute/getAttribute

        • 设置/获取域数据
      • request.getRequestDispatcher(“/”).forward(request, response)

        • 转发
  • ServletConfig

    • 为Servlet提供配置信息的类 配置信息不可更改(没有set方法)

      • 定义配置信息: 在web.xml中配置

        1
        
        <Servlet>配置信息<init-param>
        
      • 获取配置信息:

        1
        2
        
        ServletConfig servletConfig = this.getServletConfig();
        servletConfig.getInitParameter("username")
        
    • 由tomcat创建

    • ServletConfig在Servlet程序创建时,就创建了一个对应的ServletConfig对象

    • 重写init方法时(有参的那个)需要调用super.init(有参)的那个方法, 完成父类的初始化

  • ServletContext

    • Servlet上下文对象,为所有Servlet提供配置信息 配置信息不可更改,由set方法,但改了就报异常

      • 定义配置信息: 在web.xml中配置

        1
        
        <context-param>
        
      • 获取配置信息:

        1
        2
        
        ServletContext context = getServletContext()
        String appName = context.getInitParameter("appName");
        
        1
        2
        3
        4
        
        // 获取工程路径: /j2025/javaweb 在idea的edit configuration的deployment的application Context中配置的路径
        System.out.println(context.getContextPath());
        // 获取项目发布后的绝对路径: ~/IdeaProjects/j2025/out/artifacts/fishWeb_Web_exploded/  --> ROOT
        System.out.println(context.getRealPath("/"));
        
    • 一个web工程只有一个ServletContext实例

    • 由tomcat创建,在web工程启动时创建, 在web工程停止时销毁

    • 获取Context, this.getServletContext

    • 域对象:web应用中所有Servlet共享同一个Context对象, Servlet可以借助Context对象来通信

      • 传递数据方法是: setAttribute(String name, Object object); public Object getAttribute(String name); 而不是getInitParameter
  • 转发和重定向

    • 请求转发

      • 定义

        请求转发指的是一个web资源收到客户端的请求后, 通知服务端去调用另外一个web资源进行处理

      • 使用

        • request.getRequestDispatcher(“资源uri”).forward(request, response);

        • request对象同时也是一个域对象,通过request对象在实现转发时, 可以通过request对象把自己的数据带给其他web资源

          • setAttribute getAttribute removeAttirbute getAttributeNames
      • 注意事项

        • 请求转发的地址栏不会发生改变, 状态码是302

        • 在同一次http请求中,进行多次转发, 仍然是一次HTTP请求

        • 转发链上的servlet可以共享request域中的数据, 因为是同一个request对象

        • 可以转发到WEB-INF目录下

        • 不能访问到WEB工程外的资源

        • 因为浏览器地址栏会停止在第一个servlet, 如果刷新页面,会再次发生请求并携带数据, 所以在支付页面上, 不要使用请求转发, 否则会造成重复支付

    • 重定向

      • 定义

        一个web资源收到客户端请求后, 通知客户端去访问另外一个web资源 (2次请求, 多跑一趟)

      • 用法

        • 第一种方法: reponse.sendRedirect(“资源位置”) (推荐)

        • 第二种方法 response.setStatus(302) response.setHeader(“Location”,”/downloadNew)

      • 注意事项

        • 浏览器url地址是重定向的地址, 是2次http请求

        • 第一次请求响应码为302,响应头Location为重定向的地址

        • 重定向是浏览器来解析, 不是服务端解析,所以url最好写工程的全路径, /xxxServlet/xxx/bbServlet

          • 动态获取资源绝对路径:

            1
            
            sendRedirect(getServletContext.getContextPath + "/resoucrepath")
            
        • 不能共享Request域中的数据, 本质上是两次http请求, 会生成两个Request对象

        • 不能重定向到/WEB-INF目录下的资源

        • 可以重定向web工程以外的资源,比如www.google.com

      • 典型应用场景

        • 网站域名迁移

会话技术Cookie和Session

  • cookie原理

    • Cookie创建过程

      • 浏览器请求服务器时,,服务器创建Cookie对象, 添加到响应中,并返回给浏览器

        1
        2
        
        Cookie cookie = new Cookie("userId","123")
        response.addCookie(cookie)
        

      浏览器收到http请求时, 解析响应头 Set-Cookie: userId=123 把userId=123保存在本地cookie中

    • Cookie是服务器在客户端保存用户的信息, 比如登录名, 浏览历史等, 就可以使用Cookie方式保存

    • Cookie的数据量不大, 服务器在需要时可以从客户端/浏览器读取Cookie

      • 浏览器每次请求时,都会携带Cookie请求头

        • Cookie: JSESSIONID=E72676B5952D59BD7A5BA5BE3FE88568; userId=123
    • Cookie是保存在浏览器上的(其实是浏览器保存到本地文件中的, 很多网站在访问时都要求你同时使用Cookie,否则网站不能正常使用

  • Cookie常用方法

    • Cookie有点像一张表, 分为两列, 一个是那么, 一个是value, 数据类型都是String

    • 创建Cookie, cookie的名字是唯一的

      1
      2
      
      Cookie c = new Cookie(String name, String val);
      c.setMaxAge();// 保存时间
      
    • 把创建的Cookie添加到客户端

      1
      
      response.addCookie(c)
      
    • 4.读取Cookie

      1
      
      request.getCookies();
      

      获取Cookie和创建Cookie和更新Cookie(更改Cookie和创建Cookie方法是一样的)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
      Cookie[] cookies = request.getCookies();
      boolean isCookieExist = false;
      for (Cookie cookie : cookies) {
        if (cookie.getName().equals("userId")) {
          Cookie userId = new Cookie("userId", WebUtil.parseInt(cookie.getValue()) + 1 + "");
          response.addCookie(userId);
          isCookieExist = true;
        }
      	System.out.printf("%s = %s\n", cookie.getName(), cookie.getValue());
      }
      if (!isCookieExist) {
        Cookie userId = new Cookie("userId", "123");
        response.addCookie(userId);
      }
      
  • JSESSIONID

    • 服务器用来区分和哪个浏览器会话, 即用来区别不同的会话
  • Cookie的生命周期

    • cookie的声明周期是指Cookie什么时候被销毁

    • 通过setMaxAge方法设置多久过期

      • 正数: 多少秒后过期

      • 负数, 表示浏览器关闭, Cookie就会被删除(默认值是-1, 会话级别)

      • 0, 表示马上删除

      • 响应头会加上一个max-Age和过期事件 Set-Cookie: userId=130; Max-Age=7200; Expires=Sat, 22 Feb 2025 17:25:30 GMT

  • Cookie的path属性

    • Cookie的path属性可以有效的过滤哪些Cookie可以分爱送给服务器, 哪些不发给服务器, path属性是通过请求的地址来进行有效的过滤

      • Set-Cookie: password=123root; Path=/webpath/CookiePathServlet
    • 规则

      • cookie1.setPath=/webpath cookie2.setPath=/webpath/aaa 请求地址http://ip:port/webpath/resource cookie1会发送给服务器 cookie2不会发送给服务器 请求地址 http://ip:port/webpath/aaa/资源 cookie1会发送 cookie2会发送 path是前缀匹配的
  • 细节

    • 一个Cookie只能标识一种信息, 他有一个name和value对

    • 一个web站点可以给一个浏览器发送多个Cookie, 一个浏览器也可以存储多个web站点提供的cookie

    • cookie的总数没有限制, 但是每个域名的cookie数量和cookie大小是有限制的, cookie不适合存储数量大的信息

    • 删除cookie时, 必须保持path一致, 否则不能删除

    • 中文cookie一般来说不支持(设置字符集为utf8时可以支持),也可以使用URL编码来解决, URLEncoder,URLDecoder

Session

  • 定义

    • Session是服务端技术, 服务器在运行时为每一个用户的浏览器创建一个其独享的Session对象/集合

    • 由于session为各个用户浏览器独享, 所以在用户访问服务器的不同页面时, 可以从各自的Session中读取/添加数据,从而完成响应任务

  • 原理

    • 创建过程

      • 当用户请求服务器时, 操作Session时(调用request.getSession(方法)), 服务器就会在内存中为该浏览器分配一个Session对象, 该Session对象被这个浏览器(会话)独占

      • 请求是否有jsessionid的cookie

          • 请求时,如果没有携带jsessionid, 新建session对象, 分配jessionId: 响应时添加set-cookie: jsessionid=xxxxxxx,响应头, 添加jsessionid的cookie
          • 是否有id=jsessionid的Session对象

              • 获取Session,操作session
              • 这是情况, 是服务端重启, 浏览器没有关闭 创建session对象分配jsessionid, 注意这个sessionid和原来的不一样
    • 单个Session的结构

      • 类似一个hashmap对象, 有键值对<String,Object>
    • 服务端多个Session结构

      • Map<jsessionid, Session> —> Map<jsessionid, Map<String, Object>(实际上是concurrentHashMap)
  • 使用

    • 常用方法

      • 创建Session

        • 获取和当前Request关联的session, 如果有就返回, 如果没有就创建一个session request.getSession()
      • 向session添加属性

        • addAttribute(String name, Object val)
      • 获取session的某个属性

        • getAttribute(String name)
      • 删除

        • removeAttribute
      • 判断是否为新建的session

        • isNew()
      • 获取session的标识Id

        • 每个session都有一个唯一的标识id getId()方法可以获取
  • 生命周期

    • 默认生命周期

      • 在Tomcat的web.xml中可以配置, 默认是30min

        1
        
        <session-config>
        
    • 通过方法设置过期时间

      • session.setMaxInactiveInterval(30)

        • 正数, 多少秒过期

        • 负数, 永不过期

          • 服务端重启就没了
      • 销毁session对象

        • session.invalidate()
    • Session的生命周期指的是, 客户端/两次请求最大间隔时长, 而不是累计时长. 当客户端访问了自己的session, Session的生命周期将会从0开始计算(同一个会话两次请求之间的间隔)

    • tomcat使用一个线程来轮询会话状态, 如果某个会话的空闲事件超过时间设定的最大值, 则会销毁该Session

  • session能做什么

    • 网上商城的购物车

    • 保存用户的信息

    • 将数据放入Session中,供用户在访问不同的页面时, 实现跨页面访问数据

    • 防止用户没有登录, 就进入到登录才能访问的页面

监听器和过滤器

监听器

  • 定义

    • javaweb的三大组件, 监听器监听某种变化(对象创建/销毁,属性变化),触发对应方法完成任务
  • 常用监听器

    • ServletContextListener接口

      • 作用:监听ServletContext创建或者销毁(web应用启动时, 就会创建ServletContext),即生命周期监听, 应用: 加载spring的配置文件, 任务调度(配合定时器Timer/TimerTask)

      • 常用方法

        • 创建时触发 public void contextInitialized(ServletContextEvent sce)

        • 销毁时触发 public void contextDestroyed(ServletContextEvent sce)

      • 使用

        • 定义一个类实现ServletContextListener接口,在web.xml中写标签

          1
          
           <listener>
          
    • ServletContextAttributeListener

      • 作用监听ServletContext属性变化

      • 方法

        • attributeAdded

          • 添加属性时调用
        • attributeRemoved

          • 删除属性时调用
        • attributeReplaced

          • 替换属性值时调用
    • HttpSessionListener

      • 作用:监听Session的创建或销毁(生命周期)

      • 方法

        • sessionCreated

          • session创建时调用
        • sessionDestroyed

          • session销毁时调用
      • 使用案例, 监控用户上下线

    • HttpSessionAttributeListener

      • 作用:监听Session属性的变化

      • 方法

        • attributeAdded

          • 添加属性时调用
        • attributeRemoved

          • 删除属性时调用
        • attributeReplaced

          • 替换属性值时调用
    • ServletRequestListener

      • 监听request的创建和销毁

      • 方法

        • requestInitialized

        • requestDestroyed

      • 使用案例,可以用来监控某个IP访问网站的频率, 日志记录,访问资源

    • 其他监听器

      • ServletRequestAttributeListener

      • HttpSessionBindingListener

      • HttpSessionActivationListener

过滤器

  • 过滤器的必要性

    • 登录校验, 权限校验,记录日志,事务处理等等,如果在每个Servlet前面加上校验就很繁琐,代码冗余
  • 定义

    • javaweb的三大组件之一,过滤器Filter是javaEE的接口, 有三个方法init, doFilter,destroy, 需要在web.xml中配置标签和过滤路径

      1
      
      <filter>
      
  • 作用

    • 拦截请求

    • 过滤响应

  • 原理

    • 1.在web.xml中配置过滤器, 指定过滤规则url-pattern 2.如果匹配url-pattern,就走过滤器 3.如果不匹配就直接访问

    • 生命周期

      • Filter默认不是懒加载

      • Filter接口有三个方法init, doFilter, destroy分别对应Servlet的init,service, destroy基本一致

    • tomcat在反射调用时Servlet.service方法之前会判断filterMap的url路径和Servlet路径是否匹配, 匹配就会在调用service之前调用doFilter方法, 如果放行, 就继续调用service, 如果不放行就不用调用service

    • filter在web项目启动时, 有tomcat创建, 只会创建一次, 创建成功时会调用init方法, 只调用一次, 在创建filter时会创建一个filterConfig对象,通过init方法传入, 通过filterConfig对象,可以获取都在web.xml中filter的配置信息, 当一个http请求的uri路径和filter的url-pattern匹配时, 就会调用doFilter方法, 在调用doFilter方法时, tomcat会传入request和Response和FilterChain三个参数, 并通过Filter传入, 如果后面请求的数据库请求目标资源,jsp,Servlet,会使用到request, 比如登录校验, 过滤脏词

    • 放行

      • 调用filterChain.doFilter方法,如果没有写这个方法会返回空白页面,一般写个转发或者重定向到登录页面去

      • 放行后, 会继续访问目标资源, ServletRequest, ServletResponse会继续传递给目标资源

    • FilterConfig的方法

      • getServletContext 获取应用域 getInitParameter 获取配置参数By参数名 getInitParameterNames 获取所有配置参数名 getFilterName 获取过滤器的名字

        • 使用案例,封杀ip

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          
          public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){
            // 封杀ip, 在init方法中给banIp赋值, 从initParam中获取
            if (request.getRemoteAddr().startsWith(banIp)) {
            System.out.println("~~~~封杀该网段ip~~~ " + request.getRemoteAddr());
            ((HttpServletResponse) response)
                  .sendRedirect(request.getServletContext().getContextPath() + "/login.jsp");
            } else {
            System.out.println("非目标ip段放行~~~~~~~ " + request.getRemoteAddr());
            chain.doFilter(request, response);
          	}
          }
          
  • 细节说明

    • 注意tomcat的版本号和Servlet-api依赖的版本号之间的对应关系

    • url-pattern

      • 和Servlet匹配模式一样, 注意是服务端, 所以 / 表示工程路径名/后面的东西,
      • 注意/*表示拦截所有请求, / 什么都拦截不了,
      • 后缀匹配, *.do, *.action, *
      • 前缀匹配/xxx/, 不能在匹配文件名,filter只关心路径是否匹配, 至于路径后的资源是否存在, 它不管
  • 过滤器链

    • FilterChain:在处理复杂业务时, 一个过滤器不够, 可以设计多个过滤器共同完成任务,形成过滤器链

    • 原理

      • FilterChain执行顺序: Http请求–>AFilter–>AFilter的前置代码–>AdoFilter–>BFilter–>BFilter前置代码–>BdoFilter–>目标资源–>B后置代码–>A后置代码–>返回给浏览器页面/数据

      • 多个Filter和目标资源在一次请求, 在同一个线程中

      • 当一个请求的url和Filter的url-patternP匹配时,才会执行, 如果有多个匹配, 就会形成一个filter调用链(栈数据结构)

      • 多个filter共同执行时, 因为是同一个http请求, 所以共同使用一个reqeust对象

      • 多个Filter执行顺序和和web.xml的配置顺序保持一致

      • chain.doFilter(req,resp)方法, 将执行下一个过滤器的doFilter方法, 如果后面没有过滤器, 则执行目标资源

  • 转发不走过滤器

    • 转发默认不走过滤器,过滤器默认不拦截内部的请求

    • 要想走过过滤器需要配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
      <filter>
        <filter-name>TestFilter</filtername>
        <filter-class>anni.TestFilter</filter-class>
      </filter>
      <filter-mapping>
        <filter-name>TestFilter</filtername>
        <url-pattern>/*</url-pattern>
      <!-- 让转发走过滤器的配置 -->  
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>INCLUDE</dispatcher>
        <dispatcher>EXCEPTION</dispatcher>
      </filter-mapping>
      

文件上传/下载

上传

  • 准备jar包 commons-fileupload commos-io

  • 表单属性设置

    • input的type指定为file

    • 方法为post方法(文件比较大)

    • form的enctype(encodetype)编码类型,默认是application/x-www-form-urlencoded即url编码, 这种编码不适合二进制数据提交, 一般适用于文本

    • 如果要进行二进制文件提交, enctype要设置为multipart/form-data, 表示表单提交的数据有多个部分组成, 也就是可以提交二进制数据和文本数据, 使用了该类型, 后端使用request.getParameter就失效了

  • 服务端接收

    • 判断是否为一个文件表单

    • 判断表单提交的各个表单项是什么类型

    • 如果是一个普通的表单项, 就按照文本的方式来处理

    • 如果是一个文件表单项(二进制数据)使用IO技术进行处理

    • 把表单提交的文件保存到指定的目录下

    • 文件覆盖问题

      • 对上传的文件名进行处理, 前面加上一个前缀(UUID)或时间戳,保证是唯一的
  • 实例

    • 前端

      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
      
      <%@ page contentType="text/html;charset=UTF-8" language="java" %>
      <html>
          <head>
              <title>上传文件</title>
              <base href="${pageContext.request.getContextPath()}">
              <script src="js/jquery-3.7.1.min.js" type="text/javascript"></script>
              <script type="text/javascript">
                  function prev(event) {
                      // 获取文件对象
                      let file = event.files[0];
                      // 获取文件阅读器
                      let reader = new FileReader();
                      reader.readAsDataURL(file);
                     // base64的形式的图片
                      reader.onload = function () {
                          $("#preView").attr('src', this.result)
                      };
                  }
              </script>
          </head>
          <body>
              <form action="UploadServlet" method="post" enctype="multipart/form-data">
                  图片:<img src="img/a.jpeg" width="200px" id="preView"> <br>
                  <input type="file" name="pic" value="2xxx.jpg" onchange="prev(this)"><br>
                  图片人物:<input type="text" name="username"><br>
                  <input type="submit" value="上传">
              </form>
          </body>
      </html>
      
    • 后端

      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
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      
      // 判断是否为文件表单(enctype=multipart/form-data)
      if (ServletFileUpload.isMultipartContent(request)) {
        System.out.println("是文件表单");
        // 创建一个
        DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
        ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
        try {
          // servletFileUpload对象可以表单提交的数据text/文件,将其封装到FileItem文件项中
          // name=a.jpeg, StoreLocation=~/tomcat/temp/upload_45fb0d4e_b017_47d2_a35c_7e1f3cc4f6c6_00000000.tmp,
          // size=295675 bytes, isFormField=false, FieldName=pic
          // name=null, StoreLocation=null, size=5 bytes, isFormField=true, FieldName=name
          List<FileItem> fileItems = servletFileUpload.parseRequest(request);
          for (FileItem fileItem : fileItems) {
            System.out.println(fileItem);
            // 非文件表单项,即type!=file
            if (fileItem.isFormField()) {
              // username的value = ganyu
              // System.out.println(fileItem.getFieldName() + "的value = " + fileItem.getString());
            } else {
              // a.jpeg
              String filename = fileItem.getName();
              // 加上随机前缀,避免文件覆盖
              filename = UUID.randomUUID() + "_" + System.currentTimeMillis() + "_" + filename;
              // 配置上传的文件夹,优化文件夹包含文件文件大小,每天的文件放入不同的文件夹/20010101
              String filepath = getDirNameByDate();
              // 获取工程下的路径作为上传目录
              String fileRealPath = request.getServletContext().getRealPath(filepath);
              File uploadDir = new File(fileRealPath);
              // 如果文件夹不存,则创建文件夹
              if (!uploadDir.exists()) {
                uploadDir.mkdirs();
                System.out.println("创建文件上传保存目录: " + uploadDir);
              }
              // 剪切文件到指定的文件夹中,因为默认位StoreLocation在tomcat的temp目录下
              File file = new File(uploadDir, filename);
              fileItem.write(file);
              System.out.println("上传文件成功: " + file);
            }
            // 日期文件名
            private static String getDirNameByDate() {
              // /20020101
              return "/" + DateTimeFormatter.ofPattern("yyyyMMdd").format(LocalDate.now());
            }
          
      

下载

  • 请求方法get

  • 设置两个响应头

    • content-disposition

      • 表示下载的数据的展示方式,如:inline内联形式(网页形式或者文件下载方式 attachment

        • 下载完打开文件, 或者下载到本地目录中
    • ContentType,指定返回数据这类型MIME

  • 响应体

    • 在网络传输时是图片的原生数据(二进制), 图片在下载完看到,是因为浏览器做了解析
  • 下载的文件名不要包含空格, base64可能转成+号, 镭雕事故

  • 关于url编码的问题 (搞不懂, 以后再看

    • 前端发送数据先把数据进行Base64编码, 然后发送时,浏览器进行url编码,

    • 服务端收到数据时, 会先进行url解码(服务器做的),当表单数据类型为url-encoded时, 这时候会导致base64的url中+被urldecode为空格

      • 在url编码中:空格–>+
  • 示例

    • 前端

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      
      <%@ page contentType="text/html;charset=UTF-8" language="java" %>
      <html>
          <head>
              <title>下载文件</title>
              <base href="${pageContext.request.getContextPath()}">
          </head>
          <body>
              <h1>文件下载</h1>
              <a href="DownloadServlet?filename=a.jpeg">下载a.jpeg</a><br>
              <a href="DownloadServlet?filename=b.jpeg">下载b.jpeg</a> <br>
              <a href="DownloadServlet?filename=夜の向日葵.mp3">下载夜の向日葵.mp3</a>
          </body>
      </html>
      
    • 后端

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      
      request.setCharacterEncoding("utf8");
      response.setCharacterEncoding("utf8");
      String filename = request.getParameter("filename");
      ServletContext servletContext = request.getServletContext();
      String downloadPath = "/download";
      String downloadFile = downloadPath + "/" + filename;
      String mimeType = servletContext.getMimeType(downloadFile);
      response.setContentType(mimeType);
      response.setHeader("content-disposition", "attachment; filename=" +
        URLEncoder.encode(filename, "utf8"));
      // 读取下载的文件数据,返回给浏览器
      InputStream is = servletContext.getResourceAsStream(downloadFile);
      ServletOutputStream os = response.getOutputStream();
      // 使用commons-io工具copy文件
      IOUtils.copy(is, os);
      System.out.println("下载成功: " + downloadFile);
      

web应用路径问题(尽量用绝对路径)

相对路径

  • 假设我们的工程路径为http://localhost/webpath/

    1
    2
    
    <a href="hello-servlet">Hello Servlet</a>
    <a href="http://localhost/webpath/hello-servlet">
    
  • 页面所有的相对路径是,浏览器地址栏路径http://host:port/工程名/+资源来跳转

  • 所以href=”hello-servlet”与 http://localhost/webpath/hello-servlet是等价的

  • 相对路径的缺点:会让项目的互相调用关闭变得复杂

用base标签(在head标签内)指定页面的参考路径

  • 如果用base指定后, 浏览器就会在解析时,取base+资源路径,即绝对路径

    1
    2
    3
    4
    
    <base href="http://localhost/webpath/">
    斜杠/不能少
    可以简写为: <base href="/webpath/">
    <h1>当前位置: /d1/d2/b.html</h1>
    
    1
    
    <a href="../../a.html">到a.html页面去1</a>
    
    1
    
    <a href="a.html">到a.html页面去2</a>
    

    加了base标签后, 就会以base的url为相对路径的基准点 和../../a.html等价

  • 会跳转到这里

    1
    
    <a href="/a.html">
    
  • http://localhost/a.html / 开头的含义就是, 主机:端口/

  • base标签的问题, base标签基准url是写死了的硬编码, 改动时不方便

    • 动态获取(jsp)

      1
      
      动态获取的工程路径: <%=request.getContextPath()%>
      

转发路径

  • 在服务端解析第一个/会被解析成http://ip:port/Application Context

    1
    2
    3
    
    request.getRequestDispatcher("/d1/d2/b.html").forward(request,response); // 推荐
    // 这个和上面的是等价的,推荐第一个
    request.getRequestDispatcher("d1/d2/b.html").forward(request,response);
    

总结

  • 在web中 /斜杠如果被服务端解析, 得到的地址是http://ip:port/ApplicationContext/

    1
    
    <url-pattern>/servletUrl</url-pattern>
    

    servletContext.getRealPath(“/”) ==> 得到项目路径(磁盘绝对路径) request.getDispatcher(“/”)

  • 斜杠在浏览器解析成http://ip:port/

    1
    
    <a href="/">
    
  • 重定向

    • response.sendRedirect(“/”)等价于 http://ip:port/

    • response.sendRedirect(“b.html”);等价于 http://localhost/webpath/b.html 推荐的重定向写法 response.sendRedirect(getServletContext().getContextPath() + b.html”)

    • 重定向等价于浏览器解析

  • 浏览器url后面带斜杠/说明是路径, 不带斜杠说明是资源

服务端渲染技术(JSP)

为什么需要服务端渲染技术, html页面无法获取动态的数据,在Servlet中写HTML字符串拼接, 丑陋又难以维护

JSP

  • 定义

    • JSP, Java Server Pages, java服务器页面, 就是服务端渲染技术
  • 特点

    • 写JSP就像在写HTML

    • JSP不能直接用浏览器直接访问, 需要去请求Tomcat, 来解析成html文件

    • 比html, 可以写动态数据名, 比Servlet,排版更友好,更优美

    • JSP基于Servlet, JSP就是对Servlet的包装, 可以理解JSP就是Servlet

    • 理解JSP, 学thymeleaf(另一种服务端渲染技术)会更容易

  • JSP原理

    • jsp本质是一个Servlet程序,其性能和java关联

    • 第一次访问jsp页面的时候, Tomcat会把jsp页面解析成一个java源文件, 并且对它编译成class字节码文件, 目录在: Catalinna_BASE/work目录下

    • jsp继承了HttpJspBase, 而HttpJspBase继承了HttpServlet

    • 解析后的jsp.java中使用原生的out.println()拼接打印html字符串

  • 使用

    • 引入依赖

      • servlet-api和jsp-api
    • 常用指令

      • page

        1
        
        <%@ page contentType="text/html;charset=UTF-8" language="java" %>
        
        • language表示jsp翻译后的语言, 只支持java

        • contentType, 等价于response.setContentType

        • pageEncoding, 表示当前jsp页面的字符集

        • import, java中的导包

      • 声明脚本

        • <%! 声明java代码 %>

          • 定义jsp需要的属性, 方法,静态代码块和内部类

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            
            <%!
            private Integer id;
            private String name;
            private static String job;
            static {
                job = "DE";
            }
            public void setId(Integer id) {
                this.id = id;
            }
            %>
            
      • 表达式脚本

        • <%=表达式%>

          • 作用: 在jsp页面上输出数据

            1
            2
            3
            
            工作是: <%=job%> <br>
            姓名是: <%=name%> <br>
            ID是: <%=id%> <br>
            
            • 表达式脚本中的表达式不能以分号结束
      • 代码脚本

        • <% java代码 %>

    案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<%!
    private List<Hero> heroes = new ArrayList<>();
    {
        heroes.add(new Hero(1, "矮人直升机", "轰炸"));
        heroes.add(new Hero(2, "斯恩", "抢人头"));
    }
    %>

<%
    for (Hero hero : heroes) {
%>
<tr>
    <td><%=hero.getId()%>
    </td>
    <td><%=hero.getName()%>
    </td>
    <td><%=hero.getSkill()%>
    </td>
</tr>
<%
    }
%>

注释

<%– comment %>

标签

请求转发标签

1
2
<jsp:forward page="/a.jsp"></jsp:forward>
<%--与request.getRequestDispatcher("/a.jsp").forward(request,response)等价 %>

EL表达式

Expression Language语言

语法${key1}

主要作用是为了替代表达式脚本<%=%>, 本质上是语法糖

el表达式输出null为”“空串, jsp表达式标本输出null为null字符串

取得是请求域中数据, 取的对象要有get方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 <%
        Book book = new Book();
        book.setName("存在与时间");
        book.setAuthor(new String[]{"马丁", "海德格尔"});
        ArrayList<String> readers = new ArrayList<>();
        Collections.addAll(readers, "维特根斯坦", "zangxin");
        book.setReader(readers);
        HashMap<String, Object> map = new HashMap<>();
        map.put("Inder welt Sein", "在世界中存在");
        map.put("to be or not to be", "向死而在");
        book.setTopics(map);
        request.setAttribute("book",book);
    %>
    book对象: ${book} <br>
    book.name: ${book.name} <br>
    book.author: ${book.author} <br>
    book.author[0]: ${book.author[0]} <br>
    book.readers: ${book.reader}  <br>
    book.readers[0]: ${book.reader.get(0)} <br>
    book.topics: ${book.topics} <br>
    book.topics[0]: ${book.topics.get("to be or not to be")} <br>
    book.topics[0]: ${book.topics["to be or not to be"]}<br>

${}取数据按照从小大的的域取pageContext –>request –>session–>applicationContext

运算符

1
2
3
4
5
6
7
8
9
== eq
!= ne
< lt <= le
gt >= ge
&& and
|| or
! not
/ div 
% mod
  • empty

    • 判断一个数据是否为空, 空-true, 非空false

    • 为空的情况

      • null, 空串”” Object类型数组长度为0 list集合元素个数为0 map集合元素个数为0
  • 三元运算符

    ?:和java一样

  • EL的隐含对象

    • 四个特定域变量,类型为Map<String,Object>

      • pageScope

      • requestScope

      • sessionScope

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    <%
    pageContext.setAttribute("k1", "pageContext-k1");
    request.setAttribute("k1", "request-k1");
    session.setAttribute("k1", "session-k1");
    application.setAttribute("k1", "application-k1");
    %>
    pageContext数据: ${pageScope.k1} <br>
    request数据: ${requestScope.k1} <br>
    session数据: ${sessionScope.k1} <br>
    application数据: ${applicationScope.k1} <br>
    </body>
    
    • applicationScope

    • pageContext对象

      1
      2
      
      协议:${pageContext.request.scheme}
      端口:${pageContext.request.remotePort}
      
  • jstl标准标签库

    • JSP Standard Library

      • JSTL是为了替换代码脚本,jsp页面更简洁

      • 依赖

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        
        <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
        </dependency>
        <dependency>
        <groupId>taglibs</groupId>
        <artifactId>standard</artifactId>
        <version>1.1.2</version>
        </dependency>
        
    • jstl有5个标签库

      • 先看核心标签库core, 以c开头

        • 等价于request.setAttribute(“username”,”root”)

        • 单分支

          1
          
          <c:if test="${10 < 2}">为真才显示我</c:if>
          
        • 多分支选择

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          
          <c:choose>
              <c:when test="${requestScope.score >= 80}">
                  <h1>${score}-成绩优秀</h1>
              </c:when>
              <c:when test="${requestScope.score >= 60}">
                  <h1>${score}-及格了, 还不错</h1>
              </c:when>
              <c:otherwise>
                  <h1>${score}-不及格</h1>
            </c:otherwise>
          </c:choose>
          
        • 遍历数组,列表,map,必须提供get方法

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          14
          15
          16
          
          <%--   等价于for(int i=1; <=5;i++)  output 1 2 3 4 5  --%>
          <c:forEach begin="1" end="5" step="1" var="i">
          ${i}
          </c:forEach>
          <%-- 数组 request.setAttribute("sports",new String[]{"lq","zq","ymq","tc"}) --%>
          <c:forEach items="${requestScope.sports}" var="sport">
          ${sport}
          </c:forEach>
          <%-- Map<String, String> cities = new HashMap<>(); --%>
          <c:forEach items="${requestScope.cities}" var="city">
          ${city.key}===${city.value}
          </c:forEach>
          <%-- List<String> list = new ArrayList<>(); --%>
          <c:forEach items="${list}" var="item">
          ${item}
          </c:forEach>
          
  • JSP内置对象(build-in)

    • out

      • JspWriter继承了Writer
    • request 客户端请求

      • HtttpServletRequest
    • response响应对象

      • HttpServletResponse
    • Session会话对象

      • HttpSession
    • application

      • ServletContext
    • pageContext

      • jsp页面的上下文, 是一个域对象,可以setAttribute, 作用范围是本页面
    • exception

    • page

      • 类似于this
    • config

      • ServletConfig
  • JSP四大域对象

    • pageContext(jsp特有)

      • 存放的数据只能在当前jsp页面使用
    • request

      • 存放的数据在一次请求有效, 转发可以(重定向不行哦😯)
    • session

      • 多次请求都可以, 只要会话没有变化
    • application

      • ServletContext存放的数据在整个web应用运行期间有效, 范围最大,多个会话共享
    • 测试

      • 在一个jsp页面

        • 四个域对象都有数据
      • 跨jsp页面(转发)

        • pageContext没有数据
      • 重定向跨jsp页面

        • pageContext和request没有数据
      • 清除cookie(jsessionid)

        • pageContext, request, session都没有数据
      • 重启Tomcat

        • 数据全都为null 了
    • 总结

      • 域对象是可以像map一样存取数据的对象. 四个域对象功能一样, 不同的是他们对数据存储的范围

      • 存储范围大小: pageContext < request < Session < ServletContext(applicantion)

JSON转换

jackson

文档

  • https://www.baeldung.com/jackson-object-mapper-tutorial

依赖

  • jackson-databind

序列化

1
2
ObjectMapper om = new ObjectMapper();
String jsonString = om.writeValueAsString(books);

反序列化json string —> java

  • 单个对象

    1
    
    om.readValue(jsonString, Book.class);
    
  • 数组

    1
    
    List<Book> books1 = om.readValue(jsonString, new TypeReference<List<Book>>() {});
    
  • map

    1
    
    HashMap<String, Book> map = om.readValue(mapJson, new TypeReference<HashMap<String, Book>>() {});
    
  • 注意事项

    • JSON转换时,实体类必须提供getter/setter

项目练习

家居网购

  • 架构

    • MVC三层架构,包管理技术😁

      • 浏览器

        • 客户端,解析网页数据
      • view

        • html,css.js

          • 网页,和用户直接交互的界面

          • 发起http请求,接收响应数据/页面

        • 可用技术,vue

      • controller/servlet

        • 视图层: 接收用户请求,调用Service完成业务处理 返回响应数据或者重定向,转发页面 可用技术:servlet, springmvc
      • service

        • 业务层: 提供了很多业务API 业务逻辑主要在这里实现 针对不同业务逻辑, 调用dao层
      • dao

        • 数据访问层: 完成对数据库可的操作 数据库的增删改查,Create, Read, Update, Delete 可用技术: JDBC,DButils, JdbcTemplate,Mybatis,Mybatis-plus,Hibernate
      • bean

        • 放实体类, vo,domain/bean/pojo,dto
      • util

        • 工具类
      • test

        • 测试类
    • MVC正经解说

      • MVC全称是 Model模型,View视图,Controller控制器,MVC可以有效的指导WEB层代码有效分离

      • View视图

        • 只负责界面的显示, 不接收任何与现实无关的代码,便于程序员和UI的分工合作(Vue/JSP/Thymeleaf/HTML)
      • Controller控制器

        • 只负责接收请求, 调用业务层的代码处理请求, 返回派发页面, 是一个调度者的角色(Servlet)
      • Model将业务逻辑相关的数据封装为具体的javaBean, 其中不不掺杂任何与数据处理相关的代码(javabean/domain/pojo)

        • model最早期就是javabean, 通过service+dao获取的数据封装成bean, 而不去管细节,只要bean

        • 随着业务复杂, model分化为service+dao

        • 持久层技术的出现就演变为service + dao + 持久层技术(hibernate/mybatis/…)

      • MVC的理念是将软件拆分成为组件, 单独开发,组合使用(目的是解耦合),(SpringMVC)

  • Servlet合并

    • 痛点: 一个url对应一个servlet, n个功能对应n个servlet….

    • 解决方法1

      • 前端输入框传递隐藏域action, 根据action的值不同来调用不同的业务方法

      • 感觉耦合性太高, 涉及前后端高度耦合,直接淘汰

    • 模板方法模式

      • 抽象出BaseService, 在service方法中定义号要做的步骤, 把这些步骤定义为抽象的, 然后子类实现这些步骤

      • uri最后一截和方法名保持一致, 利用uri作为方法名,反射调用方法

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        
        public class BaseServlet extends HttpServlet {
          @Override
          protected void service(HttpServletRequest req, HttpServletResponse resp)
              throws ServletException, IOException {
            req.setCharacterEncoding("utf8");
            resp.setCharacterEncoding("utf8");
            String uri = req.getRequestURI();
            String methodName = uri.substring(uri.lastIndexOf("/") + 1);
            Class<? extends BaseServlet> clazz = this.getClass();
            try {
                Object o = clazz.newInstance();
                Method method = clazz.getMethod(methodName,
                        HttpServletRequest.class, HttpServletResponse.class);
                method.invoke(o, req, resp);
            } catch (Exception e) {
                resp.sendRedirect(req.getContextPath() + "/500.html");
                e.printStackTrace();
            }
          }
        }
        
      • 扩展功能, 在模板方法中如加入字符集编码设置, 统一配置

  • insertSQL页面使用转发造成重复提交提问

    • 在保存页面(insert)不能用转发,因为再次刷新页面时会出现重复插入的情况

    • 原因:当用户提交完请求,浏览器会记录下最后一次请求的全部信息。当用户刷新页 面(f5), 就会发起浏览器记录的上一次请求

    • 解决方案使用重定向即可,因为重定向本质是两次请求,而且最后一次就是请求显示家 居,而不是添加家居

  • 后端数据校验

    • 校验数据格式是否正确

      • 前端传入的数据都是string,需要查看下一下是否为数字,或者长度验证,判空验证等等
    • 不能依赖于前端校验

      • 在接口测试时, 可以随便改数据, 绕过前端
    • springmvc有一个数据校验工具支持JSR303,Hibernate validator,注解校验, 这里一个一个校验太繁琐了

  • 工具类的使用

    • commons-beanutils

      • 封装bean,把前端传过来的数据封装成java对象

      • Furn furn = new Furn(); BeanUtils.populate(furn,request.getParameterMap());

      • 需要保持name和javabean的属性名一样

      • 如果传来abc, javabean为Integer, 会设置为0(默认值), 不抛出异常, 所以指望抛出异常来校验不太行

  • 两个html页面之间传递参数问题不通过服务服务器转发或者重定向

    1
    2
    3
    4
    5
    6
    
    // 从页面A跳转到页面B,并传递参数
    window.location.href = 'pageB.html?param1=value1&param2=value2';
    // 在页面B中获取参数
    var params = new URLSearchParams(window.location.search);
    var param1 = params.get('param1');
    var param2 = params.get('param2');
    
  • 分页功能

    • 分页参数

      • pageSize –每页容量 pageNum—第几页 totalRows–总记录数 totalPage–总页数 pageTotalRow–每页行数 list—分页的数据记录
      • 封装成对象Page
    • 分页时,不要使用异步ajax, 要么代码全部写在success中, 否则导致获取的分页参数可能为null值

  • 登录功能+鉴权

    • 登录功能显示登录名和现实订单菜单入口(前端)

      • 在登录servlet中完成登录后,保存用户对象到session中, cookie中写入用户名和权限级别, 来显示用户名,根据role级别是否显示后台管理入口

        • moliza的cookie工具 https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie
    • 权限校验

      • 定义: 控制用户能访问的uri

      • 对http请求进行Filter, 在web.xml中配置拦截的url和白名单url, 在doFilter方法中完成配置

      • AuthFilter

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        
        public class AuthFilter implements Filter {
          private List<String> excludeUrls;
          private String loginHtmlPath = "/views/member/login.html";
          @Override
          public void init(FilterConfig filterConfig) throws ServletException {
          excludeUrls = Arrays.stream(filterConfig.getInitParameter("excludeUrl").split(",")).toList();
          System.out.println("excludeUrls = " + excludeUrls);
        }
        @Override
        public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
          HttpServletRequest request = (HttpServletRequest) req;
          // 配置时没有配置项目路径,这里要去掉
          String url = request.getRequestURI().substring(request.getContextPath().length());
          User user = SessionUtils.getUser(request);
          // 如果是白名单, 和 登录过用户直接放行
          if (excludeUrls.contains(url) || user != null) {
              System.out.println("放行: " + url);
              chain.doFilter(request, response);
          } else {
              ((HttpServletResponse) response).sendRedirect(request.getContextPath() + loginHtmlPath);
          }
            System.out.printf("%s请求访问url: %s\n", (user == null ? "" : user.getUsername()), url);
          }
        }
        
      • web.xml

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        
        <filter>
        <filter-name>AuthFilter</filter-name>
        <filter-class>com.xxx.jiajumall.filter.AuthFilter</filter-class>
        <init-param>
            <param-name>excludeUrl</param-name>
            <!--逗号作为分割符-->
            <param-value>/views/manage/manage_login.html,
                ,/views/member/login.html,/manage/login,/furn/get,/furn/getPageByName,
            ,/furn/getPage,/cart/getItems,/cart/getCarInfo</param-value>
        </init-param>
        </filter>
        <filter-mapping>
        <filter-name>AuthFilter</filter-name>
        <url-pattern>/views/manage/*</url-pattern>
        <url-pattern>/views/order/*</url-pattern>
        <url-pattern>/furn/*</url-pattern>
        <url-pattern>/cart/*</url-pattern>
        </filter-mapping>
        
  • 验证码功能 解决重复提交的利器

    • 1.登录html页面向服务端请求生成验证码图片, 服务端返回验证码图片 2.显示验证码图片

      • KaptchaServlet被请求生成验证码, get方法 google提供的工具, 返回验证码图片, 并且把验证存在session中, 将图片以http的方式返回
    • 3.用户输入用户名,密码,验证码等

    • 4.先在浏览器端验证,验证码是否正确

    • 5.如果验证通过发送给服务端验证码值和用户信息

      • 获取浏览器提交的信息, 从session中去除验证码值, 立即删掉验证码(防止重复提交), 比较验证值是否相等,相等时注册成功
    • 实现

      • 前端

        • 加上时间戳是为了t=xxxxxx是为刷新验证码, 否则服务会返回原来的验证码(302认为你的内容没有发生改变)

          1
          2
          3
          4
          5
          6
          7
          
          function verifyCodeEvent() {
            $("#verifyCode").click(function () {
            $("#verifyCode").get(0).src = '/jiaju_mall/KaptchaServlet?t=' + (new Date().getTime())
            });
          }
          //
          <img id="verifyCode" alt="" src="/jiaju_mall/KaptchaServlet">
          
      • 后端

        • 生成验证码

          • google的kaptchaServlet

            • 一个Servlet收到get请求时,返回一张根据验证生成的图片, 验证放在Session中
        • 校验验证码

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          
          private boolean checkCode(HttpServletRequest request) {
          // 前端传来的验证码
            String code = request.getParameter("code");
            // session 生成的验证码
            String token = (String) 	        request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
            // 立即删除Session中验证码,避免重复使用
            request.getSession().removeAttribute(Constants.KAPTCHA_SESSION_KEY);
            // 比对
            return token != null && token.equalsIgnoreCase(code);
          }
          
  • 购物车设计

    • Session版购物车, 免登录可以添加购物车,关闭会话后,就会清空信息

    • 需求

      • 可以把商品添加到,添加购物车,结算购物车, 清空购物车, 删除购物车商品, 修改商品数量
    • 模型

      • Cart购物车对象

        • 购物车项cartItemList

          • cartItem单项属性

            • id

            • img 商品图片

            • name 商品名

            • count 商品数量

            • price 商品单价

            • totalPrice 商品总价

        • clear()清空购物车

        • addItem()

        • removeItem()

        • updateCount

        • getTotalCount

        • getTotalPrice

        • getItem

  • 订单设计

    • 一个用户可以有多个订单, 一个订单可以有多个订单项

    • 具体字段根据用户界面来设计

    • 订单对象Order持有订单项对象OrderItem

    • 订单表设计(tbl_order)

      • id

      • 订单编号

        • orderCode
      • 关联用户

        • userId
      • 订单时间

        • createTime
      • 订单金额

        • amount
      • 订单状态,(未发货,未支付,..)

        • status
      create table tbl_order
      (
      id         int primary key auto_increment,
      orderCode  varchar(14)    not null default '',
      userId     int            not null default 0,
      amount     decimal(12, 2) not null default 0 comment '订单金额',
      status     tinyint        not null default 0 comment '订单状态 0 未支付 1 已支付待发货 2订单取消 3 订单已完成 4 售后',
      createTime timestamp      not null default current_timestamp on update current_timestamp
      ) charset utf8mb4
      collate utf8mb4_general_ci
      engine 'Innodb';
      
      • 订单详情

        • 订单项表(单个商品)tbl_order_item

          • id

          • 订单号

            • orderCode
          • 商品id

            • furnId
          • 商品名

            • name
          • 商品数量

            • count
          • 商品单价

            • price
          • 商品总价

            • totalPrice
      create table tbl_order_item
      (
      id         int primary key auto_increment,
      orderCode  varchar(14)    not null default '',
      furnId     int            not null default 0 comment '商品id',
      name       varchar(64)    not null default '' comment '商品名',
      count      int            not null default 0 comment '商品数量',
      price      decimal(12, 2) not null default 0,
      totalPrice decimal(12, 2) not null default 0
      ) charset utf8mb4
      collate utf8mb4_general_ci
      engine 'Innodb';
      
    • 下单流程

      • 购物车提交订单

        • 用户是否登录

          • 未登录重定向登录页面,登录后返回购物车页面

          • 已经登录

            • 开启事务 获取购物车对象, 遍历购物车, 插入订单表, 订单项表, 查询商品表的库存 扣减商品表的库存 更新销量, 调用支付方法, 提交订单后要清空购物车
              • 完成后跳转订单完成页面
  • 是否要使用外键

    • 使用外键可以从数据库层强制保证数据的一致性

    • 不使用外键, 外键影响效率, 从程序上保持数据一致性

  • 事务处理, 处理数据不一致

    • 在进行提交订单时 扣减库存–修改商品表 插入订单表 插入订单项 都使用到了BaseDao中update方法, 这个方法被调用了多次, 每次都从连接池拿取,用完放回,不能保证提交订单过程中使用是同一个连接

      1
      2
      3
      4
      
      // 从数据库连接池获取connection,不能保证获取的是同一个连接
      connection = JdbcDruidUtil.getConnection();
      return queryRunner.update(connection, sql, parameters);
      JdbcDruidUtil.close(null, null, connection);
      
    • 解决方案

      • 要保证在一次请求中, 是同一个connection

      • 将自动提交事务, 改为手动提交事务

    • 落地

      • 使用Filter + ThreadLocal进行事务管理

      • 在一次http请求中, servlet-service-dao的调用过程始终是一个线程, 这是使用ThreadLocal的前提

      • 使用ThreadLocal来确保所有dao操作都在同一个Connection连接中完成

        • 重写JDBCUtils方法 getConnection(关闭自动提交)
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        
         /**
         * 保证在同一个线程中,获取的是同一连接
         */
        public static Connection getConnection() {
           try {
               Connection connection = connectionThreadLocal.get();
               // 说明当前ThreadLocal中没有连接
               if (connection == null) {
                   // 从连接池中获取一个连接放入到ThreadLocal中
                   connection = ds.getConnection();
                   // 设置事务为手动提交
                   connection.setAutoCommit(false);
                   connectionThreadLocal.set(connection);
               }
               return connection;
           } catch (SQLException e) {
               throw new RuntimeException(e);
           }
        }
        

        commit

        ```java /**

      • 提交事务 */ public static void commit() { Connection connection = connectionThreadLocal.get(); if (connection != null) { try { connection.commit(); } catch (SQLException e) { throw new RuntimeException(e); } finally { try { connection.close(); } catch (SQLException e) { throw new RuntimeException(e); } } } // 当提交后,需要把connection从threadLocal中清除 // 不然会造成ThreadLocal长期持有该线程, 会影响效率 // 也因为Tomcat底层使用的使用线程池技术 connectionThreadLocal.remove(); } ```

      rollback

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      
      /**
      * 回滚事务
      */
        public static void rollback() {
        Connection connection = connectionThreadLocal.get();
        // 保证连接是有效的
        if (connection != null) {
          try {
            connection.rollback();
          } catch (SQLException e) {
            throw new RuntimeException(e);
          } finally {
            try {
              connection.close();
            } catch (SQLException e) {
              throw new RuntimeException(e);
            }
          }
        }
        connectionThreadLocal.remove();
      }
      

TransactionFilter事务过滤器 doFilter()

// 前置代码,手动提交事务 autoCommit=false 获取连接,跟当前线程绑定–>ThreadLocal // 后置代码 commit

// 对所有请求开启事务, 一定要把所有异常抛出来,在filter中在捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebFilter(urlPatterns = {"/*"})
public class TransactionFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
            JdbcDruidUtil.commit(); // 统一提交事务
            System.out.println("事务提交成功~~~~");
        } catch (Exception e) {
            System.out.println("发生异常,回滚事务");
            JdbcDruidUtil.rollback();
            e.printStackTrace();
        }
	}
}
  • 统一错误页面处理

    • 原理

      • 抛出的异常会给tomcat, tomcat会根据errorPage来显示对应的页面(这个是转发到项目根路径, 所以404.html图片等资源使用相对路径时要注意是相对于根路径的)
      • 异常一定要抛出来, 否则不能达成预期效果
    • 配置,web.xml中

      1
      2
      3
      4
      5
      6
      7
      8
      
      <error-page>
        <error-code>404</error-code>
        <location>/views/error/404.html</location>
      </error-page>
      <error-page>
        <error-code>500</error-code>
        <location>/views/error/500.html</location>
      </error-page>
      
  • ajax局部刷新页面功能

    • 添加购物车后,购物车数量显示加1, 但是刷新了整个页面,占用资源多

    • 修改成不刷新整个页面, 也能修改购物车数量

    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
    34
    35
    
    // 添加购物车
    function addToCart(furnId) {
    $.post({
     url: '/jiaju_mall/cart/addItem',
     data: {
         id: furnId
     },
     dataType: 'json',
     success: function (res, statusText, xhr) {
         if (res.code == 1) {
             // 处理转发或定向到ajax请求上
             if (res.url) {
              window.location.href = res.url
             } else {
                 alert("添加成功");
                 showCartItemCount()
             }
         } else {
             alert("添加失败")
         }
     },
    })
    }
    //显示购物车
    function showCartItemCount() {
    $.getJSON({
     url:'/jiaju_mall/cart/getCarInfo',
     success: function (res,statusText,xhr){
         let cart = res.cart
         if (res.code == 1) {
             $("#cart_items_count").text(cart.totalCount)
         }
     }
    })
    }
    
    • 针对ajax的转发和重定向失效问题 (过滤器产生的问题)

      • 主要是服务器得到的是ajax发送过来的request,也就是说这个请求不是浏览器请求的, 而是ajax请求的, 所以,Servlet对request进行转发和重定向都不能影响浏览器的跳转
      • 解决:在过滤器中判断是否为AJAX请求,如果是AJAX返回url, 不是AJAX请求时进行转发或者重定向 然后ajax拿到请求后执行window.location.href= url
1
2
3
4
5
if (request.getHeader("x-requested-with").equals("XMLHttpRequest")) {
  response.setContentType("application/json;utf8");
  Result result = new Result(response);
  result.put("url", request.getContextPath() + loginHtmlPath);
}
  • 上传头像图片功能

    • 处理multipart/form-data