本文摘要:

  • 理解什么是会话跟踪技术
  • 掌握Cookie的使用
  • 掌握Session的使用
  • 完善用户登录注册案例的功能

会话跟踪技术概述

概述

对于会话跟踪这四个字,我们需要拆开来进行解释,首先要理解什么是会话,然后再去理解什么是会话跟踪

  • 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应
    • 从浏览器发出请求到服务端响应数据给前端之后,一次会话(在浏览器和服务器之间)就被建立了
    • 会话被建立之后,如果浏览器或服务端都没有被关闭,则会话会持续建立着
    • 浏览器和服务器就可以继续使用该会话进行请求发送和响应,上述的整个过程就被称之为会话
      用实际场景来理解一下会话,比如在我们访问京东的时候,当打开浏览器进入京东首页之后,浏览器和京东服务器之间就建立了一次会话,后面的搜索商品,查看商品详情,加入购物车等,都是在这一次会话中完成的。
  • 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求之间共享数据
    • 服务器会收到多个请求,这多个请求可能来自多个浏览器(京东这种购物网站,肯定时时刻刻都有好多人访问)
    • 服务器需要用来识别请求是否来自同一个浏览器
    • 服务器用来识别浏览器的过程,这个过程就是会话跟踪
    • 服务器识别浏览器后就可以在同一个会话中的多次请求之间来共享数据

那么我们又有一个问题需要思考,一个会话中的多次请求为什么要共享数据呢?有了这个数据共享功能之后,能实现哪些功能呢?

  • 购物车: 加入购物车去购物车结算是两次请求,但是后面这次请求要想展示前一次请求所添加的商品,就需要用到数据共享
  • 页面展示用户登录信息:很多网站,登录后访问多个功能发送多次请求后,浏览器上都会有当前登录用户的信息[用户名],登录百度账号,访问贴吧,网盘,都不需要再登陆一次
  • 网站登录页面的记住我功能:当用户登录成功后,勾选记住我按钮后下次再登录的时候,网站就会自动填充用户名和密码,简化用户的登录操作,多次登录就会有多次请求,他们之间也涉及到共享数据
  • 登录页面的验证码功能:生成验证码和输入验证码点击注册这也是两次请求,这两次请求的数据之间要进行对比,相同则允许注册,不同则拒绝注册,该功能的实现也需要在同一次会话中共享数据。

那为什么现在浏览器和服务器不支持数据共享呢?

  • 浏览器和服务器之间使用的是HTTP请求来进行数据传输
  • HTTP协议是无状态的,每次浏览器向服务器请求时,服务器都会将该请求视为新的请求
  • HTTP协议设计成无状态的目的是让每次请求之间相互独立,互不影响
  • 请求与请求之间独立后,就无法实现多次请求之间的数据共享

分析完具体的原因后,那么该如何实现会话跟踪技术呢? 具体的实现方式有:

  1. 客户端会话跟踪技术:Cookie
  2. 服务端会话跟踪技术:Session

这两个技术都可以实现会话跟踪,它们之间最大的区别:Cookie是存储在浏览器端而Session是存储在服务器端

小结

这里主要介绍了下什么是会话和会话跟踪技术,需要注意的是:

  • HTTP协议是无状态的,靠HTTP协议是无法实现会话跟踪
  • 想要实现会话跟踪,就需要用到Cookie和Session

Cookie

学习Cookie,我们主要解决下面几个问题:

  • 什么是Cookie?
  • Cookie如何来使用?
  • Cookie是如何实现的?
  • Cookie的使用注意事项有哪些?

Cookie的基本使用

概念

Cookie:客户端会话技术,将数据保存到客户端,以后每次请求都携带Cookie数据进行访问。

Cookie的工作流程

  • 服务端提供了两个Servlet,分别是AServlet和BServlet
  • 浏览器发送HTTP请求1给服务端,服务端AServlet接收请求并进行业务处理
  • 服务端AServlet在处理请求的过程中可以创建一个Cookie对象,并将name=zs的数据存入Cookie
  • 服务端AServlet在响应数据的时候,会把Cookie对象响应给浏览器
  • 浏览器接收到响应数据,会把Cookie对象中的数据存储在浏览器内存中,此时浏览器和服务端就建立了一次会话
  • 在同一次会话中,浏览器再次发送HTTP请求2给服务端BServlet,浏览器会携带Cookie对象中的所有数据
  • BServlet接收到请求和数据后,就可以获取到存储在Cookie对象中的数据,这样同一个会话中的多次请求之间就实现了数据共享

Cookie的基本使用

对于Cookie的使用,我们更关注的应该是后台代码如何操作Cookie,对于Cookie的操作主要分两大类,分别是发送Cookie获取Cookie

  • 发送Cookie

    • 创建Cookie对象,并设置数据

      1
      2

      Cookie cookie = new Cookie("key","value");
    • 发送Cookie到客户端:使用

      1
      response

      对象

      1
      2

      response.addCookie(cookie);

介绍完发送Cookie对应的步骤后,接下面通过一个案例来完成Cookie的发送

需求:在Servlet中生成Cookie对象并存入数据,然后将数据发送给浏览器

  1. 创建Maven项目,项目名称为cookie-demo,并在pom.xml添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    </dependency>
    <dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.2</version>
    </dependency>
  2. 编写Servlet类,名称为AServlet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    @WebServlet("/aServlet")
    public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
  3. 在AServlet中创建Cookie对象,存入数据,发送给前端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    @WebServlet("/aServlet")
    public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cookie cookie = new Cookie("username","zs");
    response.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
  4. 启动测试,在浏览器查看Cookie对象中的值

    名称 username
    内容 zs
    域名 localhost
    路径 /cookie_demo
    为何发送 仅限同一网站的连接
    脚本可访问 是
    创建时间 2023年2月20日星期六 10:47:16
    到期时间 浏览会话结束时

  • 获取Cookie

    • 获取客户端携带的所有Cookie,使用

      1
      request

      对象

      1
      2

      Cookie[] cookies = request.getCookies();
    • 遍历数组,获取每一个Cookie对象,使用Cookie对象方法获取数据

      1
      2
      3
      4
      5
      6

      for (Cookie cookie : cookies) {
      String name = cookie.getName();
      String value = cookie.getValue();
      System.out.println(name + ":" + value);
      }

介绍完获取Cookie对应的步骤后,接下面再通过一个案例来完成Cookie的获取
需求:在Servlet中获取前一个案例存入在Cookie对象中的数据

  1. 编写一个新Servlet类,名称为BServlet

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    @WebServlet("/bServlet")
    public class BServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
  2. 在BServlet中使用request对象获取Cookie数组,遍历数组,从数据中获取指定名称对应的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    @WebServlet("/bServlet")
    public class BServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cookie[] cookies = request.getCookies();
    for (Cookie cookie : cookies) {
    String name = cookie.getName();
    if ("username".equals(name)) {
    String value = cookie.getValue();
    System.out.println(name + ":" + value);
    }
    }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
  3. 启动测试,在控制台打印出获取的值
    成功输出 username:zs

小结

这里主要讲解了Cookie的基本使用,包含两部分内容

  • 发送Cookie:
    • 创建Cookie对象,并设置值:Cookie cookie = new Cookie("key","value");
    • 发送Cookie到客户端使用的是Reponse对象:response.addCookie(cookie);
  • 获取Cookie:
    • 使用Request对象获取Cookie数组:Cookie[] cookies = request.getCookies();
    • 遍历数组
    • 获取数组中每个Cookie对象的值:cookie.getName()cookie.getValue()

Cookie的原理分析

对于Cookie的实现原理是基于HTTP协议的,其中设计到HTTP协议中的两个请求头信息:

  • 响应头:set-cookie
  • 请求头: cookie
  • 前面的案例中已经能够实现,AServlet给前端发送Cookie,BServlet从request中获取Cookie的功能
  • 对于AServlet响应数据的时候,Tomcat服务器都是基于HTTP协议来响应数据
  • 当Tomcat发现后端要返回的是一个Cookie对象之后,Tomcat就会在响应头中添加一行数据Set-Cookie:username=zs
  • 浏览器获取到响应结果后,从响应头中就可以获取到Set-Cookie对应值username=zs,并将数据存储在浏览器的内存中
  • 浏览器再次发送请求给BServlet的时候,浏览器会自动在请求头中添加Cookie: username=zs发送给服务端BServlet
  • Request对象会把请求头中cookie对应的值封装成一个个Cookie对象,最终形成一个数组
  • BServlet通过Request对象获取到Cookie[]后,就可以从中获取自己需要的数据

Cookie的使用细节

Cookie的存活时间

名称 username
内容 zs
域名 localhost
路径 /cookie_demo
为何发送 仅限同一网站的连接
脚本可访问 是
创建时间 2023年1月20日星期六 10:47:16
到期时间 浏览会话结束时

刚刚我们在浏览器中查看Cookie对象中的值时,发现Cookie还有到期时间,这个到期时间是浏览会话结束时,那我们关闭浏览器之后,再打开进行访问,BServlet就不能获取到Cookie数据了。

因为默认情况下,Cookie存储在浏览器内存中,当浏览器关闭,内存释放,则Cookie被销毁

但是如果使用这种默认情况下的Cookie,有些需求就无法实现,比如我们登录一些网站的时候

  • 第一次输入用户名和密码并勾选记住我然后进行登录
  • 下次再登录的时候,用户名和密码就会被自动填充,不需要再重新输入登录
  • 比如记住我这个功能需要记住用户名和密码一个星期,那么使用默认情况下的Cookie就会出现问题
  • 因为默认情况,浏览器一关,Cookie就会从浏览器内存中删除,对于记住我功能就无法实现

所以我们现在就遇到一个难题是如何将Cookie持久化存储?
Cookie其实已经为我们提供好了对应的API来完成这件事,这个API就是setMaxAge,

  • 设置Cookie存活时间

    1
    2

    setMaxAge(int seconds);

参数值为:

  1. 正数:将Cookie写入浏览器所在电脑的硬盘,持久化存储。到时间自动删除
  2. 负数:默认值,Cookie在当前浏览器内存中,当浏览器关闭,则Cookie被销毁
  3. 零:删除对应Cookie

接下来,我们就在AServlet中去设置Cookie的存活时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

@WebServlet("/aServlet")
public class AServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Cookie cookie = new Cookie("username", "zs");
cookie.setMaxAge(7 * 24 * 60 * 60); //设置七天的
response.addCookie(cookie);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}

修改完代码后,启动测试

  • 访问AServlet后,把浏览器关闭重启后访问BServlet,能在控制台打印出username:zs,说明Cookie没有随着浏览器关闭而被销毁

  • 通过浏览器查看Cookie的内容,到期时间成功续期7天

    名称 username
    内容 zs
    域名 localhost
    路径 /cookie_demo
    为何发送 仅限同一网站的连接
    脚本可访问 是
    创建时间 2023年1月20日星期六 10:47:16
    到期时间 2023年1月27日星期六 10:47:16

Cookie存储中文

Cookie是不能直接存储中文的,但是如果有这方面的需求,这个时候该如何解决呢?
我们可以使用URL编码,所以如果需要存储中文,就需要进行转码,具体的实现思路为:

  1. 在AServlet中对中文进行URL编码,采用URLEncoder.encode(),将编码后的值存入Cookie中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    @WebServlet("/aServlet")
    public class AServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String value = "张三";
    //URL编码
    value = URLEncoder.encode(value, "UTF-8");
    Cookie cookie = new Cookie("username", value);
    cookie.setMaxAge(7 * 24 * 60 * 60);
    response.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
  2. 在BServlet中获取Cookie中的值,获取的值为URL编码后的值(%E5%BC%A0%E4%B8%89

    名称 username
    内容 %E5%BC%A0%E4%B8%89
    域名 localhost
    路径 /cookie_demo
    为何发送 仅限同一网站的连接
    脚本可访问 是
    创建时间 2023年1月20日星期六 12:25:37
    到期时间 2023年1月27日星期六 12:25:37

  3. 将获取的值在进行URL解码,采用URLDecoder.decode(),就可以获取到对应的中文值,控制台输出username:张三

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    @WebServlet("/bServlet")
    public class BServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cookie[] cookies = request.getCookies();
    for (Cookie cookie : cookies) {
    String name = cookie.getName();
    if ("username".equals(name)) {
    String value = cookie.getValue();
    //解码
    value = URLDecoder.decode(value, "utf-8");
    System.out.println(name + ":" + value);
    }
    }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }

Session

Session的基本使用

概念

Session:服务端会话跟踪技术:将数据保存到服务端。

  • Session是存储在服务端而Cookie是存储在客户端
  • 存储在客户端的数据容易被窃取和截获,存在很多不安全的因素
  • 存储在服务端的数据相比于客户端来说就更安全

工作流程

  • 在服务端的AServlet获取一个Session对象,把数据存入其中
  • 在服务端的BServlet获取到相同的Session对象,从中取出数据
  • 就可以实现一次会话中多次请求之间的数据共享了
  • 现在最大的问题是如何保证AServlet和BServlet使用的是同一个Session对象(后面的Session原理会说)

基本使用

在JavaEE中提供了HttpSession接口,来实现一次会话的多次请求之间数据共享功能。

具体的使用步骤为:

  • 获取Session对象,使用的是request对象

    1
    HttpSession session = request.getSession();
  • Session对象提供的功能:

    • 存储数据到 session 域中

      1
      void setAttribute(String name, Object o)
    • 根据 key,获取值

      1
      Object getAttribute(String name)
    • 根据 key,删除该键值对

      1
      void removeAttribute(String name)

介绍完Session相关的API后,接下来通过一个案例来完成对Session的使用

需求:在一个Servlet中往Session中存入数据,在另一个Servlet中获取Session中存入的数据

  1. 创建名为SessionDemo1的Servlet类:获取Session对象、存储数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    @WebServlet("/demo1")
    public class SessionDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //获取Session对象
    HttpSession session = request.getSession();
    //存储数据
    session.setAttribute("username","ls");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
  2. 创建名为SessionDemo2的Servlet类:获取Session对象、获取数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    @WebServlet("/demo2")
    public class SessionDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //获取Session对象
    HttpSession session = request.getSession();
    //获取数据
    Object username = session.getAttribute("username");
    //看看能否输出到控制台
    System.out.println(username);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }
    }
  3. 启动测试
    先访问SessionDemo1,将数据存入Session,再访问SessionDemo2,从Session中获取数据,控制台成功输出ls

小结

至此Session的基本使用就已经完成了,重点要掌握的是:

  • Session的获取

    1
    2

    HttpSession session = request.getSession();
  • Session常用方法的使用

    1
    2
    3

    void setAttribute(String name, Object o)
    Object getAttribute(String name)

注意:Session中可以存储的是一个Object类型的数据,也就是说Session中可以存储任意数据类型。

介绍完Session的基本使用之后,那么Session的底层到底是如何实现一次会话两次请求之间的数据共享呢?

Session的原理分析

Session是基于Cookie实现的

这句话其实不太能详细的说明Session的底层实现,接下来,我们一步步来分析下Session的具体实现原理:

  • 前提条件

    • Session要想实现一次会话多次请求之间的数据共享,就必须要保证多次请求获取Session的对象是同一个。
  • 如何验证

    • 在上面案例中的两个ServletDemo中分别打印下Session对象,比较其地址值是否相同

      • ServletDemo1
      • ServletDemo2
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

      @WebServlet("/demo1")
      public class SessionDemo1 extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      HttpSession session = request.getSession();
      System.out.println(session);
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
      }
      }
  • 启动测试,分别访问/demo1/demo2,输出如下

    org.apache.catalina.session.StandardSessionFacade@69186990
    org.apache.catalina.session.StandardSessionFacade@69186990

  • 通过打印可以得到如下结论:

    • 两个Servlet类中获取的Session对象是同一个
    • 把demo1和demo2请求刷新多次,控制台最终打印的结果都是同一个
  • 那么问题又来了,如果新开一个浏览器,访问demo1或者demo2,打印在控制台的Session还是同一个对象么?

    • 测试的结果:打印的Session不一样。

      org.apache.catalina.session.StandardSessionFacade@1c7eb9a
      org.apache.catalina.session.StandardSessionFacade@1c7eb9a

  • 所以Session实现的也是一次会话中的多次请求之间的数据共享。

    • 那么最主要的问题就来了,Session是如何保证在一次会话中获取的Session对象是同一个呢?
  1. demo1在第一次获取session对象的时候,session对象会有一个唯一的标识,假如是id:10
  2. demo1在session中存入其他数据并处理完成所有业务后,需要通过Tomcat服务器响应结果给浏览器
  3. Tomcat服务器发现业务处理中使用了session对象,就会把session的唯一标识id:10当做一个cookie,添加Set-Cookie:JESSIONID=10到响应头中,并响应给浏览器
  4. 浏览器接收到响应结果后,会把响应头中的coookie数据存储到浏览器的内存中
  5. 浏览器在同一会话中访问demo2的时候,会把cookie中的数据按照cookie: JESSIONID=10的格式添加到请求头中并发送给服务器Tomcat
  6. demo2获取到请求后,从请求头中就读取cookie中的JSESSIONID值为10,然后就会到服务器内存中寻找id:10的session对象,如果找到了,就直接返回该对象,如果没有则新创建一个session对象
  7. 关闭打开浏览器后,因为浏览器的cookie已被销毁,所以就没有JESSIONID的数据,服务端获取到的session就是一个全新的session对象

至此,Session是基于Cookie来实现的这就话,我们就解释完了

小结

介绍完Session的原理,我们只需要记住

  • Session是基于Cookie来实现的

Session的使用细节

Session的钝化与活化

首先需要大家思考的问题是:

  • 服务器重启后,Session中的数据是否还在?

要想回答这个问题,我们首先应该想到

  1. 服务器端AServlet和BServlet共用的session对象应该是存储在服务器的内存中
  2. 服务器重新启动后,内存中的数据应该是已经被释放,对象也应该都销毁了

所以session数据应该也已经不存在了。但是如果session不存在会引发什么问题呢?
举个例子说明下,

  1. 用户把需要购买的商品添加到购物车,因为要实现同一个会话多次请求数据共享,所以假设把数据存入Session对象中
  2. 用户正要付钱的时候接到一个电话,付钱的动作就搁浅了
  3. 正在用户打电话的时候,购物网站因为某些原因需要重启
  4. 重启后session数据被销毁,购物车中的商品信息也就会随之而消失
  5. 用户想再次发起支付,就会出为问题

所以说对于session的数据,我们应该做到就算服务器重启了,也应该能把数据保存下来才对。

那么Tomcat服务器在重启的时候,session数据到底会不会保存以及是如何保存的,我们可以通过实际案例来演示下:

  1. 先启动Tomcat服务器
  2. 访问/demo1将数据存入session中
  3. Ctrl+C正确停止Tomcat服务器
  4. 再次重新启动Tomcat服务器
  5. 访问/demo2 查看是否能获取到session中的数据

经过测试,会发现只要服务器是正常关闭和启动,session中的数据是可以被保存下来的。

那么Tomcat服务器到底是如何做到的呢?

具体的原因就是:Session的钝化和活化:

  • 钝化:在服务器正常关闭后,Tomcat会自动将Session数据写入硬盘的文件中
    • 钝化的数据路径为:项目目录\target\tomcat\work\Tomcat\localhost\项目名称\SESSIONS.ser
  • 活化:再次启动服务器后,从文件中加载数据到Session中
    • 数据加载到Session中后,路径中的SESSIONS.ser文件会被删除掉

对于上述的整个过程,都是Tomcat自己完成的,不需要我们参与,了解一下即可。

小结

Session的钝化和活化介绍完后,需要我们注意的是:

  • session数据存储在服务端,服务器重启后,session数据会被保存
  • 浏览器被关闭启动后,重新建立的连接就已经是一个全新的会话,获取的session数据也是一个新的对象
  • session的数据要想共享,浏览器不能关闭,所以session数据不能长期保存数据
  • cookie是存储在客户端,是可以长期保存

Session的销毁

Session的销毁会有两种方式:

  • 默认情况下,无操作,30分钟自动销毁

    • 对于这个失效时间,是可以通过配置进行修改的
      • 在项目的web.xml中配置
      • 如果没有配置,默认是30分钟,默认值是在Tomcat的web.xml配置文件中写死的
  • 调用Session对象的invalidate()进行销毁

    • 在SessionDemo2类中添加session销毁的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      @WebServlet("/demo2")
      public class SessionDemo2 extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      HttpSession session = request.getSession();
      //销毁
      session.invalidate();
      Object username = session.getAttribute("username");
      System.out.println(username);
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
      }
      }
  • 启动访问测试,先访问demo1将数据存入到session,再次访问demo2从session中获取数据,会得到如下错误信息
    java.lang.IllegalStateException: getAttribute: Session already invalidated

  • 该销毁方法一般会在用户退出的时候,需要将session销毁掉。

Cookie和Session小结

Cookie 和 Session 都是来完成一次会话内多次请求间数据共享的。所需两个对象放在一块,就需要思考:

Cookie和Session的区别是什么?

  • 区别:
    • 存储位置:Cookie 是将数据存储在客户端,Session 将数据存储在服务端
    • 安全性:Cookie不安全,Session安全
    • 数据大小:Cookie最大3KB,Session无大小限制
    • 存储时间:Cookie可以通过setMaxAge()长期存储,Session默认30分钟
    • 服务器性能:Cookie不占服务器资源,Session占用服务器资源

Cookie和Session的应用场景分别是什么?

  • 应用场景:
    • 购物车:使用Cookie来存储
    • 以登录用户的名称展示:使用Session来存储
    • 记住我功能:使用Cookie来存储
    • 验证码:使用session来存储

结论

  • Cookie是用来保证用户在未登录情况下的身份识别
  • Session是用来保存用户登录后的数据

用户登录注册案例

需求分析

在上篇文章,我们已经完成了对品牌数据的增删改查操作,现在我们继续完善登录功能

需求说明:

  1. 完成用户登录功能,如果用户勾选”记住用户” ,则下次访问登录页面自动填充用户名密码
  2. 完成注册功能,并实现验证码功能

用户登录功能

  1. 需求:

    • 用户登录成功后,跳转到列表页面,并在页面上展示当前登录的用户名称
    • 用户登录失败后,跳转回登录页面,并在页面上展示对应的错误信息
  2. 实现流程分析

    • 前端通过表单发送请求和数据给Web层的LoginServlet
    • 在LoginServlet中接收请求和数据[用户名和密码]
    • LoginServlet接收到请求和数据后,调用Service层完成根据用户名和密码查询用户对象
    • 在Service层需要编写UserService类,在类中实现login方法,方法中调用Dao层的UserMapper
    • 在UserMapper接口中,声明一个根据用户名和密码查询用户信息的方法
    • Dao层把数据查询出来以后,将返回数据封装到User对象,将对象交给Service层
    • Service层将数据返回给Web层
    • Web层获取到User对象后,判断User对象,如果为Null,则将错误信息响应给登录页面,如果不为Null,则跳转到列表页面,并把当前登录用户的信息存入Session携带到列表页面。
  3. 具体实现

    • db1数据库下新建tb_user

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      -- 创建用户表
      CREATE TABLE tb_user(
      id int primary key auto_increment,
      username varchar(20) unique,
      password varchar(32)
      );

      -- 添加数据
      INSERT INTO tb_user(username,password) values('zhangsan','123'),('lisi','234');

      SELECT * FROM tb_user;
    • com.blog.pojo下新建User类
      根据数据表中的数据类型创建即可

      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

      package com.blog.pojo;

      public class User {

      private Integer id;
      private String username;
      private String password;

      public Integer getId() {
      return id;
      }

      public void setId(Integer id) {
      this.id = id;
      }

      public String getUsername() {
      return username;
      }

      public void setUsername(String username) {
      this.username = username;
      }

      public String getPassword() {
      return password;
      }

      public void setPassword(String password) {
      this.password = password;
      }

      @Override
      public String toString() {
      return "User{" +
      "id=" + id +
      ", username='" + username + '\'' +
      ", password='" + password + '\'' +
      '}';
      }
      }
    • com.blog.mapper包下新建UserMapper接口
      我们只需要登录,添加(注册)和查询(检测用户名是否已被占用)这三个功能

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      public interface UserMapper {
      @Select("select * from tb_user where username=#{username} and password=#{password}")
      User login(@Param("username") String username, @Param("password") String password);

      @Select("select * from tb_user where username=#{username}")
      User selectByUsername(String username);

      @Insert("insert into tb_user values (null,#{username},#{password})")
      void add(User user);
      }
    • resources/com/itheima/mapper目录下新建UserMapper.xml
      由于我们使用注解开发,所以这里啥也不用写,当SQL语句很复杂的时候,我们才会采用此种方式

      1
      2
      3
      4
      5
      6
      7
      8

      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.blog.mapper.UserMapper">

      </mapper>
    • com.itheima.service包下,创建UserService类
      这里要实现登录的逻辑,上一篇文章也练习过很多遍了

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12

      public class UserService {
      private SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();

      public User login(String username, String password) {
      SqlSession sqlSession = sqlSessionFactory.openSession();
      UserMapper mapper = sqlSession.getMapper(UserMapper.class);
      User user = mapper.login(username, password);
      sqlSession.close();
      return user;
      }
      }
    • 写一个登录的页面login.jsp
      我这里就不搞那些花里胡哨的了,平平淡淡才是真,主要实现的是后端的逻辑,前端暂时无所谓了

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19

      <%@ page contentType="text/html;charset=UTF-8" language="java" %>
      <%@ page isELIgnored="false" %>
      <html>
      <head>
      <title>Title</title>
      </head>
      <body>
      <form action="/brand_demo/loginServlet" method="post">
      <h1>登录系统</h1>
      用户名:<input name="username" type="text"><br>
      密码:<input name="password" type="password"><br>
      记住账号:<input name="remember" type="checkbox" value="1"><br>
      <input value="登录" type="submit">
      <!--这里提前写了一下注册的跳转链接,后面我们会把这个页面也写完-->
      <a href="/brand_demo/register.jsp">没有账号?</a>
      </form>
      </body>
      </html>
    • 创建LoginServlet类

      • 实现登录功能,我们要先获取用户输入的用户名和密码

      • 然后调用service查询User

      • 判断User是否为null

        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

        @WebServlet("/loginServlet")
        public class LoginServlet extends HttpServlet {
        private UserService userService = new UserService();

        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //获取用户名和密码
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        //调用service查询User
        User user = userService.login(username, password);
        if (user != null) {
        //登陆成功,跳转到查询所有BrandServlet
        HttpSession session = request.getSession();
        //将user的信息存储到session域中,便于在brand.jsp页面添加提示信息
        session.setAttribute("user", user);
        response.sendRedirect("/brand_demo/selectAllServlet");
        } else {
        //登陆失败,将错误信息存储到request域中
        request.setAttribute("login_msg", "用户名或密码错误");
        //并跳转到login.jsp
        request.getRequestDispatcher("/login.jsp").forward(request, response);
        }
        }

        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doGet(request, response);
        }
        }
    • 登陆成功,由于我们将user的信息存储到了session域中,我们就可以在brand.jsp页面上添加提示信息<h1>${user.username},欢迎你</h1>
      当我们登陆成功后,会在页面的最上方用h1标题显示username,欢迎你

    • 登陆失败,由于我们将错误提示语句存储到了request域中,所以我们可以在login.jsp中用EL表达式接收一下错误信息,并展示到页面上

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      <form action="/brand_demo/loginServlet" method="post">
      <h1>登录系统</h1>
      <div>${login_msg}</div> <!--当登陆失败的时候,会在这里显示"用户名或密码错误"-->
      用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
      密码:<input name="password" type="password" value="${cookie.password.value}"><br>
      记住账号:<input name="remember" type="checkbox" value="1"><br>
      <input value="登录" type="submit">
      <a href="/brand_demo/register.jsp">没有账号?</a>
      </form>
    • 启动,访问测试,输入错误账号密码,会显示用户名或密码错误,输入正确的账号密码,会跳转到brand.jsp页面,展示所有商品信息,同时页面最上方有username,欢迎你字样

    • 小结

      • 在LoginServlet中,将登录成功的用户数据存入session中,方法在列表页面中获取当前登录用户信息进行展示
      • 在LoginServlet中,将登录失败的错误信息存入到request中,如果存入到session中就会出现这次会话的所有请求都有登录失败的错误信息,这个是不需要的,所以不用存入到session中

记住我-设置Cookie

  1. 需求:
    如果用户勾选”记住用户” ,则下次访问登陆页面自动填充用户名密码。这样可以提升用户的体验。
    对应上面这个需求,最大的问题就是: 如何自动填充用户名和密码?

  2. 实现流程分析

    • 因为

      1
      记住我

      功能要实现的效果是,就算用户把浏览器关闭过几天再来访问也能自动填充,所以需要将登陆信息存入一个可以长久保存,并且能够在浏览器关闭重新启动后依然有效的地方,就是我们前面讲的Cookie,所以:

      • 将用户名和密码写入Cookie中,并且持久化存储Cookie,下次访问浏览器会自动携带Cookie
      • 在页面获取Cookie数据后,设置到用户名和密码框中
      • 何时写入Cookie?
        • 用户必须登陆成功后才需要写
        • 用户必须在登录页面勾选了记住我的复选框
  3. 具体实现

  • 在LoginServlet获取复选框的值并在登录成功后进行设置Cookie
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

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
//这里要多获取一个复选框的值,在之前的jsp代码中,我将复选框的值设为了"1"
String remember = request.getParameter("remember");
User user = userService.login(username, password);
if (user != null) {
//当登录成功后,我们再来判断一下复选框选了没
if ("1".equals(remember)) {
//创建Cookie对象
Cookie c_username = new Cookie("username", username);
//设置一下存活时间为7天,更长也行,随便你
c_username.setMaxAge(7 * 24 * 60 * 60);
Cookie c_password = new Cookie("password", password);
c_password.setMaxAge(7 * 24 * 60 * 60);
//发送Cookie
response.addCookie(c_username);
response.addCookie(c_password);
}
HttpSession session = request.getSession();
session.setAttribute("user", user);
response.sendRedirect("/brand_demo/selectAllServlet");
} else {
request.setAttribute("login_msg", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
  • 启动访问测试,只有当前用户名和密码输入正确,并且勾选了Remeber的复选框,用F12打开开发者工具,监测网络,点击登录,在响应头中可以看得cookie的相关数据

    Set-Cookie: username=zhangsan; Expires=Sat, 27-Aug-2023 14:35:56 GMT
    Set-Cookie: password=123; Expires=Sat, 27-Aug-2023 14:35:56 GMT

记住我-获取Cookie

  1. 需求
    登录成功并勾选了Remeber后,后端返回给前端的Cookie数据就已经存储好了,接下来就需要在页面获取Cookie中的数据,并把数据设置到登录页面的用户名和密码框中。
    如何在页面直接获取Cookie中的值呢?

  2. 实现流程分析
    在页面可以使用EL表达式,${cookie.key.value}
    key:指的是存储在cookie中的键名称,例如${cookie.username.value}

  3. 具体实现

    • 在login.jsp用户名的表单输入框使用value值给表单元素添加默认值,value可以使用${cookie.username.value}

    • 在login.jsp密码的表单输入框使用value值给表单元素添加默认值,value可以使用

      1
      ${cookie.password.value}
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      <form action="/brand_demo/loginServlet" method="post">
      <h1>登录系统</h1>
      <div>${login_msg} ${register_msg}</div>
      用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
      密码:<input name="password" type="password" value="${cookie.password.value}"><br>
      记住账号:<input name="remember" type="checkbox" value="1"><br>
      <input value="登录" type="submit">
      <a href="/brand_demo/register.jsp">没有账号?</a>
      </form>
    • 访问测试,重新访问登录页面,就可以看得用户和密码已经被填充。

用户注册功能

  1. 需求

    • 注册功能:保存用户信息到数据库
    • 验证码功能
      • 展示验证码:展示验证码图片,并可以点击切换
      • 校验验证码:验证码填写不正确,则注册失败
  2. 实现流程分析

    • 前端通过表单发送请求和数据给Web层的RegisterServlet
    • 在RegisterServlet中接收请求和数据[用户名和密码]
    • RegisterServlet接收到请求和数据后,调用Service层完成用户信息的保存
    • 在Service层需要编写UserService类,在类中实现register方法,需要判断用户是否已经存在,如果不存在,则完成用户数据的保存
    • 在UserMapper接口中,声明两个方法,一个是根据用户名查询用户信息方法,另一个是保存用户信息方法
    • 在UserService类中保存成功则返回true,失败则返回false,将数据返回给Web层
    • Web层获取到结果后,如果返回的是true,则提示注册成功,并转发到登录页面,如果返回false则提示用户名已存在并转发到注册页面
  3. 具体实现

    • 在UserService中实现注册逻辑

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      //返回值告诉我们是否注册成功了
      public boolean register(User user) {
      SqlSession sqlSession = sqlSessionFactory.openSession();
      UserMapper mapper = sqlSession.getMapper(UserMapper.class);
      User u = mapper.selectByUsername(user.getUsername());
      if (u == null) {
      //u为null,说明未找到username对应的用户,即用户名不重复,可以注册
      mapper.add(user);
      //注册操作记得提交事务
      sqlSession.commit();
      sqlSession.close();
      }
      sqlSession.close();
      return u == null;
      }
    • 编写一个注册的页面

      1
      register.jsp
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      <%@ page contentType="text/html;charset=UTF-8" language="java" %>
      <%@ page isELIgnored="false" %>
      <html>
      <head>
      <title>Title</title>
      </head>
      <body>
      <form action="/brand_demo/registerServlet" method="post">
      <h1>欢迎注册</h1>
      <!--已有账号的话就跳转至登录页面-->
      已有账号?<a href="login.jsp">点击登录</a><br>
      用户名:<input name="username" type="text"><br>
      密码:<input name="password" type="password"><br>
      <input value="注册" type="submit">
      </form>
      </body>
      </html>
    • 编写RegisterServlet

      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

      @WebServlet("/registerServlet")
      public class RegisterServlet extends HttpServlet {
      private UserService userService = new UserService();

      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      //获取客户输入的用户名和密码
      String username = request.getParameter("username");
      String password = request.getParameter("password");
      //封装成一个User
      User user = new User();
      user.setUsername(username);
      user.setPassword(password);
      //注册账号,用flag来接收是否注册成功
      boolean flag = userService.register(user);
      if (flag){
      //如果成功注册,将注册成功的提示存入request域中,随后跳转到登录页面
      request.setAttribute("register_msg","注册成功请登录");
      request.getRequestDispatcher("/login.jsp").forward(request,response);
      }else {
      //如果注册失败,就是用户名重复了,将错误信息存入request域中,并返回注册页面
      request.setAttribute("register_msg","用户名已存在");
      request.getRequestDispatcher("/register.jsp").forward(request,response);
      }
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
      }
      }
    • 注册成功,跳转至登录页面,我们需要将注册成功的提示显示在登录页面上,修改login.jsp

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11

      <form action="/brand_demo/loginServlet" method="post">
      <h1>登录系统</h1>
      <!--还是放在这里,一个登录的错误提示信息,一个注册成功的提示信息,谁有信息就显示谁-->
      <div>${login_msg} ${register_msg}</div>
      用户名:<input name="username" type="text" value="${cookie.username.value}"><br>
      密码:<input name="password" type="password" value="${cookie.password.value}"><br>
      记住账号:<input name="remember" type="checkbox" value="1"><br>
      <input value="登录" type="submit">
      <a href="/brand_demo/register.jsp">没有账号?</a>
      </form>
    • 注册失败,跳转至注册页面,同时将错误信息展示在注册页面上,修改register.jsp

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      <form action="/brand_demo/registerServlet" method="post">
      <h1>欢迎注册</h1>
      已有账号?<a href="login.jsp">点击登录</a><br>
      用户名:<input name="username" type="text"><br>
      <!--在这里显示一下注册失败的错误信息-->
      <div>${register_msg}</div>
      密码:<input name="password" type="password"><br>
      <input value="注册" type="submit">
      </form>
    • 启动测试,如果是注册的用户信息已经存在,则会显示用户名已存在,如果注册成功,会跳转至登录页面,同时也会显示注册成功请登录提示语句

验证码-展示

  1. 需求分析

    • 展示验证码:展示验证码图片,并可以点击

      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
      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
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281

      package com.blog.util;

      import javax.imageio.ImageIO;
      import java.awt.*;
      import java.awt.geom.AffineTransform;
      import java.awt.image.BufferedImage;
      import java.io.File;
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.io.OutputStream;
      import java.util.Arrays;
      import java.util.Random;

      /**
      * 生成验证码工具类
      */
      public class CheckCodeUtil {

      public static final String VERIFY_CODES = "123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
      private static Random random = new Random();

      /**
      * 输出随机验证码图片流,并返回验证码值(一般传入输出流,响应response页面端,Web项目用的较多)
      *
      * @param w
      * @param h
      * @param os
      * @param verifySize
      * @return
      * @throws IOException
      */
      public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
      String verifyCode = generateVerifyCode(verifySize);
      outputImage(w, h, os, verifyCode);
      return verifyCode;
      }

      /**
      * 使用系统默认字符源生成验证码
      *
      * @param verifySize 验证码长度
      * @return
      */
      public static String generateVerifyCode(int verifySize) {
      return generateVerifyCode(verifySize, VERIFY_CODES);
      }

      /**
      * 使用指定源生成验证码
      *
      * @param verifySize 验证码长度
      * @param sources 验证码字符源
      * @return
      */
      public static String generateVerifyCode(int verifySize, String sources) {
      // 未设定展示源的字码,赋默认值大写字母+数字
      if (sources == null || sources.length() == 0) {
      sources = VERIFY_CODES;
      }
      int codesLen = sources.length();
      Random rand = new Random(System.currentTimeMillis());
      StringBuilder verifyCode = new StringBuilder(verifySize);
      for (int i = 0; i < verifySize; i++) {
      verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
      }
      return verifyCode.toString();
      }

      /**
      * 生成随机验证码文件,并返回验证码值 (生成图片形式,用的较少)
      *
      * @param w
      * @param h
      * @param outputFile
      * @param verifySize
      * @return
      * @throws IOException
      */
      public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
      String verifyCode = generateVerifyCode(verifySize);
      outputImage(w, h, outputFile, verifyCode);
      return verifyCode;
      }

      /**
      * 生成指定验证码图像文件
      *
      * @param w
      * @param h
      * @param outputFile
      * @param code
      * @throws IOException
      */
      public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
      if (outputFile == null) {
      return;
      }
      File dir = outputFile.getParentFile();
      //文件不存在
      if (!dir.exists()) {
      //创建
      dir.mkdirs();
      }
      try {
      outputFile.createNewFile();
      FileOutputStream fos = new FileOutputStream(outputFile);
      outputImage(w, h, fos, code);
      fos.close();
      } catch (IOException e) {
      throw e;
      }
      }

      /**
      * 输出指定验证码图片流
      *
      * @param w
      * @param h
      * @param os
      * @param code
      * @throws IOException
      */
      public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
      int verifySize = code.length();
      BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
      Random rand = new Random();
      Graphics2D g2 = image.createGraphics();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

      // 创建颜色集合,使用java.awt包下的类
      Color[] colors = new Color[5];
      Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
      Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
      Color.PINK, Color.YELLOW};
      float[] fractions = new float[colors.length];
      for (int i = 0; i < colors.length; i++) {
      colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
      fractions[i] = rand.nextFloat();
      }
      Arrays.sort(fractions);
      // 设置边框色
      g2.setColor(Color.GRAY);
      g2.fillRect(0, 0, w, h);

      Color c = getRandColor(200, 250);
      // 设置背景色
      g2.setColor(c);
      g2.fillRect(0, 2, w, h - 4);

      // 绘制干扰线
      Random random = new Random();
      // 设置线条的颜色
      g2.setColor(getRandColor(160, 200));
      for (int i = 0; i < 20; i++) {
      int x = random.nextInt(w - 1);
      int y = random.nextInt(h - 1);
      int xl = random.nextInt(6) + 1;
      int yl = random.nextInt(12) + 1;
      g2.drawLine(x, y, x + xl + 40, y + yl + 20);
      }

      // 添加噪点
      // 噪声率
      float yawpRate = 0.05f;
      int area = (int) (yawpRate * w * h);
      for (int i = 0; i < area; i++) {
      int x = random.nextInt(w);
      int y = random.nextInt(h);
      // 获取随机颜色
      int rgb = getRandomIntColor();
      image.setRGB(x, y, rgb);
      }
      // 添加图片扭曲
      shear(g2, w, h, c);

      g2.setColor(getRandColor(100, 160));
      int fontSize = h - 4;
      Font font = new Font("Algerian", Font.ITALIC, fontSize);
      g2.setFont(font);
      char[] chars = code.toCharArray();
      for (int i = 0; i < verifySize; i++) {
      AffineTransform affine = new AffineTransform();
      affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
      g2.setTransform(affine);
      g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
      }

      g2.dispose();
      ImageIO.write(image, "jpg", os);
      }

      /**
      * 随机颜色
      *
      * @param fc
      * @param bc
      * @return
      */
      private static Color getRandColor(int fc, int bc) {
      if (fc > 255) {
      fc = 255;
      }
      if (bc > 255) {
      bc = 255;
      }
      int r = fc + random.nextInt(bc - fc);
      int g = fc + random.nextInt(bc - fc);
      int b = fc + random.nextInt(bc - fc);
      return new Color(r, g, b);
      }

      private static int getRandomIntColor() {
      int[] rgb = getRandomRgb();
      int color = 0;
      for (int c : rgb) {
      color = color << 8;
      color = color | c;
      }
      return color;
      }

      private static int[] getRandomRgb() {
      int[] rgb = new int[3];
      for (int i = 0; i < 3; i++) {
      rgb[i] = random.nextInt(255);
      }
      return rgb;
      }

      private static void shear(Graphics g, int w1, int h1, Color color) {
      shearX(g, w1, h1, color);
      shearY(g, w1, h1, color);
      }

      private static void shearX(Graphics g, int w1, int h1, Color color) {

      int period = random.nextInt(2);

      boolean borderGap = true;
      int frames = 1;
      int phase = random.nextInt(2);

      for (int i = 0; i < h1; i++) {
      double d = (double) (period >> 1)
      * Math.sin((double) i / (double) period
      + (6.2831853071795862D * (double) phase)
      / (double) frames);
      g.copyArea(0, i, w1, 1, (int) d, 0);
      if (borderGap) {
      g.setColor(color);
      g.drawLine((int) d, i, 0, i);
      g.drawLine((int) d + w1, i, w1, i);
      }
      }

      }

      private static void shearY(Graphics g, int w1, int h1, Color color) {

      int period = random.nextInt(40) + 10; // 50;

      boolean borderGap = true;
      int frames = 20;
      int phase = 7;
      for (int i = 0; i < w1; i++) {
      double d = (double) (period >> 1)
      * Math.sin((double) i / (double) period
      + (6.2831853071795862D * (double) phase)
      / (double) frames);
      g.copyArea(i, 0, 1, h1, 0, (int) d);
      if (borderGap) {
      g.setColor(color);
      g.drawLine(i, (int) d, i, 0);
      g.drawLine(i, (int) d + h1, i, h1);
      }

      }

      }
      }
    • 验证码就是使用Java代码生成的一张图片

    • 验证码的作用:防止机器自动注册,攻击服务器

  2. 实现流程分析

    • 前端发送请求给CheckCodeServlet
    • CheckCodeServlet接收到请求后,生成验证码图片,将图片用Reponse对象的输出流写回到前端
    • 思考:如何将图片写回到前端浏览器呢?
      • 前面在学Reponse对象的时候,它有一个方法可以获取其字节输出流,getOutputStream()
      • 我们可以把写往磁盘的流对象更好成Response的字节流,即可完成图片响应给前端
  3. 具体实现

    • 修改Register.jsp页面,将验证码的图片从后台获取

      1
      看不清

      绑定一个点击事件,

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19

      <form action="/brand_demo/registerServlet" method="post">
      <h1>欢迎注册</h1>
      已有账号?<a href="login.jsp">点击登录</a><br>
      用户名:<input name="username" type="text"><br>
      <div>${register_msg}</div>
      密码:<input name="password" type="password"><br>
      验证码:<input name="checkCode" type="text">
      <img id="checkCodeImg" src="/brand_demo/checkCodeServlet">
      <a href="#" id="checkImg">看不清?</a>
      <input value="注册" type="submit">
      </form>

      <script>
      document.getElementById("checkImg").onclick = function () {
      //路径后面加一个时间戳,能保证生成的图片永远不一样,避免浏览器缓存静态资源
      document.getElementById("checkCodeImg").src = "/brand_demo/checkCodeServlet?" + new Date().getMilliseconds();
      }
      </script>
    • 编写CheckCodeServlet类,用来接收请求生成验证码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15

      @WebServlet("/checkCodeServlet")
      public class CheckCodeServlet extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      //生成验证码
      OutputStream os = response.getOutputStream();
      String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
      }
      }
    • 完成这些之后,我们启动服务器,就可以看到验证码了,点击看不清,也会自动刷新验证码

验证码-校验

  1. 需求

    • 判断程序生成的验证码 和 用户输入的验证码 是否一样,如果不一样,则阻止注册
    • 验证码图片访问和提交注册表单是两次请求,所以要将程序生成的验证码存入Session中

    • 思考:为什么要把验证码数据存入到Session中呢?

      • 生成验证码和校验验证码是两次请求,此处就需要在一个会话的两次请求之间共享数据
      • 验证码属于安全数据类的,所以我们选中Session来存储验证码数据。
  2. 实现流程分析

    • 在CheckCodeServlet中生成验证码的时候,将验证码数据存入Session对象
    • 前端将验证码和注册数据提交到后台,交给RegisterServlet类
    • RegisterServlet类接收到请求和数据后,其中就有验证码,和Session中的验证码进行对比
    • 如果一致,则完成注册,如果不一致,则提示错误信息
  3. 具体实现

    • 修改CheckCodeServlet类,将验证码存入Session域

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      @WebServlet("/checkCodeServlet")
      public class CheckCodeServlet extends HttpServlet {
      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      //生成验证码
      OutputStream os = response.getOutputStream();
      String checkCode = CheckCodeUtil.outputVerifyImage(100, 50, os, 4);
      //将验证码存入session域
      HttpSession session = request.getSession();
      session.setAttribute("checkCodeGen", checkCode);
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
      }
      }
    • 在RegisterServlet中,获取页面的和session对象中的验证码,进行对比

      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

      @WebServlet("/registerServlet")
      public class RegisterServlet extends HttpServlet {
      private UserService userService = new UserService();

      @Override
      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      //获取客户输入的用户名和密码
      String username = request.getParameter("username");
      String password = request.getParameter("password");
      //多获取一个用户输入的验证码
      String checkCode = request.getParameter("checkCode");
      //封装成一个User
      User user = new User();
      user.setUsername(username);
      user.setPassword(password);
      //从Session中获取验证码
      HttpSession session = request.getSession();
      String checkCodeGen = (String) session.getAttribute("checkCodeGen");
      //如果验证码不一致
      if (!checkCodeGen.equals(checkCode)) {
      //将错误信息存储到request域中
      request.setAttribute("checkCode_msg", "验证码输入错误");
      //同时跳转至注册页面
      request.getRequestDispatcher("/register.jsp").forward(request, response);
      //不允许注册,结束本次操作
      return;
      }
      //注册账号,用flag来接收是否注册成功
      boolean flag = userService.register(user);
      if (flag) {
      //如果成功注册,将注册成功的提示存入request域中,随后跳转到登录页面
      request.setAttribute("register_msg", "注册成功请登录");
      request.getRequestDispatcher("/login.jsp").forward(request, response);
      } else {
      //如果注册失败,就是用户名重复了,将错误信息存入request域中,并返回注册页面
      request.setAttribute("register_msg", "用户名已存在");
      request.getRequestDispatcher("/register.jsp").forward(request, response);
      }
      }

      @Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
      this.doGet(request, response);
      }
      }
    • 验证码错误,需要在注册页面上显示提示信息,修改

      1
      register.jsp
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

      <form action="/brand_demo/registerServlet" method="post">
      <h1>欢迎注册</h1>
      已有账号?<a href="login.jsp">点击登录</a><br>
      用户名:<input name="username" type="text"><br>
      <div>${register_msg}</div>
      密码:<input name="password" type="password"><br>
      <div>${checkCode_msg}</div> <!--在这里显示一个提示信息-->
      验证码:<input name="checkCode" type="text">
      <img id="checkCodeImg" src="/brand_demo/checkCodeServlet">
      <a href="#" id="checkImg">看不清?</a>
      <input value="注册" type="submit">
      </form>
    • 至此,用户的注册登录功能就已经完成了。

本文摘要:

  • 能够使用 Filter 完成登陆状态校验功能
  • 能够使用 axios 发送 ajax 请求
  • 熟悉 json 格式,并能使用 Fastjson 完成 java 对象和 json 串的相互转换
  • 使用 axios + json 完成综合案例

Filter

Filter概述

Filter 表示过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。我们之前都已经学习过了Servlet ,现在我们来学习Filter和Listener。

过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。

例如某些网站未登录不能查看评论,不能将商品加入购物车,得把你拦下来,先让你登录,也就是在访问前,先经过Filter。

下面来具体说说,拦截器拦截到后可以做什么功能呢?

过滤器一般完成一些通用的操作。比如每个资源都要写一些代码完成某个功能,我们总不能在每个资源中写这样的代码吧,而此时我们可以将这些代码写在过滤器中,因为请求每一个资源都要经过过滤器。

我们之前做的品牌数据管理的案例中就已经做了登陆的功能,但这个登录功能其实是如同虚设的,我们可以直接访问登录后的页面,所以本文的目标就是完善登录功能,不登录就无法查看数据。

Filter入门

开发步骤

进行Filter开发分为以下三步实现

  1. 定义类,实现Filter接口,并重写其所有方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    //实现Filter接口,重写所有方法
    public class FilterDemo1 implements Filter {

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

    }

    public void destroy() {

    }
    }
  2. 配置Filter拦截路径资源:在类上定义

    1
    @WebFilter

    注解。而注解的value属性值

    1
    /*

    表示拦截所有资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    //这里暂时先拦截所有资源,后面我们会仔细讲拦截路径的配置
    @WebFilter("/*")
    public class FilterDemo1 implements Filter {

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

    }

    public void destroy() {

    }
    }
  3. 在doFilter方法中输出一句话,并放行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    @WebFilter("/*")
    public class FilterDemo1 implements Filter {

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    //这句话只是来测试是否调用了该方法
    System.out.println("doFilter...");
    //一定要放行才能访问资源
    filterChain.doFilter(servletRequest, servletResponse);
    }

    public void destroy() {

    }
    }

代码演示

  • 创建一个web项目,在webapp下创建hello.jsp页面

    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</title>
    </head>
    <body>

    <!--随便输出点什么东西-->
    <h1>HELLO FILTER</h1>

    </body>
    </html>
  • pom.xml配置如下

    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

    <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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>filter-demo</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>filter-demo Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <dependencies>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>3.8.1</version>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5-20081211</version>
    </dependency>
    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>compile</scope>
    </dependency>
    </dependencies>
    <build>
    <finalName>filter-demo</finalName>
    </build>
    </project>
  • 在java目录下新建com.blog.web.filter包,并新建FilterDemo1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    @WebFilter("/*")
    public class FilterDemo1 implements Filter {

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    System.out.println("doFilter...");
    filterChain.doFilter(servletRequest, servletResponse);
    }

    public void destroy() {

    }
    }
  • 测试

    • 将放行的代码注释掉,我们访问hello.jsp页面,不会有任何内容,因为被拦截了,且没有放行,控制台会输出doFilter...
    • 打开放行的代码,我们访问hello.jsp页面,会有h1标签正常输出HELLO FILTER

上述效果说明了FilterDemo1这个过滤器的doFilter方法被执行了,且必须添加放行的方法才能访问hello.jsp页面

Filter执行流程

Filter执行流程.png

如上图是使用过滤器的流程,我们通过以下问题来研究过滤器的执行流程:

  • 放行后访问对应资源,资源访问完成后,还会回到Filter中吗?
    • 从上图就可以看出肯定会回到Filter中
  • 如果回到Filter中,是重头执行还是执行放行后的逻辑呢?
    • 如果是重头执行的话,就意味着 放行前逻辑 会被执行两次,肯定不会这样设计了;所以访问完资源后,会回到 放行后逻辑,执行该部分代码。

通过上述的说明,我们可以总结一下Filter的执行流程

  • 执行放行前逻辑 —> 放行 —> 访问资源 —> 执行放行后逻辑

接下来我们通过代码验证一下,在 doFilter() 方法前后都加上输出语句

1
2
3
4
5
6

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("1.Filter...");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("3.Filter...");
}

hello.jsp中添加输出语句如下

1
2
3
4
5
6
7

<body>
<h1>HELLO FILTER</h1>
<%
System.out.println("2.Filter...");
%>
</body>

重启服务器,访问hello.jsp页面,控制台输出结果如下,符合我们的预期结果

1.Filter…
2.Filter…
3.Filter…

Filter拦截路径配置

拦截路径表示 Filter 会对请求的哪些资源进行拦截,使用 @WebFilter 注解进行配置。如:@WebFilter("拦截路径")

拦截路径有如下四种配置方式:

  • 拦截具体的资源:/index.jsp:只有访问index.jsp时才会被拦截
  • 目录拦截:/user/*:访问/user下的所有资源,都会被拦截
  • 后缀名拦截:*.jsp:访问后缀名为jsp的资源,都会被拦截
  • 拦截所有:/*:访问所有资源,都会被拦截

过滤器链

概述

过滤器链是指在一个Web应用,可以配置多个过滤器,这多个过滤器称为过滤器链。

如下图就是一个过滤器链,我们学习过滤器链主要是学习过滤器链执行的流程

过滤器链.png

上图中的过滤器链执行是按照以下流程执行:

  1. 执行 Filter1 的放行前逻辑代码
  2. 执行 Filter1 的放行代码
  3. 执行 Filter2 的放行前逻辑代码
  4. 执行 Filter2 的放行代码
  5. 访问到资源
  6. 执行 Filter2 的放行后逻辑代码
  7. 执行 Filter1 的放行后逻辑代码

以上流程串起来就像一条链子,故称之为过滤器链。

代码演示

  • 我们在

    1
    com.blog.web.filter

    包下再新建一个

    1
    FilterDemo2

    类,并实现Filter接口,重写其所有方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    @WebFilter("/*")
    public class FilterDemo2 implements Filter {
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    System.out.println("2.Filter...");
    filterChain.doFilter(servletRequest, servletResponse);
    System.out.println("4.Filter...");
    }

    public void destroy() {

    }
    }
  • 修改

    1
    FilterDemo1

    类的

    1
    doFilter

    方法,将输出语句调成我们预期的结果,最终测试的时候,看看是否符合预期

    1
    2
    3
    4
    5
    6

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    System.out.println("1.Filter...");
    filterChain.doFilter(servletRequest, servletResponse);
    System.out.println("5.Filter...");
    }
  • 修改

    1
    hello.jsp

    ,将输出语句改为预期结果

    1
    2
    3
    4
    5
    6
    7

    <body>
    <h1>HELLO FILTER</h1>
    <%
    System.out.println("3.Filter...");
    %>
    </body>
  • 重启服务器,访问

    1
    hello.jsp

    页面,控制台输出如下,符合我们预期的结果

    1.Filter…
    2.Filter…
    3.Filter…
    4.Filter…
    5.Filter…

问题

上面代码中为什么是先执行 FilterDemo ,后执行 FilterDemo2 呢?

1
2
- 我们现在使用的是注解配置Filter,而这种配置方式的优先级是按照过滤器类名(字符串)的自然排序。
- 比如有如下两个名称的过滤器 : `BFilterDemo` 和 `AFilterDemo` 。那一定是 `AFilterDemo` 过滤器先执行。

案例

需求

访问服务器资源时,需要先进行登录验证,如果没有登录,则自动跳转到登录页面

分析

要是搁以前,我们要实现该功能可能得在每一个资源里加入登陆状态校验的代码
但现在,只需要写一个 Filter ,在该过滤器中进行登陆状态校验即可。而在该 Filter 中逻辑如下:

  • 判断访问的是否为登录之后才能看的资源
    • 是:放行
    • 不是:进行登录验证
  • 判断用户是否登录:Session中是否有user对象
    • 登录:放行
    • 未登录:跳转至登录页面,并给出提示信息

代码实现

创建Filter

brand-demo 工程创建 com.itheima.web.filter 包,在该下创建名为 LoginFilter 的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {

}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}

编写逻辑代码

doFilter() 方法中编写登陆状态校验的逻辑代码。

我们首先需要从 session 对象中获取用户信息,但是 ServletRequest 类型的 requset 对象没有获取 session 对象的方法,所以此时需要将 request对象强转成 HttpServletRequest 对象。

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

@WebFilter("/*")
public class LoginFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
//强转操作
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpSession session = httpRequest.getSession();
//获取是否有user对象
Object user = session.getAttribute("user");
//如果有user对象,那么就是已登录状态,直接放行
if (user != null) {
chain.doFilter(request, response);
//如果没有user对象,那就是未登录,不允许访问,给出错误信息并跳转到登录页面
} else {
httpRequest.setAttribute("login_msg", "您尚未登录");
httpRequest.getRequestDispatcher("/login.jsp").forward(request, response);
}
}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}

测试并抛出问题

重启服务器,访问register.jsp注册页面,竟然访问不了了,也是直接跳转到了登录页面,而且如果你配置了css文件,css样式也显示不出来了,这是怎么回事呢?

问题分析及解决

因为我们配置的是对所有页面进行拦截,但现在需要对所有的登陆和注册相关的资源进行放行。
所以我们需要在判断session中是否包含用户信息之前,应该加上对登陆及注册相关资源放行的逻辑处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

HttpServletRequest httpRequest = (HttpServletRequest) request;
//在数组中存储注册和登录相关的资源路径
String[] urls = {"/login.jsp","/register.jsp","/checkCodeServlet","/registerServlet","/loginServlet"};
//获取当前的访问路径
String url = httpRequest.getRequestURL().toString();
//遍历数组
for (String u : urls) {
/*
判断当前访问的资源路径字符串是否包含要放行的的资源路径字符串
比如当前访问的资源路径是 /brand-demo/login.jsp
而字符串 /brand-demo/login.jsp 包含了字符串 /login.jsp ,所以这个字符串就需要放行
*/
if (url.contains(u)){
//找到了就放行
chain.doFilter(request,response);
return;
}
}

过滤器完整代码

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

@WebFilter("/*")
public class LoginFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String[] urls = {"/login.jsp","/register.jsp","/checkCodeServlet","/registerServlet","/loginServlet"};
String url = httpRequest.getRequestURL().toString();
for (String u : urls) {
if (url.contains(u)){
chain.doFilter(request,response);
return;
}
}

HttpSession session = httpRequest.getSession();
Object user = session.getAttribute("user");
if (user != null) {
chain.doFilter(request, response);
} else {
httpRequest.setAttribute("login_msg", "您尚未登录");
httpRequest.getRequestDispatcher("/login.jsp").forward(request, response);
}
}

public void init(FilterConfig config) throws ServletException {
}

public void destroy() {
}
}

Listener

概述

  • Listener 表示监听器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。

  • 监听器可以监听就是在 applicationsessionrequest 三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。

    request 和 session 我们学习过。而 applicationServletContext 类型的对象。

    ServletContext 代表整个web应用,在服务器启动的时候,tomcat会自动创建该对象。在服务器关闭时会自动销毁该对象。

分类

JavaWeb 提供了8个监听器:

监听器分类 监听器名称 作用
servletContext监听 servletContextListener 用于对ServletContext对象进行监听(创建、销毁)
ServletContextAttributeListener 对ServletContext对象中属性的监听(增删改属性)
session监听 HttpSessionListener 对Session对象的整体状态的监听(创建、销毁)
HttpSessionAttributeListener 对Session对象中的属性监听(增删改属性)
HttpSessionBindingListener 监听对象于Session的绑定和解除
HttpsessionActivationListener 对Session数据的钝化和活化的监听
Request监听 servletRequestListener 对Request对象进行监听(创建、销毁)
servletRequestAttributeListener 对Request对象中属性的监听(增删改属性)

这里面只有 ServletContextListener 这个监听器后期我们会接触到,ServletContextListener 是用来监听 ServletContext 对象的创建和销毁。

ServletContextListener 接口中有以下两个方法

  • void contextInitialized(ServletContextEvent sce)ServletContext 对象被创建了会自动执行的方法
  • void contextDestroyed(ServletContextEvent sce)ServletContext 对象被销毁时会自动执行的方法

代码演示

我们只演示一下 ServletContextListener 监听器

  • 定义一个类,实现ServletContextListener 接口
  • 重写所有的抽象方法
  • 使用 @WebListener 进行配置

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@WebListener
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
//加载资源
System.out.println("ContextLoaderListener...");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
//释放资源
}
}

启动服务器,就可以在控制台输出了 ContextLoaderListener...,同时也说明了 ServletContext 对象在服务器启动的时候被创建了。

Ajax

概述

AJAX (Asynchronous JavaScript And XML):异步的 JavaScript 和 XML。

作用

AJAX 作用有以下两方面:

  1. 与服务器进行数据交换:通过AJAX可以给服务器发送请求,服务器将数据直接响应回给浏览器。
    • 在之前,我们做功能的流程是,Servlet 调用完业务逻辑层,然后将数据存储到域对象中,然后跳转到指定的 jsp 页面,在页面上使用 EL表达式JSTL 标签库进行数据的展示。
    • 而我们学习了AJAX 后,就可以使用AJAX和服务器进行通信,以达到使用HTML+AJAX来替换JSP页面了。浏览器发送请求servlet,servlet 调用完业务逻辑层后将数据直接响应回给浏览器页面,页面使用 HTML 来进行数据展示。
  2. 异步交互:可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术,如:搜索联想、用户名是否可用校验,等等…
    • 当我们在百度输入一些关键字(例如 奥运)后就会在下面联想出相关的内容,而联想出来的这部分数据肯定是存储在百度的服务器上,而我们并没有看出页面重新刷新,这就是更新局部页面的效果。
    • 我们在用户名的输入框输入用户名,当输入框一失去焦点,如果用户名已经被占用就会在下方展示提示的信息;在这整个过程中也没有页面的刷新,只是在局部展示出了提示信息,这就是更新局部页面的效果。

同步与异步

知道了局部刷新后,接下来我们再聊聊同步和异步:

  • 同步发送请求过程如下
    同步请求.png
    浏览器页面在发送请求给服务器,在服务器处理请求的过程中,浏览器页面不能做其他的操作。只能等到服务器响应结束后才能,浏览器页面才能继续做其他的操作。
  • 异步发送请求过程如下
    异步请求.png
    浏览器页面发送请求给服务器,在服务器处理请求的过程中,浏览器页面还可以做其他的操作。

复盘补充:
所谓异步,就是主程序一直往下走,延迟和等待程序放在另一个列表中等待执行,不阻塞主程序继续往下进行
JS中大部分都是同步,少数的异步有以下几个

  • 定时器settimeout和steInterval
  • ajax异步请求
  • promise
  • ……

异步的好处是:它不会因为延时和等待阻塞程序
但异步存在一些问题,例如原计划从1到4执行

  1. 挂加速器
  2. 打开steam
  3. 上号
  4. 打派派

但现在3是异步操作,结果变成1243,没登录账号,你打个锤子的派派

快速入门

创建一个ajax-demo的web项目,并导入servlet的坐标

服务端实现

在项目的java目录下创建 com.itheima.blog.servlet包,并在该包下创建名为 AjaxServlet 的servlet

1
2
3
4
5
6
7
8
9
10
11
12
13

@WebServlet("/ajaxServlet")
public class AjaxServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("HELLO AJAX");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}

客户端实现

webapp 下创建名为 01-ajax-demo1.html 的页面,在该页面书写 ajax 代码

  • 创建核心对象,不同的浏览器创建的对象是不同的

    1
    2
    3
    4
    5
    6
    7
    8

    var xhttp;
    if (window.XMLHttpRequest) {
    xhttp = new XMLHttpRequest();
    } else {
    // code for IE6, IE5
    xhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
  • 发送请求

    1
    2
    3
    4
    5

    //建立连接,这里要写全地址,因为在后期,前端的代码和后端的代码并不是部署在同一个服务器上
    xhttp.open("GET", "http://localhost:8080/ajax_demo/ajaxServlet");
    //发送请求
    xhttp.send();
  • 获取响应

    1
    2
    3
    4
    5
    6
    7

    xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
    // 通过 this.responseText 可以获取到服务端响应的数据
    alert(this.responseText);
    }
    };

完整代码如下:

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

</body>

<script>
var xhttp;
if (window.XMLHttpRequest) {
xhttp = new XMLHttpRequest();
} else {
// code for IE6, IE5
xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
//建立连接
xhttp.open("GET", "http://localhost:8080/ajax_demo/ajaxServlet");
//发送请求
xhttp.send();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
// 通过 this.responseText 可以获取到服务端响应的数据
alert(this.responseText);
}
};
</script>
</html>

测试

在浏览器地址栏输入 http://localhost:8080/ajax-demo/01-ajax-demo1.html ,在 01-ajax-demo1.html加载的时候就会发送 ajax 请求,并有一个alert弹窗输出HELLO AJAX

案例

需求:在完成用户注册时,当用户名输入框失去焦点时,校验用户名是否在数据库已存在

分析

  • 前端完成的逻辑
    1. 给用户名输入框绑定光标失去焦点事件 onblur
    2. 发送 ajax请求,携带username参数
    3. 处理响应:是否显示提示信息
  • 后端完成的逻辑
    1. 接收用户名
    2. 调用service查询User。此案例是为了演示前后端异步交互,所以此处我们不做业务逻辑处理
    3. 返回标记

后端实现

com.blog.web.servlet 包中定义名为 SelectUserServlet 的servlet。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

@WebServlet("/selectUserServlet")
public class SelectUserServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//接收用户名
String username = request.getParameter("username");
//2. 调用service查询User对象,此处不进行业务逻辑处理,直接给 flag 赋值为 true,表明用户名占用
boolean flag = true;
//响应标记
response.getWriter().write("" + flag);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}

前端实现

  • 随便写一个注册的页面

    1
    register.html
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    <!DOCTYPE html>
    <html>
    <head>
    <title>Title</title>
    <meta charset="utf-8">
    </head>
    <body>
    <form action="/ajax_demo/selectUserServlet" method="get">
    <h1>欢迎注册</h1>
    用户名:<input name="username" type="text" id="username">
    <span id="username_arr" style="display:none;">用户名已被占用</span><br>
    密码:<input name="password" type="password"><br>
    <input value="注册" type="submit">
    </form>
    </body>
    </html>
  • 然后给用户名输入框绑定光标失去焦点事件

1
onblur
1
2
3
4

document.getElementById("username").onblur = function () {

}
  • 发送 ajax请求,携带username参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    // 发送ajax请求
    // 获取用户名
    var username = this.value;
    var xhttp;
    if (window.XMLHttpRequest) {
    xhttp = new XMLHttpRequest();
    } else {
    // code for IE6, IE5
    xhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    // 发送请求,由于我们发送的是 GET 请求,所以需要在 URL 后拼接从输入框获取的用户名数据。
    xhttp.open("GET", "http://localhost:8080/ajax-demo/selectUserServlet?username=" + username);
    xhttp.send();

    // 获取响应
    xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
    //处理响应的结果
    }
    };
  • 处理响应:是否显示提示信息
    this.readyState == 4 && this.status == 200 条件满足时,说明已经成功响应数据了。
    此时需要判断响应的数据是否是 “true” 字符串,如果是说明用户名已经占用给出错误提示;如果不是说明用户名未被占用清除错误提示。代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9

    //判断
    if(this.responseText == "true"){
    //用户名存在,显示提示信息
    document.getElementById("username_err").style.display = '';
    }else {
    //用户名不存在 ,清楚提示信息
    document.getElementById("username_err").style.display = 'none';
    }
  • 综上所述,前端的完整代码如下

    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

    <!DOCTYPE html>
    <html>
    <head>
    <title>Title</title>
    <meta charset="utf-8">
    </head>
    <body>
    <form action="/ajax_demo/selectUserServlet" method="get">
    <h1>欢迎注册</h1>
    用户名:<input name="username" type="text" id="username">
    <span id="username_arr" style="display:none;">用户名已被占用</span><br>
    密码:<input name="password" type="password"><br>
    <input value="注册" type="submit">
    </form>

    <script>
    document.getElementById("username").onblur = function () {
    var xhttp;
    var username = this.value;
    if (window.XMLHttpRequest) {
    xhttp = new XMLHttpRequest();
    } else {
    // code for IE6, IE5
    xhttp = new ActiveXObject("Microsoft.XMLHTTP");
    }
    //建立连接
    xhttp.open("GET", "http://localhost:8080/ajax_demo/selectUserServlet?username=" + username);
    //发送请求
    xhttp.send();
    xhttp.onreadystatechange = function () {
    if (this.readyState == 4 && this.status == 200) {
    // 通过 this.responseText 可以获取到服务端响应的数据
    // alert(this.responseText);
    if (this.responseText == "true") {
    document.getElementById("username_arr").style.display = '';
    } else {
    document.getElementById("username_arr").style.display = 'none';
    }
    }
    };
    }
    </script>
    </body>
    </html>
  • 测试
    重启服务器,访问http://localhost:8080/ajax_demo/register.html,随便输入用户名之后,鼠标点击其他位置失去焦点,会显示用户名已被占用

Axios

Axios 对原生的AJAX进行封装,简化书写。

Axios官网是:https://www.axios-http.cn

基本使用

  • 使用axios 发送请求,并获取响应结果

    • 发送 get 请求

      1
      2
      3
      4
      5
      6
      7

      axios({
      method:"get",
      url:"http://localhost:8080/ajax-demo1/ajaxDemo?username=zhangsan"
      }).then(function (resp){
      alert(resp.data);
      })
    • 发送 post 请求

      1
      2
      3
      4
      5
      6
      7
      8

      axios({
      method:"post",
      url:"http://localhost:8080/ajax-demo1/ajaxDemo",
      data:"username=zhangsan"
      }).then(function (resp){
      alert(resp.data);
      });

axios() 是用来发送异步请求的,小括号中使用 js 对象传递请求相关的参数:

  • method 属性:用来设置请求方式的。取值为 get 或者 post
  • url 属性:用来书写请求的资源路径。如果是 get 请求,需要将请求参数拼接到路径的后面,格式为: url?参数名=参数值&参数名2=参数值2
  • data 属性:作为请求体被发送的数据。也就是说如果是 post 请求的话,数据需要作为 data 属性的值。
  • then() 需要传递一个匿名函数。我们将 then() 中传递的匿名函数称为回调函数,意思是该匿名函数在发送请求时不会被调用,而是在成功响应后调用的函数。而该回调函数中的 resp 参数是对响应的数据进行封装的对象,通过 resp.data 可以获取到响应的数据。

快速入门

后端实现

  • 定义一个用于接收请求的servlet,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    @WebServlet("/ajaxServlet")
    public class AjaxServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("get...");
    //接收请求参数
    String username = request.getParameter("username");
    System.out.println(username);
    //响应数据
    response.getWriter().write("HELLO AXIOS");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    System.out.println("post...");
    this.doGet(request, response);
    }
    }

前端实现

  • 引入 js 文件

    1
    2

    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  • 发送 ajax 请求

    • get 请求

      1
      2
      3
      4
      5
      6
      7

      axios({
      method: "get",
      url: "http://localhost:8080/ajax_demo/ajaxServlet?username=zhangsan"
      }).then(function (resp) {
      alert(resp.data);
      })
    • post 请求

      1
      2
      3
      4
      5
      6
      7
      8

      axios({
      method: "post",
      url: "http://localhost:8080/ajax_demo/ajaxServlet",
      data: "username=zhangsan"
      }).then(function (resp) {
      alert(resp.data);
      })
  • 整体页面代码如下:

    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

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
    // axios({
    // method: "get",
    // url: "http://localhost:8080/ajax_demo/ajaxServlet?username=zhangsan"
    // }).then(function (resp) {
    // alert(resp.data);
    // })

    axios({
    method: "post",
    url: "http://localhost:8080/ajax_demo/ajaxServlet",
    data: "username=zhangsan"
    }).then(function (resp) {
    alert(resp.data);
    })

    </script>
    </body>
    </html>
  • 分别测试get请求和post请求,

    • get请求会在页面上出现一个alert弹窗显示

      1
      HELLO AXIOS

      ,同时控制台输出

      get…
      zhangsan

    • post请求会在页面上出现一个alert弹窗显示

      1
      HELLO AXIOS

      ,同时控制台输出

      post…
      get…
      zhangsan

请求方法别名

为了方便起见, Axios 已经为所有支持的请求方法提供了别名。如下:

  • get 请求 : axios.get(url[,config])
  • delete 请求 : axios.delete(url[,config])
  • head 请求 : axios.head(url[,config])
  • options 请求 : axios.option(url[,config])
  • post 请求:axios.post(url[,data[,config])
  • put 请求:axios.put(url[,data[,config])
  • patch 请求:axios.patch(url[,data[,config])

而我们只关注 get 请求和 post 请求。

  • 入门案例中的
1
get

请求代码可以改为如下:

1
2
3
4

axios.get("http://localhost:8080/ajax-demo/axiosServlet?username=zhangsan").then(function (resp) {
alert(resp.data);
});
  • 入门案例中的
1
post

请求代码可以改为如下:

1
2
3
4

axios.post("http://localhost:8080/ajax-demo/axiosServlet","username=zhangsan").then(function (resp) {
alert(resp.data);
})

JSON

概述

概念:JavaScript Object Notation。JavaScript 对象表示法.

如下是 JavaScript 对象的定义格式:

1
2
3
4
5
6

{
name:"zhangsan",
age:23,
city:"北京"
}

接下来我们再看看 JSON 的格式:

1
2
3
4
5
6

{
"name":"zhangsan",
"age":23,
"city":"北京"
}

通过上面 js 对象格式和 json 格式进行对比,发现两个格式特别像。只不过 js 对象中的属性名可以使用引号(可以是单引号,也可以是双引号);而 json 格式中的键要求必须使用双引号括起来,这是 json 格式的规定。json 格式的数据有什么作用呢?

作用:由于其语法格式简单,层次结构鲜明,现多用于作为数据载体

JSON基础语法

定义格式

JSON本质就是一个字符串,但是该字符串内容是有一定的格式要求的。 定义格式如下:

1
2

var 变量名 = {"key":value,"key":value,...};

JSON串的键要求必须使用双引号括起来,而值根据要表示的类型确定。value 的数据类型分为如下

  • 数字(整数或浮点数)
  • 字符串(使用双引号括起来)
  • 逻辑值(true或者false)
  • 数组(在方括号中)
  • 对象(在花括号中)
  • null

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
let jsonStr = '{"name":"zhangsan","age":18,"addr":["北京","上海","广州","深圳"]}';
alert(jsonStr);
</script>
</body>
</html>

通过浏览器打开,浏览器会有一个弹窗显示`{“name”:”zhangsan”,”age”:18,”addr”:[“北京”,”上海”,”广州”,”深圳”]}

现在我们需要获取到该 JSON 串中的 name 属性值,应该怎么处理呢?

  • 如果它是一个 js 对象,我们就可以通过 js对象.属性名 的方式来获取数据。JS 提供了一个对象 JSON ,该对象有如下两个方法:

    • parse(str) :将 JSON串转换为 js 对象。使用方式是:var jsObject = JSON.parse(jsonStr);
    • stringify(obj) :将 js 对象转换为 JSON 串。使用方式是:var jsonStr = JSON.stringify(jsObject)
  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <script>
    let jsonStr = '{"name":"zhangsan","age":18,"addr":["北京","上海","广州","深圳"]}';
    alert(jsonStr);
    //将json字符串转化为js对象
    let jsonObj = JSON.parse(jsonStr);
    alert(jsonObj);
    alert(jsonObj.name);
    alert(jsonObj.age);
    alert(jsonObj.addr);
    //将js对象转化为json字符串
    let jsonStr2 = JSON.stringify(jsonObj);
    alert(jsonStr2);
    </script>
    </body>
    </html>

发送异步请求携带参数

后面我们使用 axios 发送请求时,如果要携带复杂的数据时都会以 JSON 格式进行传递

请求参数不可能由我们自己拼接字符串的,我们可以提前定义一个 js 对象,用来封装需要提交的参数,然后使用 JSON.stringify(js对象) 转换为 JSON 串,再将该 JSON 串作为 axiosdata 属性值进行请求参数的提交。如下:

1
2
3
4
5
6
7
8
9
10

var jsObject = {name:"张三"};

axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data: JSON.stringify(jsObject)
}).then(function (resp) {
alert(resp.data);
})

axios 是一个很强大的工具。我们只需要将需要提交的参数封装成 js 对象,并将该 js 对象作为 axiosdata 属性值进行,它会自动将 js 对象转换为 JSON 串进行提交。如下:

1
2
3
4
5
6
7
8
9
10

var jsObject = {name:"张三"};

axios({
method:"post",
url:"http://localhost:8080/ajax-demo/axiosServlet",
data:jsObject //这里 axios 会将该js对象转换为 json 串的
}).then(function (resp) {
alert(resp.data);
})

注意:
js 提供的 JSON 对象我们只需要了解一下即可。因为 axios 会自动对 js 对象和 JSON 串进行转换。
发送异步请求时,如果请求参数是 JSON 格式,那请求方式必须是 POST。因为 JSON 串需要放在请求体中。

JSON串与Java对象的相互转换

学习完 json 后,接下来聊聊 json 的作用。以后我们会以 json 格式的数据进行前后端交互。前端发送请求时,如果是复杂的数据就会以 json 提交给后端;而后端如果需要响应一些复杂的数据时,也需要以 json 格式将数据响应回给浏览器。

在后端我们就需要重点学习以下两部分操作:

  • 请求数据:JSON字符串转为Java对象
  • 响应数据:Java对象转为JSON字符串

阿里提供的一套 API Fastjson,可以帮我们实现上面两部分操作。

Fastjson概述

Fastjson 是阿里巴巴提供的一个Java语言编写的高性能功能完善的 JSON 库,是目前Java语言中最快的 JSON 库,可以实现 Java 对象和 JSON 字符串的相互转换。

Fastjson使用

Fastjson 使用也是比较简单的,分为以下三步完成

  1. 导入坐标

    1
    2
    3
    4
    5
    6

    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
    </dependency>
  2. Java对象转JSON

    1
    2

    String jsonStr = JSON.toJSONString(obj);

    将 Java 对象转换为 JSON 串,只需要使用

1
Fastjson

提供的

1
JSON

类中的

1
toJSONString()

静态方法即可。

  1. JSON字符串转Java对象

    1
    2

    User user = JSON.parseObject(jsonStr, User.class);

    将 json 转换为 Java 对象,只需要使用

1
Fastjson

提供的

1
JSON

类中的

1
parseObject()

静态方法即可。

代码演示

  • 先创建一个User类

    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

    package com.blog.web.pojo;

    public class User {
    private Integer id;
    private String username;
    private String password;

    public User() {
    }

    public User(Integer id, String username, String password) {
    this.id = id;
    this.username = username;
    this.password = password;
    }

    public Integer getId() {
    return id;
    }

    public void setId(Integer id) {
    this.id = id;
    }

    public String getUsername() {
    return username;
    }

    public void setUsername(String username) {
    this.username = username;
    }

    public String getPassword() {
    return password;
    }

    public void setPassword(String password) {
    this.password = password;
    }

    @Override
    public String toString() {
    return "User{" +
    "id=" + id +
    ", username='" + username + '\'' +
    ", password='" + password + '\'' +
    '}';
    }
    }
  • 测试方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    import com.alibaba.fastjson.JSON;
    import com.blog.web.pojo.User;

    public class FastjsonDemo {
    public static void main(String[] args) {
    //1. 将Java对象转为JSON字符串
    User user = new User();
    user.setId(1);
    user.setUsername("zhangsan");
    user.setPassword("asd123");
    String jsonString = JSON.toJSONString(user);
    System.out.println(jsonString);
    //2. 将JSON字符串转为Java对象
    User u = JSON.parseObject("{\"id\":1,\"password\":\"asd123\",\"username\":\"zhangsan\"}", User.class);
    System.out.println(u);
    }
    }
  • 得到输出结果如下

    {“id”:1,”password”:”asd123”,”username”:”zhangsan”}
    User{id=1, username=’zhangsan’, password=’asd123’}

案例

需求

使用Axios + JSON 完成品牌列表数据查询和添加。

查询所有功能

前后端需以 JSON 格式进行数据的传递;由于此功能是查询所有的功能,前端发送 ajax 请求不需要携带参数,而后端响应数据需为 json 数据

环境准备

依旧是使用我们之前的brand-demo项目,将brand.jsp页面复制一份为brand.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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>${user.username},欢迎你</h1>

<input type="button" value="新增" id="add"><br>
<hr>
<table border="1" cellspacing="0" width="1200">
<tr>
<th>序号</th>
<th>品牌名称</th>
<th>企业名称</th>
<th>排序</th>
<th>品牌介绍</th>
<th>状态</th>
<th>操作</th>
</tr>
<c:forEach items="${brands}" var="brand">
<tr align="center">
<td>${brand.id}</td>
<td>${brand.brandName}</td>
<td>${brand.companyName}</td>
<td>${brand.ordered}</td>
<td>${brand.description}</td>
<td>${brand.status == 1 ? "启用" : "禁用"}</td>
<td><a href="/brand_demo/selectByIdServlet?id=${brand.id}">修改</a> <a href="/brand_demo/deleteServlet?id=${brand.id}">删除</a></td>
</tr>
</c:forEach>
</table>
<script>
document.getElementById("add").onclick = function (){
location.href = "/brand_demo/addBrand.jsp";
}
</script>
</body>
</html>

后端实现

修改 com.itheima.web 包下的 SelectAllServlet,具体的逻辑如下:

  • 调用 service 的 selectAll() 方法进行查询所有的逻辑处理
  • 将查询到的集合数据转换为 json 数据。我们将此过程称为序列化;如果是将 json 数据转换为 Java 对象,我们称之为反序列化
  • 将 json 数据响应回给浏览器。这里一定要设置响应数据的类型及字符集 response.setContentType("text/json;charset=utf-8");

SelectAllServlet 代码如下:

  • 修改前
  • 修改后

之前我们是将获取到的brands对象存入request域中,然后根据JSTL和EL表达式来进行遍历和取值

1
2
3
4
5
6
7

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Brand> brands = brandService.selectAll();
request.setAttribute("brands", brands);
request.getRequestDispatcher("/brand.jsp").forward(request, response);
}

前端实现

  • brand.html 页面引入 axios 的 js 文件,我这就先不用本地的js了

    1
    2

    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  • 绑定页面加载完毕事件
    brand.html 页面绑定加载完毕事件,该事件是在页面加载完毕后被触发,代码如下

    1
    2
    3
    4

    window.onload = function() {

    }
  • 在页面加载完毕事件绑定的匿名函数中发送异步请求,代码如下:

    1
    2
    3
    4
    5
    6
    7

    axios({
    method:"get",
    url:"http://localhost:8080/brand-demo/selectAllServlet"
    }).then(function (resp) {

    });
  • 处理响应数据

  • then 中的回调函数中通过 resp.data 可以获取响应回来的数据.

  • 现在我们需要拼接字符串,将表格中的所有的 tr 拼接到一个字符串中,然后使用 document.getElementById("brandTable").innerHTML = 拼接好的字符串 就可以动态的展示出用户想看到的数据

  • 而表头行是固定的,所以先定义初始值是表头行数据的字符串,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    let tableData = " <tr>\n" +
    " <th>序号</th>\n" +
    " <th>品牌名称</th>\n" +
    " <th>企业名称</th>\n" +
    " <th>排序</th>\n" +
    " <th>品牌介绍</th>\n" +
    " <th>状态</th>\n" +
    " <th>操作</th>\n" +
    " </tr>";
  • 接下来获取并遍历响应回来的数据

1
brands

,拿到每一条品牌数据

1
2
3
4
5
6

let brands = resp.data;
for (let i = 0; i < brands.length ; i++) {
let brand = brands[i];

}
  • 紧接着就是从 brand 对象中获取数据并且拼接 数据行,累加到 tableData 字符串变量中

    • 之前的JSP写法
    • 现在的HTML写法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    <c:forEach items="${brands}" var="brand">
    <tr align="center">
    <td>${brand.id}</td>
    <td>${brand.brandName}</td>
    <td>${brand.companyName}</td>
    <td>${brand.ordered}</td>
    <td>${brand.description}</td>
    <td>${brand.status == 1 ? "启用" : "禁用"}</td>
    <td><a href="/brand_demo/selectByIdServlet?id=${brand.id}">修改</a> <a href="/brand_demo/deleteServlet?id=${brand.id}">删除</a></td>
    </tr>
    </c:forEach>
  • 最后将拼接好的字符串写到表格中就大功告成了,整体的代码如下,我们也就完成了上面说的用HTML + AJAX替换JSP的操作

    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

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <table border="1" cellspacing="0" width="1200" id="brandTable">


    </table>

    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
    window.onload = function () {
    axios({
    method: "get",
    url: "http://localhost:8080/brand_demo/selectAllServlet"
    }).then(function (resp) {
    let tableData = " <tr>\n" +
    " <th>序号</th>\n" +
    " <th>品牌名称</th>\n" +
    " <th>企业名称</th>\n" +
    " <th>排序</th>\n" +
    " <th>品牌介绍</th>\n" +
    " <th>状态</th>\n" +
    " <th>操作</th>\n" +
    " </tr>";
    let brands = resp.data;
    for (let i = 0; i < brands.length; i++) {
    let brand = brands[i];
    tableData += "<tr align=\"center\">\n" +
    " <td>" + (i + 1) + "</td>\n" +
    " <td>" + brand.brandName + "</td>\n" +
    " <td>" + brand.companyName + "</td>\n" +
    " <td>" + brand.ordered + "</td>\n" +
    " <td>" + brand.description + "</td>\n" +
    " <td>" + (brand.status == 1 ? "启用" : "禁用") + "</td>\n" +
    "<td><a href=\"/brand_demo/selectByIdServlet?id=" + brand.id + "\">修改</a> <a href=\"/brand_demo/deleteServlet?id=" + brand.id + "\">删除</a></td>" +
    " </tr>"
    }
    document.getElementById("brandTable").innerHTML = tableData;
    })
    }
    </script>
    </body>
    </html>

添加品牌功能

当我们点击 新增 按钮,会跳转到 addBrand.html 页面。在 addBrand.html 页面输入数据后点击 提交 按钮,就会将数据提交到后端,而后端将数据保存到数据库中。
说明:前端需要将用户输入的数据提交到后端,这部分数据需要以 json 格式进行提交

后端实现

修改 com.itheima.web 包下的 AddServlet,具体的逻辑如下:

  • 获取请求参数

    • 由于前端提交的是 json 格式的数据,所以我们不能使用
1
request.getParameter()
方法获取请求参数 - 如果提交的数据格式是 `username=zhangsan&age=23` ,后端就可以使用 `request.getParameter()` 方法获取 - 如果提交的数据格式是 json,后端就需要通过 request.getReader() 来获取输入流,然后通过输入流读取数据
  • 将获取到的请求参数(json格式的数据)转换为 Brand 对象

  • 调用 service 的 add() 方法进行添加数据的逻辑处理

  • 将 json 数据响应回给浏览器。

AddServlet 代码如下:

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

@WebServlet("/addServlet")
public class AddServlet extends HttpServlet {

private BrandService brandService = new BrandService();

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

//1. 接收数据,request.getParameter 不能接收json的数据
/* String brandName = request.getParameter("brandName");
System.out.println(brandName);*/

// 获取请求体数据
BufferedReader br = request.getReader();
String params = br.readLine();
// 将JSON字符串转为Java对象
Brand brand = JSON.parseObject(params, Brand.class);
//2. 调用service 添加
brandService.add(brand);
//3. 响应成功标识
response.getWriter().write("success");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}

前端实现

  • 还是先复制一份addBrand.jsp的代码,然后进行修改
    将action路径删掉,然后将提交按钮的类型改为普通的button

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    <head>
    <meta charset="UTF-8">
    <title>添加品牌</title>
    </head>
    <body>
    <h3>添加品牌</h3>
    <form action="" method="post">
    品牌名称:<input id="brandName" name="brandName"><br>
    企业名称:<input id="companyName" name="companyName"><br>
    排序:<input id="ordered" name="ordered"><br>
    描述信息:<textarea rows="5" cols="20" id="description" name="description"></textarea><br>
    状态:
    <input type="radio" name="status" value="0">禁用
    <input type="radio" name="status" value="1">启用<br>

    <input type="button" id="btn" value="提交">
    </form>
  • 然后给提交按钮绑定点击事件,并在绑定的匿名函数中发送异步请求,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    //1. 给按钮绑定单击事件
    document.getElementById("btn").onclick = function () {
    //2. 发送ajax请求
    axios({
    method:"post",
    url:"http://localhost:8080/brand-demo/addServlet",
    data:???
    }).then(function (resp) {
    // 判断响应数据是否为 success,如果是,则跳转到查询所有页面
    if(resp.data == "success"){
    location.href = "http://localhost:8080/brand-demo/brand.html";
    }
    })
    }
  • 现在我们只需要考虑如何获取页面上用户输入的数据即可。
    首先我们先定义如下的一个 js 对象,该对象是用来封装页面上输入的数据,并将该对象作为上面发送异步请求时 data 属性的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9

    // 将表单数据转为json
    var formData = {
    brandName:"",
    companyName:"",
    ordered:"",
    description:"",
    status:"",
    };
  • 接下来获取输入框输入的数据,并将获取到的数据赋值给 formData 对象指定的属性。比如获取用户名的输入框数据,并把该数据赋值给 formData 对象的 brandName 属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    // 获取表单数据
    let brandName = document.getElementById("brandName").value;
    // 设置数据
    formData.brandName = brandName;

    // 获取表单数据
    let companyName = document.getElementById("companyName").value;
    // 设置数据
    formData.companyName = companyName;

    // 获取表单数据
    let ordered = document.getElementById("ordered").value;
    // 设置数据
    formData.ordered = ordered;

    // 获取表单数据
    let description = document.getElementById("description").value;
    // 设置数据
    formData.description = description;
  • 其中状态数据比较特殊,我们这里来进行特殊处理
    根据name来获取一个status数组,然后进行遍历,status有一个checked的属性,选中了则是true,我们通过它来判断到底选了谁,然后将它的值赋给formData的对应属性

    1
    2
    3
    4
    5
    6
    7

    let status = document.getElementsByName("status");
    for (let i = 0; i < status.length; i++) {
    if(status[i].checked){
    formData.status = status[i].value ;
    }
    }
  • 整体代码如下,至此我们就将addBrand.jsp页面修改为了HTML + AJAX的形式

    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

    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <title>添加品牌</title>
    </head>
    <body>
    <h3>添加品牌</h3>
    <form action="" method="post">
    品牌名称:<input id="brandName" name="brandName"><br>
    企业名称:<input id="companyName" name="companyName"><br>
    排序:<input id="ordered" name="ordered"><br>
    描述信息:<textarea rows="5" cols="20" id="description" name="description"></textarea><br>
    状态:
    <input type="radio" name="status" value="0">禁用
    <input type="radio" name="status" value="1">启用<br>

    <input type="button" id="btn" value="提交">
    </form>

    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

    <script>
    //1. 给按钮绑定单击事件
    document.getElementById("btn").onclick = function () {
    // 将表单数据转为json
    var formData = {
    brandName:"",
    companyName:"",
    ordered:"",
    description:"",
    status:"",
    };
    // 获取表单数据
    let brandName = document.getElementById("brandName").value;
    // 设置数据
    formData.brandName = brandName;

    // 获取表单数据
    let companyName = document.getElementById("companyName").value;
    // 设置数据
    formData.companyName = companyName;

    // 获取表单数据
    let ordered = document.getElementById("ordered").value;
    // 设置数据
    formData.ordered = ordered;

    // 获取表单数据
    let description = document.getElementById("description").value;
    // 设置数据
    formData.description = description;

    let status = document.getElementsByName("status");
    for (let i = 0; i < status.length; i++) {
    if(status[i].checked){
    //
    formData.status = status[i].value ;
    }
    }
    //console.log(formData);
    //2. 发送ajax请求
    axios({
    method:"post",
    url:"http://localhost:8080/brand-demo/addServlet",
    data:formData
    }).then(function (resp) {
    // 判断响应数据是否为 success
    if(resp.data == "success"){
    location.href = "http://localhost:8080/brand-demo/brand.html";
    }
    })
    }
    </script>
    </body>
    </html>

查询所有 功能和 添加品牌 功能就全部实现,现在感觉前端的代码很复杂,但这只是暂时的,下篇文章学习了 vue 前端框架后,这部分前端代码就可以进行很大程度的简化。

本文摘要:

  • 能够使用Vue中常用指令和插值表达式
  • 能够使用Vue生命周期函数 mounted
  • 能够进行简单的 Element 页面修改
  • 能够完成查询所有功能
  • 能够完成添加功能

Vue

概述

Vue是一套前端框架,用于简化JavaScript中的DOM操作,简化书写。

在之前我们也学习过后端的框架MyBatis,MyBatis是用来简化JDBC代码编写的;而Vue是前端框架,简化JavaScript代码编写的。

在上篇文章中,我们做了一个综合性的案例,里面进行了大量的DOM操作,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// 获取表单数据
let brandName = document.getElementById("brandName").value;
// 设置数据
formData.brandName = brandName;

// 获取表单数据
let companyName = document.getElementById("companyName").value;
// 设置数据
formData.companyName = companyName;

// 获取表单数据
let ordered = document.getElementById("ordered").value;
// 设置数据
formData.ordered = ordered;

// 获取表单数据
let description = document.getElementById("description").value;
// 设置数据
formData.description = description;

学习了Vue之后,这部分代码我们就不需要再写了,那么Vue是如何简化DOM书写呢?

基于MVVM(Model-View-ViewModel)思想,实现数据的双向绑定,将变成的关注点放在数据上。之前我们是将关注点放在了DOM操作上;而要了解MVVM思想,就得先聊聊MVC思想,下面是MVC思想的图解

MVC思想.png

M 就是数据,而 V 是页面上展示的内容,C是我们的JS代码,如下是我们之前写的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

let brands = resp.data;
for (let i = 0; i < brands.length; i++) {
let brand = brands[i]; //这行就是我们的数据

//下面这些就是到时候要展示到浏览器上的视图
tableData += "<tr align=\"center\">\n" +
" <td>" + (i + 1) + "</td>\n" +
" <td>" + brand.brandName + "</td>\n" +
" <td>" + brand.companyName + "</td>\n" +
" <td>" + brand.ordered + "</td>\n" +
" <td>" + brand.description + "</td>\n" +
" <td>" + (brand.status == 1 ? "启用" : "禁用") + "</td>\n" +
"<td><a href=\"/brand_demo/selectByIdServlet?id=" + brand.id + "\">修改</a> <a href=\"/brand_demo/deleteServlet?id=" + brand.id + "\">删除</a></td>" +
" </tr>"
}

MVC 思想是没法进行双向绑定的。双向绑定是指当数据模型数据发生变化时,页面展示的会随之发生变化,而如果表单数据发生变化,绑定的模型数据也随之发生变化。接下来我们聊聊 MVVM 思想,如下图是三个组件图解

MVVM.png

图中的 Model 就是我们的数据,View 是视图,也就是页面标签,用户可以通过浏览器看到的内容;ModelView 是通过 ViewModel 对象进行双向绑定的,而 ViewModel 对象是 Vue 提供的。接下来让我们看看双向绑定的效果,输入框绑定了 username 模型数据,而在页面上也使用插值表达式绑定了 username 模型数据

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input v-model="username">
<!--插值表达式-->
{{username}}
</div>

<script src="js/vue.js"></script>
<!--创建Vue核心对象-->
<script>
new Vue({
el: "#app",
data() {
return {
username: ""
}
}
})
</script>
</body>
</html>

当我们访问该页面时,在输入框中输入内容,而输入框后面随之实时的展示我们输入的内容,这就是双向绑定的效果。

快速入门

Vue 使用起来是比较简单的,总共分为如下三步:

  1. 新建 HTML 页面,引入 Vue.js文件

    1
    2

    <script src="js/vue.js"></script>
  2. 在JS代码区域,创建Vue核心对象,进行数据绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9

    new Vue({
    el: "#app",
    data() {
    return {
    username: ""
    }
    }
    });

    创建 Vue 对象时,需要传递一个 js 对象,而该对象中需要如下属性:

    • el : 用来指定哪儿些标签受 Vue 管理。 该属性取值 #app 中的 app 需要是受管理的标签的id属性值
    • data :用来定义数据模型
    • methods :用来定义函数。这个我们在后面就会用到
  3. 编写视图

    1
    2
    3
    4
    5

    <div id="app">
    <input name="username" v-model="username" >
    {{username}}
    </div>
    1
    {{}}

是 Vue 中定义的

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input v-model="username">
<!--插值表达式-->
{{username}}
</div>
<script src="js/vue.js"></script>
<script>
//1. 创建Vue核心对象
new Vue({
el:"#app",
data(){ // data() 是 ECMAScript 6 版本的新的写法
return {
username:""
}
}

/*data: function () {
return {
username:""
}
}*/
});

</script>
</body>
</html>

Vue 指令

指令:HTML 标签上带有 v- 前缀的特殊属性,不同指令具有不同含义。例如:v-ifv-for..

常用的指令有

指令 作用
v-bind 为HTML标签绑定属性值,如设置 href , css样式等
v-model 在表单元素上创建双向数据绑定
v-on 为HTML标签绑定事件
v-if 条件性的渲染某元素,判定为true时渲染,否则不渲染
v-else 条件性的渲染某元素,判定为true时渲染,否则不渲染
v-else-if 条件性的渲染某元素,判定为true时渲染,否则不渲染
v-show 根据条件展示某元素,区别在于切换的是display属性的值
v-for 列表渲染,遍历容器的元素或者对象的属性

v-bind&v-model指令

  • v-bind

    该指令可以给标签原有属性绑定模型数据。这样模型数据发生变化,标签属性值也随之发生变化

    例如:

    1
    2

    <a v-bind:href="url">百度一下</a>

    上面的 v-bind:" 可以简化写成 : ,如下:

    1
    2
    3
    4
    5

    <!--
    v-bind 可以省略
    -->
    <a :href="url">百度一下</a>
  • v-model

    该指令可以给表单项标签绑定模型数据。这样就能实现双向绑定效果。例如:

    1
    2

    <input name="username" v-model="username">

代码演示:

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input v-model="url"> <!--给表单项绑定模型数据-->
<a :href="url">`链接`</a> <!--点击链接,可以根据我们输入的url来访问不同的网站-->
</div>

<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
username: "",
url:""
}
}
})
</script>
</body>
</html>

v-on指令

我们在页面定义一个按钮,并给该按钮使用 v-on 指令绑定单击事件,html代码如下

1
2

<input type="button" value="一个按钮" v-on:click="show()">

而使用 v-on 时还可以使用简化的写法,将 v-on: 替换成 @,html代码如下

1
2

<input type="button" value="一个按钮" @click="show()">

上面代码绑定的 show() 需要在 Vue 对象中的 methods 属性中定义出来

1
2
3
4
5
6
7
8
9

new Vue({
el: "#app",
methods: {
show() {
alert("我被点了")
}
}
})
  • 注意:

    1
    v-on:

后面的事件名称是之前原生事件属性名去掉on。

  • 单击事件 : 事件属性名是 onclick,而在vue中使用是 v-on:click
  • 失去焦点事件:事件属性名是 onblur,而在vue中使用时 v-on:blur

整体页面代码如下:

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="button" value="按钮" v-on:click="show()">
</div>

<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
methods: {
show() {
alert("我被点了")
}
}
})
</script>
</body>
</html>

条件判断指令

在 Vue中定义一个 count 的数据模型,如下

1
2
3
4
5
6
7
8
9

new Vue({
el: "#app",
data() {
return {
count: ""
}
}
})

现在要实现,当 count 模型的数据是1时,在页面上展示 div1 内容;当 count 模型的数据是2时,在页面上展示 div2 内容;count 模型数据是其他值时,在页面上展示 div3。这里为了动态改变模型数据 count 的值,再定义一个输入框绑定 count 模型数据。html 代码如下:

1
2
3
4
5
6
7

<div id="app">
<div v-if="count==1">div1</div>
<div v-else-if="count==2">div2</div>
<div v-else>div3</div>
<input v-model="count">
</div>

整体代码如下

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<div v-if="count==1">div1</div>
<div v-else-if="count==2">div2</div>
<div v-else>div3</div>
<input v-model="count">
</div>

<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
count: ""
}
}
})
</script>
</body>
</html>

通过浏览器打开页面并在输入框输入不同的值,会显示不同的div

v-showv-if 效果一样,但实现原理不一样,通过查看源码发现,v-show 不展示的原理是给对应的标签添加 display css属性,并将该属性值设置为 none,而v-if是直接没有div标签,只有符合条件的时候,源码中才会出现对应的div标签

1
2

<div style="display: none;">div4</div>

v-for指令

这个指令看到名字就知道是用来遍历的,该指令使用的格式如下:

1
2
3
4

<标签 v-for="变量名 in 集合模型数据">
{{变量名}}
</标签>

注意:需要循环哪个标签,v-for 指令就写在哪个标签上。

如果在页面需要使用到集合模型数据的索引,就需要使用如下格式:

1
2
3
4
5

<标签 v-for="(变量名,索引变量) in 集合模型数据">
<!--索引变量是从0开始,所以要表示序号的话,需要手动的加1-->
{{索引变量 + 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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<div v-for="addr in addrs">
{{addr}}<br>
</div>

<div v-for="(addr,i) in addrs">
{{i + 1}}--{{addr}}<br>
</div>
</div>

<script src="js/vue.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
addrs: ["北京", "上海", "成都"]
}
}
})
</script>
</body>
</html>

浏览器上展示的内容

北京
上海
成都
1—北京
2—上海
3—成都

生命周期

生命周期的八个阶段:每触发一个生命周期事件,会自动执行一个生命周期方法,这些生命周期方法也被称为钩子方法。

状态 阶段周期
beforeCreate 创建前
created 创建后
beforeMount 载入前
mounted 挂载完成
beforeUpdate 更新前
updated 更新后
beforeDestroy 销毁前
destroyed 销毁后

下图是 Vue 官网提供的从创建 Vue 到效果 Vue 对象的整个过程及各个阶段对应的钩子函数
vue生命周期
看到上面的图,大家无需过多的关注这张图。这些钩子方法我们只关注 mounted 就行了。

1
mounted`:挂载完成,Vue初始化成功,HTML页面渲染成功。而以后我们会在该方法中`发送异步请求,加载数据。

案例

需求

使用 Vue 简化我们在上一篇文章学完Ajax后做的品牌列表数据查询和添加功能
此案例只是使用 Vue 对前端代码进行优化,后端代码无需修改。

查询所有功能

  • 在 brand.html 页面引入 vue和Axios 的js文件

    1
    2
    3

    <script src="js/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  • 创建 Vue 对象

    • 在 Vue 对象中定义模型数据

    • 在mounted函数中发送异步请求(也就是页面加载完成之后),并将响应的数据赋值给数据模型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21

      new Vue({
      el: "#app",
      data() {
      return {
      brands: []
      }
      },
      mounted() {
      /*_this就是个中介,因为在axios中不能直接获取到vue中的brands,
      所以需要在mounted内,axios外,用_this这个中介来将数据赋给brands
      */
      var _this = this;
      axios({
      method: "get",
      url: "http://localhost:8080/brand_demo/selectAllServlet"
      }).then(function (resp) {
      _this.brands = resp.data;
      })
      }
      })
  • 修改视图

    • 定义 <div id="app"></div> ,指定该 div 标签受 Vue 管理

    • 在表格中的数据行上使用

1
v-for
指令遍历
1
2
3
4
5
6
7
8
9
10
11

<tr v-for="(brand,i) in brands" align="center">
<td>{{i + 1}}</td>
<td>{{brand.brandName}}</td>
<td>{{brand.companyName}}</td>
<td>{{brand.ordered}}</td>
<td>{{brand.description}}</td>
<td>{{brand.status == 1 ? "启用" : "禁用"}}</td>
<td><a href="#">修改</a>
<a href="#">删除</a></td>
</tr>
  • 整体代码如下

    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

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <h1>欢迎你</h1>

    <input type="button" value="新增" id="add"><br>
    <hr>
    <div id="app">
    <table border="1" cellspacing="0" width="1200">
    <tr>
    <th>序号</th>
    <th>品牌名称</th>
    <th>企业名称</th>
    <th>排序</th>
    <th>品牌介绍</th>
    <th>状态</th>
    <th>操作</th>
    </tr>

    <tr v-for="(brand,i) in brands" align="center">
    <td>{{i + 1}}</td>
    <td>{{brand.brandName}}</td>
    <td>{{brand.companyName}}</td>
    <td>{{brand.ordered}}</td>
    <td>{{brand.description}}</td>
    <td>{{brand.status == 1 ? "启用" : "禁用"}}</td>
    <td><a href="#">修改</a>
    <a href="#">删除</a></td>
    </tr>

    </table>
    </div>
    <script src="js/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
    new Vue({
    el: "#app",
    data() {
    return {
    brands: []
    }
    },
    mounted() {
    var _this = this;
    axios({
    method: "get",
    url: "http://localhost:8080/brand_demo/selectAllServlet"
    }).then(function (resp) {
    _this.brands = resp.data;
    })
    }
    })
    </script>
    </body>
    </html>

添加功能

  • 在 vueAddBrand.html 页面引入 vue和axios 的js文件

    1
    2
    3

    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="js/vue.js"></script>
  • 创建 Vue 对象

    • 在 Vue 对象中定义模型数据 brand

    • 定义一个 submitForm() 函数,用于给 提交 按钮提供绑定的函数

1
submitForm()
函数中发送 ajax 请求,并将模型数据
1
brand
作为参数进行传递
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

new Vue({
el: "#app",
data(){
return {
brand:{}
}
},
methods:{
submitForm(){
// 发送ajax请求,添加
var _this = this;
axios({
method:"post",
url:"http://localhost:8080/brand_demo/addServlet",
data:_this.brand
}).then(function (resp) {
// 判断响应数据是否为 success
if(resp.data == "success"){
location.href = "http://localhost:8080/brand_demo/vueBrand.html";
}
})
}
}
})
  • 修改视图

    • 定义 <div id="app"></div> ,指定该 div 标签受 Vue 管理

    • 将表单的所有的内容复制到 div 标签中

    • 给每一个表单项标签绑定模型数据。最后这些数据要被封装到

1
brand
对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

<div id="app">
<h3>添加品牌</h3>
<form action="" method="post">
品牌名称:<input id="brandName" v-model="brand.brandName" name="brandName"><br>
企业名称:<input id="companyName" v-model="brand.companyName" name="companyName"><br>
排序:<input id="ordered" v-model="brand.ordered" name="ordered"><br>
描述信息:<textarea rows="5" cols="20" id="description" v-model="brand.description" name="description"></textarea><br>
状态:
<input type="radio" name="status" v-model="brand.status" value="0">禁用
<input type="radio" name="status" v-model="brand.status" value="1">启用<br>

<input type="button" id="btn" @click="submitForm" value="提交">
</form>
</div>
  • 整体页面代码如下:

    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

    <!DOCTYPE html>
    <html lang="en">

    <head>
    <meta charset="UTF-8">
    <title>添加品牌</title>
    </head>
    <body>
    <div id="app">
    <h3>添加品牌</h3>
    <form action="" method="post">
    品牌名称:<input id="brandName" v-model="brand.brandName" name="brandName"><br>
    企业名称:<input id="companyName" v-model="brand.companyName" name="companyName"><br>
    排序:<input id="ordered" v-model="brand.ordered" name="ordered"><br>
    描述信息:<textarea rows="5" cols="20" id="description" v-model="brand.description" name="description"></textarea><br>
    状态:
    <input type="radio" name="status" v-model="brand.status" value="0">禁用
    <input type="radio" name="status" v-model="brand.status" value="1">启用<br>

    <input type="button" id="btn" @click="submitForm" value="提交">
    </form>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="js/vue.js"></script>
    <script>
    new Vue({
    el: "#app",
    data(){
    return {
    brand:{}
    }
    },
    methods:{
    submitForm(){
    // 发送ajax请求,添加
    var _this = this;
    axios({
    method:"post",
    url:"http://localhost:8080/brand_demo/addServlet",
    data:_this.brand
    }).then(function (resp) {
    // 判断响应数据是否为 success
    if(resp.data == "success"){
    location.href = "http://localhost:8080/brand_demo/vueBrand.html";
    }
    })
    }
    }
    })
    </script>
    </body>
    </html>

Element

  • Element:是饿了么公司前端开发团队提供的一套基于 Vue 的网站组件库,用于快速构建网页。
  • Element 提供了很多组件(组成网页的部件)供我们使用。例如 超链接、按钮、图片、表格等等~
  • 我们学习 Element 其实就是学习怎么从官网拷贝组件到我们自己的页面并进行修改,官网网址是 https://element.eleme.cn/#/zh-CN

快速入门

  1. 引入Element 的css、js文件 和 Vue.js

  2. 创建Vue核心对象,Element是基于Vue的,所以使用Element时必须创建Vue对象

    1
    2
    3
    4
    5
    6

    <script>
    new Vue({
    el: "#app"
    })
    </script>
  3. 官网复制Element组件代码,然后随便改改就变成自己的了,本文就不过多赘述了,下篇文章会有一个具体的示例。