Java Web

tomcat文件夹介绍

bin:存放各个平台下启动和停⽌ Tomcat 服务的脚本⽂件。
conf:存放各种 Tomcat 服务器的配置⽂件。
lib:存放 Tomcat 服务器所需要的 jar。
logs:存放 Tomcat 服务运⾏的⽇志。
temp:Tomcat 运⾏时的临时⽂件(文件上传和下载会用到。当上传大文件时,如果是存到内存中显然是不合理的,temp就是用来存放这些临时文件的)。
webapps:存放允许客户端访问的资源(Java 程序)。
work:存放 Tomcat 将 JSP 转换之后的 Servlet ⽂件。

IDEA集成tomcat

  • 新建普通java项目(注意:不是maven项目)

  • 新建好的项目只有src,对于java web应用需要的文件,我们可以这么来新建
    在这里插入图片描述

选择【Help】【Find Action】,在新弹窗里输入 add framework support,点击,然后选择webapplication。我这里已经选择过了,不让选了。点击后就会生成web目录。
在这里插入图片描述

  • 配置tomcat。编辑配置,找到tomcat server,选择local。
    在这里插入图片描述

接着给tomcat重命名。
在这里插入图片描述
接下来找到本地配置的tomcat。点击configure。
在这里插入图片描述
选择到bin的上一级目录。选择之后,会自动检测出来版本,点击确定。
在这里插入图片描述
在这里插入图片描述
这样tomcat9.0.83就出现了。
在这里插入图片描述

  • 接下来是选择默认浏览器,这里选择谷歌。==这里还有一个After launch,选中的话,启动项目后不需要我们手动启动浏览器,他会为我们自动打开。==后面的url就是访问地址。
    在这里插入图片描述
  • 把我们本地的工程添加进来。选择Deployment,选择加号。选择artifact,就会自动加进来。最后点击apply、ok。
    在这里插入图片描述
    在这里插入图片描述
  • 这样就可以点击运行开启动应用了,启动起来我们发现,应用名特别长,可以这样修改。选择deployment,修改下面的Application context。
    在这里插入图片描述
    在这里插入图片描述

servlet

  • 什么是 Servlet?
    Servlet 是 Java Web 开发的基⽯,与平台⽆关的服务器组件,它是运⾏在 Servlet 容器/Web 应⽤服务器/Tomcat,负责与客户端进⾏通信。

  • Servlet 的功能:
    1、创建并返回基于客户请求的动态 HTML ⻚⾯。
    2、与数据库进⾏通信。

  • 如何使⽤ Servlet?
    Servlet 本身是⼀组接⼝,⾃定义⼀个类,并且实现 Servlet 接⼝,这个类就具备了接受客户端请求以及做出响应的功能。

package com.southwind.servlet;

import javax.servlet.*;
import java.io.IOException;

public class MyServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String id  = servletRequest.getParameter("id");
        System.out.println("我已经收到信息了,id是:"+id);
        servletResponse.setContentType("text/html;charset=UTF-8");
        servletResponse.getWriter().write("你好,客户端");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

浏览器不能直接访问 Servlet ⽂件,只能通过映射的⽅式来间接访问 Servlet,映射需要开发者⼿动配置,有两种配置⽅式。

  • 基于 XML ⽂件的配置⽅式,在web.xml里面添加映射。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>MyServlet</servlet-name>
        <servlet-class>com.southwind.servlet.MyServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>MyServlet</servlet-name>
        <url-pattern>/myservlet</url-pattern>
    </servlet-mapping>
</web-app>
  • 基于注解的⽅式。直接在类上面添加注解,括号里面是访问servlet的地址。
@WebServlet("/myservlet")
public class MyServlet implements Servlet {
    
}

servlet的生命周期

package com.southwind.servlet;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/myservlet")
public class MyServlet implements Servlet {
    public MyServlet(){
        System.out.println("created");
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("init");
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
//        String id  = servletRequest.getParameter("id");
//        System.out.println("我已经收到信息了,id是:"+id);
//        servletResponse.setContentType("text/html;charset=UTF-8");
//        servletResponse.getWriter().write("你好,客户端");
        System.out.println("service");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
        System.out.println("destory");
    }
}
--输出结果:
created
init
service
destory

1、当浏览器访问 Servlet 的时候,Tomcat 会查询当前 Servlet 的实例化对象是否存在,如果不存在,则通过反射机制动态创建对象(调用构造方法),如果存在,直接执⾏第 3 步。
2、调⽤ init ⽅法完成初始化操作。
3、调⽤ service ⽅法完成业务逻辑操作。
4、关闭 Tomcat 时,会调⽤ destory ⽅法,释放当前对象所占⽤的资源。

ServletConfig

该接⼝是⽤来描述 Servlet 的基本信息的。
getServletName() 返回 Servlet 的名称,全类名(带着包名的类名)
getInitParameter(String key) 获取 init 参数的值(web.xml)
getInitParameterNames() 返回所有的 initParamter 的 name 值,⼀般⽤作遍历初始化参数
getServletContext() 返回 ServletContext 对象,它是 Servlet 的上下⽂,整个 Servlet 的管理者。

@Override
public void init(ServletConfig servletConfig) throws ServletException {
	System.out.println(servletConfig.getServletName());
	System.out.println(servletConfig.getInitParameter("username"));
	Enumeration<String> enumeration  =   servletConfig.getInitParameterNames();
	while(enumeration.hasMoreElements()){
		String ele = enumeration.nextElement();
		System.out.println("参数名:"+ele);
		System.out.println("参数值:"+servletConfig.getInitParameter(ele));
	}
}
System.out.println(servletContext.getContextPath());

web.xml配置

<servlet>
	<servlet-name>MyServlet</servlet-name>
	<servlet-class>com.southwind.servlet.MyServlet</servlet-class>
	<init-param>
		<param-name>username</param-name>
		<param-value>admin</param-value>
	</init-param>
	<init-param>
		<param-name>password</param-name>
		<param-value>123123</param-value>
	</init-param>
</servlet>

<servlet-mapping>
	<servlet-name>MyServlet</servlet-name>
	<url-pattern>/myservlet</url-pattern>
</servlet-mapping>

上述代码输出为:

com.southwind.servlet.MyServlet
admin
参数名:password
参数值:123123
参数名:username
参数值:admin
/test(这里的/test需要在tomcat的deployment的Application Context进行配置才可以)

ServletConfig 和 ServletContext 的区别:

ServletConfig 作⽤于某个 Servlet 实例,每个 Servlet 都有对应的 ServletConfig,ServletContext 作⽤于整个 Web 应⽤,⼀个 Web 应⽤对应⼀个 ServletContext,多个 Servlet 实例对应⼀个ServletContext。
⼀个是局部对象,⼀个是全局对象。

Servlet 的层次结构

Servlet —》(顶层)GenericServlet —〉(顶层)HttpServlet
HTTP 请求有很多种类型,常⽤的有四种:
GET 读取
POST 保存
PUT 修改
DELETE 删除
GenericServlet 实现 Servlet 接⼝,同时为它的子类屏蔽了不常⽤的⽅法,⼦类只需要重写 service ⽅法即可。
HttpServlet 继承 GenericServlet,根据请求类型进⾏分发处理,GET 进⼊ doGET ⽅法,POST 进⼊doPOST ⽅法。
开发者⾃定义的 Servlet 类只需要继承 HttpServlet 即可,重新 doGET 和 doPOST。

JSP

JSP 本质上就是⼀个 Servlet,JSP 主要负责与⽤户交互,将最终的界⾯呈现给⽤户,HTML+JS+CSS+Java 的混合⽂件。
当服务器接收到⼀个后缀是 jsp 的请求时,将该请求交给 JSP 引擎去处理,每⼀个 JSP ⻚⾯第⼀次被访问的时候,JSP 引擎会将它翻译成⼀个 Servlet ⽂件,再由 Web 容器调⽤ Servlet 完成响应。
单纯从开发的⻆度看,JSP 就是在 HTML 中嵌⼊ Java 程序。
具体的嵌⼊⽅式有 3 种:

1、JSP 脚本,执⾏ Java 逻辑代码

<% java代码 %>
<%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2023/11/20
  Time: 21:37
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
  hello
  <%
    String str = "hello world";
    System.out.println(str);
  %>
  </body>
</html>

访问对应jsp页面时,页面展示hello,hello world不会在页面展示,他会在后端的控制台输出。

2、JSP 声明:定义 Java ⽅法

<%!
声明java方法
%>
<%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2023/11/20
  Time: 21:37
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
  hello
  <%!
    public String test(){
      return "haha";
    }
  %>

  <%
    System.out.println(test());
  %>
  </body>
</html>

3、JSP 表达式:把 Java 对象直接输出到 HTML ⻚⾯中

<%=Java变量 %>

JSP内置对象9个

1、request:表示⼀次请求,来自HttpServletRequest类。
2、response:表示⼀次响应,来自HttpServletResponse类。
3、pageContext:⻚⾯上下⽂,获取⻚⾯信息,来自PageContext类。
4、session:表示⼀次会话,保存⽤户信息,来自HttpSession类。
5、application:表示当前 Web 应⽤,全局对象,保存所有⽤户共享信息,来自ServletContext类。
6、config:当前 JSP 对应的 Servlet 的 ServletConfig 对象,获取当前 Servlet 的信息。
7、out:向浏览器输出数据,来自JspWriter类。
8、page:当前 JSP 对应的 Servlet 对象,来自Servlet类。
9、exception:表示 JSP ⻚⾯发⽣的异常,来自Exception类。
常用的是 request、response、session、application、pageContext

request 常⽤⽅法:
1、String getParameter(String key) 获取客户端传来的参数。
2、void setAttribute(String key,Object value) 通过键值对的形式保存数据。(服务端内部传递数据)
3、Object getAttribute(String key) 通过 key 取出 value。(服务端内部传递数据)
4、RequestDispatcher getRequestDispatcher(String path) 返回⼀个 RequestDispatcher 对象,该对
象的 forward ⽅法⽤于请求转发。
5、String[] getParameterValues() 获取客户端传来的多个同名参数。
6、void setCharacterEncoding(String charset) 指定每个请求的编码。

<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.List" %><%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2023/11/20
  Time: 21:37
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
  hello
  <%
    String str = request.getParameter("id");
  %>
  <%=str%>
  </body>
</html>

上述代码,在浏览器地址栏输入?id=111,会展示出id的值。

index.jsp

<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.List" %><%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2023/11/20
  Time: 21:37
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
  hello
  <%
    String str = request.getParameter("id");
    Integer id = Integer.parseInt(str);
    id++;
    request.setAttribute("id", id);
    request.getRequestDispatcher("test2.jsp").forward(request, response);
  %>
  <%=id%>
  </body>
</html>

test2.jsp

<%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2023/11/24
  Time: 21:53
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<%
    Integer id = (Integer) request.getAttribute("id");
%>
<%=id%>
</body>
</html>

上述代码,当我们访问index.jsp?id=123,回车后会访问到test2.jsp。同时会将124打印出来。

request.setAttribute("id", id);在服务端设置id属性,方便相互调用。

request.getRequestDispatcher("test2.jsp").forward(request, response);request.getRequestDispatcher获取请求转发,括号里面是要请求的页面,这样还不能实现请求,还需要加上forward,里面加上request和response实现这个功能。

response 常⽤⽅法:
1、sendRedirect(String path) 重定向,⻚⾯之间的跳转。
转发 getRequestDispatcher .forward和重定向 sendRedirect 的区别:
转发是将同⼀个请求传给下⼀个⻚⾯,重定向是创建⼀个新的请求传给下⼀个⻚⾯,之前的请求结束⽣命周期。

转发:同⼀个请求在服务器之间传递,地址栏不变,也叫服务器跳转。
重定向:由客户端发送⼀次新的请求来访问跳转后的⽬标资源,地址栏改变,也叫客户端跳转。
如果两个⻚⾯之间需要通过 request 来传值,则必须使⽤转发,不能使⽤重定向。
⽤户登录,如果⽤户名和密码正确,则跳转到⾸⻚,并且展示⽤户名(转发),否则重新回到登陆⻚⾯(重定向)。

session

⽤户会话
服务器⽆法识别每⼀次 HTTP 请求的出处(不知道来⾃于哪个终端),它只会接受到⼀个请求信号,所以就存在⼀个问题:将⽤户的响应发送给其他⼈,必须有⼀种技术来让服务器知道请求来⾃哪,这就是会话技术。
会话:就是客户端和服务器之间发⽣的⼀系列连续的请求和响应的过程,打开浏览器进⾏操作到关闭浏览器的过程。
会话状态:指服务器和浏览器在会话过程中产⽣的状态信息,借助于会话状态,服务器能够把属于同⼀次会话的⼀系列请求和响应关联起来。

实现会话有两种⽅式:

  • session(服务端)
  • cookie(客户端)

属于同⼀次会话的请求都有⼀个相同的标识符,sessionID
session 常⽤的⽅法:
String getId() 获取 sessionID
void setMaxInactiveInterval(int interval) 设置 session 的失效时间,单位为秒
int getMaxInactiveInterval() 获取当前 session 的失效时间,默认30分钟
void invalidate() 设置 session ⽴即失效
void setAttribute(String key,Object value) 通过键值对的形式来存储数据
Object getAttribute(String key) 通过键获取对应的数据
void removeAttribute(String key) 通过键删除对应的数据

cookie

Cookie 是服务端在 HTTP 响应中附带传给浏览器的⼀个⼩⽂本⽂件,⼀旦浏览器保存了某个 Cookie,在之后的请求和响应过程中,会将此 Cookie 来回传递,这样就可以通过 Cookie 这个载体完成客户端和服务端的数据交互。

  • 创建 Cookie
Cookie cookie = new Cookie("name","tom");
response.addCookie(cookie);
  • 读取 Cookie
Cookie[] cookies = request.getCookies();
for (Cookie cookie:cookies){
 out.write(cookie.getName()+":"+cookie.getValue()+"<br/>");
}

Cookie 常⽤的⽅法
void setMaxAge(int age) 设置 Cookie 的有效时间,单位为秒
int getMaxAge() 获取 Cookie 的有效时间,默认值为-1即关闭浏览器cookie就没了。
String getName() 获取 Cookie 的 name
String getValue() 获取 Cookie 的 value

Session 和 Cookie 的区别
session:保存在服务器;保存的数据类型是 Object;会随着会话的结束⽽销毁;保存重要信息
cookie:保存在浏览器;保存的数据类型是 String;可以⻓期保存在浏览器中,与会话⽆关;保存不重要信息

存储⽤户信息:
session:setAttribute(“name”,“admin”) 存
getAttribute(“name”) 取
⽣命周期:服务端:只要 WEB 应⽤重启就销毁,客户端:只要浏览器关闭就销毁。
退出登录:session.invalidate()
cookie:response.addCookie(new Cookie(name,“admin”)) 存

Cookie[] cookies = request.getCookies();
for (Cookie cookie:cookies){
 if(cookie.getName().equals("name")){
 out.write("欢迎回来"+cookie.getValue());
 }
}

​ ⽣命周期:不随服务端的重启⽽销毁,客户端:默认是只要关闭浏览器就销毁,我们通过 setMaxAge()⽅法设置有效期,⼀旦设置了有效期,则不随浏览器的关闭⽽销毁,⽽是由设置的时间来决定。
​ 退出登录:setMaxAge(0)

JSP 内置对象作⽤域

4个:page、request、session、application
setAttribute、getAttribute
page 作⽤域:对应的内置对象是 pageContext。
request 作⽤域:对应的内置对象是 request。
session 作⽤域:对应的内置对象是 session。
application 作⽤域:对应的内置对象是 application。
作用域大小:page < request < session < application
page 只在当前⻚⾯有效。
request 在⼀次请求内有效。
session 在⼀次会话内有效。
application 对应整个 WEB 应⽤的。

  • ⽹站访问量统计
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.List" %><%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2023/11/20
  Time: 21:37
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
  hello
  <%
    Integer count =(Integer) application.getAttribute("count");
    if(count == null){
      count = 1;
      application.setAttribute("count", count);
    }else{
      count++;
      application.setAttribute("count", count);
    }
  %>
  您是<%=count%>
  </body>
</html>

EL 表达式

Expression Language 表达式语⾔,替代 JSP ⻚⾯中数据访问时的复杂编码,可以⾮常便捷地取出域对象(pageContext、request、session、application)中保存的数据,前提是⼀定要先 setAttribute,EL 就相当于在简化 getAttribute。
${变量名} 变量名就是 setAttribute 对应的 key 值。

1、EL 对于 4 种域对象的默认查找顺序:

pageContext -》request-〉session-》application

按照上述的顺序进⾏查找,找到⽴即返回,在 application 中也⽆法找到,则返回 null

2、指定作⽤域进⾏查找
pageContext:${pageScope.name}

request:${requestScope.name}

session:${sessionScope.name}

application:${applicationScope.name}

EL 执⾏表达式

${num1&&num2}
&& || ! < > <= <= ==
&& and
|| or
! not
== eq
!= ne
< lt
> gt
<= le
>= ge
empty 变量为 null,⻓度为0的String,size为0的集合

JSTL

JSP Standard Tag Library JSP 标准标签库,JSP 为开发者提供的⼀系列的标签,使⽤这些标签可以完成⼀些逻辑处理,⽐如循环遍历集合,让代码更加简洁,不再出现 JSP 脚本穿插的情况。

过滤器

Filter
功能:
1、⽤来拦截传⼊的请求和传出的响应。
2、修改或以某种⽅式处理正在客户端和服务端之间交换的数据流。

如何使⽤?
与使⽤ Servlet 类似,Filter 是 Java WEB 提供的⼀个接⼝,开发者只需要⾃定义⼀个类并且实现该接⼝即可。Filter接口内容如下:

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}

注意看上面的三个方法,init和destroy前面加了default,doFilter没加。加了default的方法可以被称为默认方法,该方法其实是有方法体的,实现类可以不实现该方法也不会报错。这其实与一句话是矛盾的:接口中只能有方法的定义,没有方法的实现。从JDK1.8以后,在接口方法前面加上default,该方法也可以有方法的实现。所以,上面的一句话还是要看jdk版本的。

使用过滤器解决中文乱码问题

之前出现中文乱码,都是在servlet里面添加request.setCharacterEncoding("UTF-8")来指定字符集。如果有多个servlet,就需要每个servlet都加上这一行,显然有些重复,我们可以加这些重复的代码提取出来,就用到了过滤器。

过滤器其实是位于客户端和服务器servlet之间,客户端请求首先要经过过滤器

这里我们定义解决中文乱码的过滤器

public class CharacterFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //设置字符集
        servletRequest.setCharacterEncoding("UTF-8");
        //注意:下面的一行必须要加上,因为过滤器默认会带一个中断请求的功能,过滤器处理完请求后,请求不会自动往下走,需要我们手动调用filterChain.doFilter让它往后走。filterChain是一个过滤器链,我们可以定义多个多滤器,多个过滤器组成过滤器链,一个过滤器过了之后再过下一个过滤器
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

然后在web.xml里面配置过滤器

<filter>
        <filter-name>character</filter-name>
        <filter-class>com.southwind.servlet.filter.CharacterFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>character</filter-name>
        <!-- 如果要多个url都使用该过滤器,指定多个url-pattern就可以 -->
        <url-pattern>/test</url-pattern>
        <url-pattern>/login</url-pattern>
    </filter-mapping>

注意:doFilter ⽅法中处理完业务逻辑之后,必须添加filterChain.doFilter(servletRequest,servletResponse);否则请求/响应⽆法向后传递,⼀直停留在过滤器中。

Filter 的⽣命周期
当 Tomcat 启动时,通过反射机制调⽤ Filter 的⽆参构造函数创建实例化对象,同时调⽤ init ⽅法实现初始化,doFilter ⽅法调⽤多次,当 Tomcat 服务关闭的时候,调⽤ destory 来销毁 Filter 对象。
⽆参构造函数:只调⽤⼀次,当 Tomcat 启动时调⽤(Filter ⼀定要在web.xml里面进⾏配置)
init ⽅法:只调⽤⼀次,当 Filter 的实例化对象创建完成之后调⽤
doFilter:调⽤多次,访问 Filter 的业务逻辑都写在 Filter 中
destory:只调⽤⼀次,Tomcat 关闭时调⽤。

同时配置多个 Filter,Filter 的调⽤顺序是由 web.xml 中的配置顺序来决定的,写在上⾯的配置先调⽤,因为 web.xml 是从上到下顺序读取的。

<filter>
        <filter-name>myfilter</filter-name>
        <filter-class>com.southwind.servlet.filter.MyFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>myfilter</filter-name>
        <url-pattern>/test</url-pattern>
        <url-pattern>/login</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>character</filter-name>
        <filter-class>com.southwind.servlet.filter.CharacterFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>character</filter-name>
        <url-pattern>/test</url-pattern>
        <url-pattern>/login</url-pattern>
    </filter-mapping>

上面的执行顺序:MyFilter、CharacterFilter

也可以通过注解的⽅式来简化 web.xml 中的配置

<filter>
	<filter-name>myfilter</filter-name>
	<filter-class>com.southwind.servlet.filter.MyFilter</filter-class>
</filter>
<filter-mapping>
	<filter-name>myfilter</filter-name>
	<url-pattern>/login</url-pattern>
</filter-mapping>

等同于

@WebFilter("/login")
public class MyFilter implements Filter {
}

但是,如果有多个filter,我们要保证每个filter的先后执行顺序,那么只能通过web.xml方式进行配置,注解方式不能实现。

实际开发中 Filter 的使⽤场景:
1、统⼀处理中⽂乱码。
2、屏蔽敏感词。

test.jsp

<body>
    <form action="test" method="post">
        <input type="text" name="name">
        <input type="submit" name="提交">
    </form>
</body>

WordFilter.java

@WebFilter("/test")
public class WordFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        servletRequest.setCharacterEncoding("UTF-8");
        String name = servletRequest.getParameter("name");
        System.out.println(name);
        //将敏感词替换为***
        name = name.replaceAll("敏感词", "***");
        System.out.println(name);
        //将替换后的词覆盖name属性,存到attribute中,因为request没有setParameter属性
        servletRequest.setAttribute("name", name);
        filterChain.doFilter(servletRequest,  servletResponse);
    }
}

TestServlet.java

@WebServlet("/test")
public class TestServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       String s =(String) req.getAttribute("name");
        System.out.println(s);
    }
}

上面的代码,我们在浏览器中输入http://localhost:8080/test.jsp,会将test.jsp页面展示出来。我们在输入框中输入【张三敏感词李四】,点击提交。会请求action="test"对应的servlet,但是在servlet之前有一个filter,filter优先级更高,会先进到filter中。在filter中会替换一些敏感词,然后将用替换后的词汇替换原来的,最后做请求传递。来到servlet,将替换后的词汇取出来。

3、控制资源的访问权限。

download.jsp

<body>
    <a href="">资源1</a>
    <a href="">资源2</a>
    <a href="">资源3</a>
</body>

login.jsp

<form action="login" method="post">
    <input type="text" name="name"><br/>
    <input type="password" name="password"><br/>
    <input type="submit" name="提交">
</form>

LoginServlet.java

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name  = req.getParameter("name");
        String password = req.getParameter("password");
        if("admin".equals(name) && "123".equals(password)){
            HttpSession session = req.getSession();
            session.setAttribute("name", name);
            resp.sendRedirect("download.jsp");
        }
    }
}

DownLoadFilter.java

@WebFilter("/download.jsp")
public class DownLoadFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request =(HttpServletRequest) servletRequest;
        HttpServletResponse response =(HttpServletResponse) servletResponse;
        HttpSession session = request.getSession();
        String name = (String) session.getAttribute("name");
        if(name  == null){
            response.sendRedirect("login.jsp");
        }else{
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
}

上面的代码,我们有一个下载页面download.jsp,里面有3个资源。我们现在要做的是,用户必须是登录状态才能访问该页面,否则就跳转到登陆页面login.jsp。我们首先写login.jsp页面,页面设置输入用户名和密码,然后使用action="login"来访问对应的servlet。LoginServlet.java中使用@WebServlet注解,在这里我们获取到用户名密码,然后给session存值,重定向到download.jsp页面。为了实现对download.jsp有无登录页面下权限的控制,我们使用filter来实现。这里创建DownLoadFilter,使用@WebFilter注解,里面配置download.jsp这个资源。然后在doFilter方法里面做判断,如果session有值,就放行doFilter;如果session为空就调整到登录页面。

监听器Listener

专门用来监听其他对象(web资源对象)本身发生的变化(比如对象内的东西被修改了,对象被删除等等)或状态的改变。

当被监听的对象发生变化时,可以立即采取行动做相应的处理。

监听器的分类:

1、监听域对象(pageContext,request,session,application)的创建和销毁

2、监听域对象中属性的新增、修改、删除的事件

3、监听绑定到HttpSession域中的某个对象的状态

实现方式:

1、监听域对象(pageContext,request,session,application)的创建和销毁

需要实现ServletContextListener接口(对应application域对象)、ServletRequestListener接口(对应request域对象)、HttpSessionListener(对应session域对象),pageContext很少使用。

名称创建时机销毁时机
ServletRequestListener每次请求开始时创建每次请求结束时(服务器做出响应时)销毁
HttpSessionListener浏览器与服务器创建会话时1、手动调用Invalid()方法 2、session失效时
ServletContextListenerweb应用启动时web应用关闭时
  • ServletRequestListener
package com.southwind.listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class MyListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
        System.out.println("销毁了request对象");
    }

    @Override
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
        System.out.println("创建request对象");
    }
}

在web.xml中配置listener

<listener>
  <listener-class>com.southwind.listener.MyListener</listener-class>
</listener>

输出结果:

创建request对象
销毁了request对象

当访问某个jsp或者servlet就会触发监听器。request表示一次请求,发请求之前会创建listener对象,请求结束之后listener就会销毁,所以上面会输出创建和销毁两条语句。

  • HttpSessionListener
package com.southwind.listener;

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class MySessionListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent httpSessionEvent) {
        System.out.println("create session");
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
        System.out.println("destory session");
    }
}

在web.xml中配置listener

<listener>
  <listener-class>com.southwind.listener.MySessionListener</listener-class>
</listener>

当我们在浏览器中访问某个页面,就会输出create session,再次访问不会输出create session,是只输出一次。因为这是属于一次会话。和request不一样,request是请求,每次访问都会输出。什么时候会输出destory session呢?当我们关闭浏览器不会输出destory session。因为关闭浏览器是客户端的行为,服务端监听不到。要输出destory session,需要调用session的invalid()方法。

  • ServletContextListener
package com.southwind.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class MyApplicationListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("create application");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.println("destroy application");
    }
}

在web.xml中配置listener

<listener>
  <listener-class>com.southwind.listener.MyApplicationListener</listener-class>
</listener>

在启动应用时就会输出create application,关闭应用会输出destroy application。

2、监听域对象中属性的新增、修改、删除的事件,这里只演示request,其他的类似。

  • ServletRequestAttributeListener

MyRequestAttributeListener

package com.southwind.listener;

import javax.servlet.ServletRequestAttributeEvent;
import javax.servlet.ServletRequestAttributeListener;

public class MyRequestAttributeListener implements ServletRequestAttributeListener {
    @Override
    public void attributeAdded(ServletRequestAttributeEvent servletRequestAttributeEvent) {
        System.out.println("add attribute");
    }

    @Override
    public void attributeRemoved(ServletRequestAttributeEvent servletRequestAttributeEvent) {
        System.out.println("remove attribute");
    }

    @Override
    public void attributeReplaced(ServletRequestAttributeEvent servletRequestAttributeEvent) {
        System.out.println("replace attribute");
    }
}

MyServlet

package com.southwind.controller;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req,  resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setAttribute("name", "zhangsan");
        req.setAttribute("name",   "lisi");
        req.removeAttribute("name");
    }
}

web.xml中配置listener

<listener>
  <listener-class>com.southwind.listener.MyRequestAttributeListener</listener-class>
</listener>

⽂件上传

  • JSP

1、input 的 type 设置为 file
2、form 表单的 method 设置 post,get 请求会将⽂件名传给服务端,⽽不是⽂件本身
3、form 表单的 enctype 设置 multipart/form-data,以⼆进制的形式传输数据

<body>
    <form enctype="multipart/form-data" action="upload" method="post">
        <input name="img" type="file"/><br/>
        <input type="submit" value="上传">
    </form>
</body>
  • Servlet
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //通过输入流获取客户端传来的数据流
        //通过前端上传的东西,已经封装到req中了。我们首先获取对应的字节流
        InputStream inputStream = req.getInputStream();
        // 基于上一步的字节流创建字符流。字节流读取出来的是二进制,字符流就可以读取出文本了。
        // 字节流是几个字节几个字节读取读取比较慢,字符流是按照字符来读取,会快一些
        Reader reader = new InputStreamReader(inputStream);
        //将上一步的字符流按照字符读取,改成按照文本整行读取,进一步提速
        BufferedReader bufferedReader = new BufferedReader(reader);
        //通过输出流将数据流保存到本地文件夹
        //获取文件夹绝对路径。这里我们是要把上传的文件放到file文件夹下
        // (注意:需要在tomcat编译后的out对应目录下创建file文件夹,不然会报错)
        //指定要保存的文件名为copy.txt
        String path = req.getServletContext().getRealPath("file/copy.txt");
        //创建输出字节流
        OutputStream outputStream = new FileOutputStream(path);
        //创建输出字符流
        Writer writer = new OutputStreamWriter(outputStream);
        //创建BufferedWriter
        BufferedWriter bufferedWriter = new BufferedWriter(writer);
        String str = "";
        //每次读取一行,如果调用readLine()不为null,表示没读完
        while ((str = bufferedReader.readLine()) != null){
            bufferedWriter.write(str);
        }
        //最后不要忘记关闭流
        bufferedWriter.close();
        writer.close();
        outputStream.close();
        bufferedReader.close();
        reader.close();
        inputStream.close();
    }
}

上面写法比较复杂,实际开发都是调用成熟工具进行开发。我们这里使用apache的fileupload组件

1、先去官网下载相应jar包,地址:https://archive.apache.org/dist/。找到commons点进去
在这里插入图片描述
分别点击fileupload和io
在这里插入图片描述
然后选择binary,下载对应jar包即可。

2、将下载好的2个jar包导入工程

fileupload 组件可以将所有的请求信息都解析成 FileIteam 对象,可以通过对 FileItem 对象的操作完成上传,⾯向对象的思想。

package com.southwind.servlet;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.List;

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        //通过输入流获取客户端传来的数据流
//        //通过前端上传的东西,已经封装到req中了。我们首先获取对应的字节流
//        InputStream inputStream = req.getInputStream();
//        // 基于上一步的字节流创建字符流。字节流读取出来的是二进制,字符流就可以读取出文本了。
//        // 字节流是几个字节几个字节读取读取比较慢,字符流是按照字符来读取,会快一些
//        Reader reader = new InputStreamReader(inputStream);
//        //将上一步的字符流按照字符读取,改成按照文本整行读取,进一步提速
//        BufferedReader bufferedReader = new BufferedReader(reader);
//        //通过输出流将数据流保存到本地文件夹
//        //获取文件夹绝对路径。这里我们是要把上传的文件放到file文件夹下
//        // (注意:需要在tomcat编译后的out对应目录下创建file文件夹,不然会报错)
//        //指定要保存的文件名为copy.txt
//        String path = req.getServletContext().getRealPath("file/copy.txt");
//        //创建输出字节流
//        OutputStream outputStream = new FileOutputStream(path);
//        //创建输出字符流
//        Writer writer = new OutputStreamWriter(outputStream);
//        //创建BufferedWriter
//        BufferedWriter bufferedWriter = new BufferedWriter(writer);
//        String str = "";
//        //每次读取一行,如果调用readLine()不为null,表示没读完
//        while ((str = bufferedReader.readLine()) != null){
//            bufferedWriter.write(str);
//        }
//        //最后不要忘记关闭流
//        bufferedWriter.close();
//        writer.close();
//        outputStream.close();
//        bufferedReader.close();
//        reader.close();
//        inputStream.close();
        try {
            DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
            ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
            List<FileItem> list = servletFileUpload.parseRequest(req);
            for(FileItem fileItem : list){
                //fileItem为文本框
                if(fileItem.isFormField()){
                    String name =  fileItem.getFieldName();
                    //通过UTF-8编码取内容,因为内容可能是中文
                    String value = fileItem.getString("UTF-8");
                    System.out.println("name:"+name);
                    System.out.println("value:"+value);
                }else{
                    //是文件,需要保存
                    String fileName = fileItem.getName();
                    //转换为输入流
                    InputStream inputStream = fileItem.getInputStream();
                    Reader reader = new InputStreamReader(inputStream);
                    BufferedReader bufferedReader = new BufferedReader(reader);

                    String path = req.getServletContext().getRealPath("file/"+fileName);
                    OutputStream outputStream = new FileOutputStream(path);
                    Writer writer =  new OutputStreamWriter(outputStream);
                    BufferedWriter bufferedWriter = new BufferedWriter(writer);
                    String temp = null;
                    while((temp = bufferedReader.readLine()) != null){
                        bufferedWriter.write(temp);
                    }
                    //最后不要忘记关闭流
                    bufferedWriter.close();
                    writer.close();
                    outputStream.close();
                    bufferedReader.close();
                    reader.close();
                    inputStream.close();
                    System.out.println("上传成功");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

上述代码测试发现,通过bufferedReader和bufferedWriter实现上传保存的txt文本,中文会有一部分乱码,还有就是中文没有换行了。

还有一个问题,通过上述方式上传的图片无法打开。是因为图片读出来是二进制流的形式,然后输出的时候通过字符进行输出的,图片内部数据的构造方式被打乱了。

解决方法是换用inputStream和outputStream。最终的代码如下

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//        //通过输入流获取客户端传来的数据流
//        //通过前端上传的东西,已经封装到req中了。我们首先获取对应的字节流
//        InputStream inputStream = req.getInputStream();
//        // 基于上一步的字节流创建字符流。字节流读取出来的是二进制,字符流就可以读取出文本了。
//        // 字节流是几个字节几个字节读取读取比较慢,字符流是按照字符来读取,会快一些
//        Reader reader = new InputStreamReader(inputStream);
//        //将上一步的字符流按照字符读取,改成按照文本整行读取,进一步提速
//        BufferedReader bufferedReader = new BufferedReader(reader);
//        //通过输出流将数据流保存到本地文件夹
//        //获取文件夹绝对路径。这里我们是要把上传的文件放到file文件夹下
//        // (注意:需要在tomcat编译后的out对应目录下创建file文件夹,不然会报错)
//        //指定要保存的文件名为copy.txt
//        String path = req.getServletContext().getRealPath("file/copy.txt");
//        //创建输出字节流
//        OutputStream outputStream = new FileOutputStream(path);
//        //创建输出字符流
//        Writer writer = new OutputStreamWriter(outputStream);
//        //创建BufferedWriter
//        BufferedWriter bufferedWriter = new BufferedWriter(writer);
//        String str = "";
//        //每次读取一行,如果调用readLine()不为null,表示没读完
//        while ((str = bufferedReader.readLine()) != null){
//            bufferedWriter.write(str);
//        }
//        //最后不要忘记关闭流
//        bufferedWriter.close();
//        writer.close();
//        outputStream.close();
//        bufferedReader.close();
//        reader.close();
//        inputStream.close();
        try {
            DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
            ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
            List<FileItem> list = servletFileUpload.parseRequest(req);
            for(FileItem fileItem : list){
                //fileItem为文本框
                if(fileItem.isFormField()){
                    String name =  fileItem.getFieldName();
                    //通过UTF-8编码取内容,因为内容可能是中文
                    String value = fileItem.getString("UTF-8");
                    System.out.println("name:"+name);
                    System.out.println("value:"+value);
                }else{
                    //是文件,需要保存
                    String fileName = fileItem.getName();
                    InputStream inputStream = fileItem.getInputStream();
//                    Reader reader = new InputStreamReader(inputStream);
//                    BufferedReader bufferedReader = new BufferedReader(reader);

                    String path = req.getServletContext().getRealPath("file/"+fileName);
                    OutputStream outputStream = new FileOutputStream(path);
//                    Writer writer =  new OutputStreamWriter(outputStream);
//                    BufferedWriter bufferedWriter = new BufferedWriter(writer);
                    int temp = 0;
                    while((temp = inputStream.read()) != -1){
                        outputStream.write(temp);
                    }
                    //最后不要忘记关闭流
//                    bufferedWriter.close();
//                    writer.close();
                    outputStream.close();
//                    bufferedReader.close();
//                    reader.close();
                    inputStream.close();
                    System.out.println("上传成功");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

文件下载

和文件上类似,文件下载就是通过输入流将服务端的文件读取到内存中,然后通过输出流将文件写入到本地。

download.jsp。这是设置type区分下载类型。

<body>
    <a href="/download?type=png">1.PNG</a>
    <a href="/download?type=txt">test.txt</a>
</body>

DownloadServlet.java

@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置下载后的文件名
        String fileName =  "";
        //获取下载类型
        String type = req.getParameter("type");
        switch (type){
            case "png":
                fileName = "1.PNG";
                break;
            case  "txt":
                fileName =  "test.txt";
                break;
        }

        //上传通过request,下载通过response
        //设置响应方式。启动下载器,会调用浏览器的下载工具
        resp.setContentType("application/x-msdownload");
        //设置下载后的文件名
        resp.setHeader("Content-Disposition", "attachment;filename="+fileName);
        //获取输出流
        OutputStream  outputStream = resp.getOutputStream();
        String path = req.getServletContext().getRealPath("file/"+fileName);
        InputStream  inputStream = new FileInputStream(path);
        int temp = 0;
        while((temp = inputStream.read()) != -1){
            outputStream.write(temp);
        }
        inputStream.close();
        outputStream.close();
    }
}

需要注意的是:如果我们在IDEA的tomcat项目的web文件夹下面保存着要下载的资源,启动项目提示没找到对应资源,这是因为out打包后的文件夹没有对应的文件。可以这样做:

1、点击Build,选择Build Artifacts
在这里插入图片描述

2、选择Rebuild。这样就会在打包后的out目录下生成对应的文件,点击下载就没有问题了。
在这里插入图片描述

Ajax

Asynchronous JavaScript And XML:异步的 JavaScript 和 XML
AJAX 不是新的编程,指的是⼀种交互⽅式,异步加载,客户端和服务器的数据交互更新在局部⻚⾯的技术,不需要刷新整个⻚⾯(局部刷新)
优点:
1、局部刷新,效率更⾼
2、⽤户体验更好

同步方式必须是一步一步执行,前一步没执行,后一步会一直等待前一步的结果

  • 同步代码示例

test.jsp

<body>
    ${str}
    <form action="/test" method="post">
        <input type="text">
        <input type="submit" value="提交">
    </form>
</body>

TestServlet

@WebServlet("/test")
public class TestServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }

        String str = "hello";
        req.setAttribute("str", str);
        req.getRequestDispatcher("test.jsp").forward(req, resp);
    }
}

我们在浏览器输入http://localhost:8080/test.jsp,会看到下面的页面
在这里插入图片描述
当我们点击提交后,会请求TestServlet,这个类里面我们定义线程等待3秒。在3秒期间,我们可以在文本框输入一些内容,3秒后会将hello存入request中,同时将请求转发回test.jsp,然后test.jsp就会展示出hello。但是,我们发现一个问题,刚刚在文本框中输入的内容都清空了。这就是同步导致的,解决方法就是换为异步
在这里插入图片描述

  • 异步示例。基于 jQuery 的 A JAX
<head>
    <title>Title</title>
    <!-- 引入jquery -->
    <script type="text/javascript" src="js/jquery-3.5.1.min.js"></script>
    <script type="text/javascript">
        $(function (){
            //获取id为btn的dom节点
            var btn = $("#btn");
            //为btn绑定点击事件
            btn.click(function (){
                $.ajax({
                    url: "/test", //请求地址
                    type: "post", //请求方式
                    dataType: "text", //服务端返回格式,这里服务端返回文本类型,使用text
                    success: function (data){   //请求成功
                        var text = $("#text");
                        //在text对应dom节点前加上data
                        text.before("<span>" +  data +  "</span><br/>");
                    }
                });
            });
        })
    </script>
</head>
<body>
    <!--  这里不能使用form表单提交了,form表单是同步的 -->
    <input id="text" type="text">
    <input id="btn" type="button" value="提交">
</body>
@WebServlet("/test")
public class TestServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }

        String str = "hello";
        //Servlet 不能返回 JSP,只能将数据返回
        resp.getWriter().write(str);
    }
}

改为异步之后,再次点击提交,在等待3秒过程中,我们再次在文本框中输入内容,3秒结束后,hello被展示出来,同时之前在文本框中输入的内容不会被清空。

在这里插入图片描述
不能⽤表单提交请求,改⽤ jQuery ⽅式动态绑定事件来提交。**

Servlet 不能返回 JSP,只能将数据返回。

传统的 WEB 数据交互 VS AJAX 数据交互

  • 客户端请求的⽅式不同:

    传统,浏览器发送同步请求 (form表单、a标签)
    AJAX,异步引擎对象发送异步请求

  • 服务器响应的⽅式不同:

    传统,响应⼀个完整 JSP ⻚⾯(视图)
    AJAX,响应需要的数据(数据)

  • 客户端处理⽅式不同:

    传统:需要等待服务器完成响应并且重新加载整个⻚⾯之后,⽤户才能进⾏后续的操作
    AJAX:动态更新⻚⾯中的局部内容,不影响⽤户的其他操作

AJAX原理
在这里插入图片描述
用户的操作现在要经过AJAX引擎来传递到服务器,用户是通过JavaScript调用AJAX引擎(对应的是$.ajax那一段代码)。然后AJAX引擎发生HTTP请求到服务器,服务器处理请求并返回数据(格式有TEXT/JSON/HTML/XML),交给AJAX引擎,然后再通过操作JavaScript(对应图上的DOM+CSS)来实现数据展示。

基于 jQuery 的 AJAX 语法

$.ajax({属性})
常⽤的属性参数:

  • url:请求的后端服务地址
  • type:请求⽅式,默认 get
  • data:请求参数
  • dataType:服务器返回的数据类型,常用类型为text/json
  • success:请求成功的回调函数
  • error:请求失败的回调函数
  • complete:请求完成的回调函数(⽆论成功或者失败,都会调⽤)
<script type="text/javascript">
    $(function (){
        //获取id为btn的dom节点
        var btn = $("#btn");
        //为btn绑定点击事件
        btn.click(function (){
            $.ajax({
                url: "/test", //请求地址
                type: "post", //请求方式
                dataType: "text", //服务端返回格式,这里服务端返回文本类型,使用text
                success: function (data){   //请求成功
                    var text = $("#text");
                    //在text对应dom节点前加上data
                    text.before("<span>" +  data +  "</span><br/>");
                },error: function (){
                    alert("失败了");
                },complete: function (){
                    alert("请求完成");
                }
            });
        });
    })
</script>

还是上面的代码,我们加上了error和complete。在执行时,如果请求成功会先执行success,然后执行complete。

如果我们把请求url改为/test2,会先展示“失败了”,然后展示“请求完成”。

JSON
JavaScript Object Notation,⼀种轻量级数据交互格式,完成 js 与 Java 等后端开发语⾔对象数据之间的转换。

java中要使用json需要导入相应的jar包,一共涉及6个,版本根据自己需要更换

json-lib-2.4-jdk15.jar
ezmorph-1.0.6.jar
commons-lang-2.6.jar
commons-collections-3.2.2.jar
commons-beanutils-1.9.4.jar
commons-logging-1.2.jar

json-lib下载地址:https://sourceforge.net/projects/json-lib/files/json-lib/

ezmorph下载地址:https://sourceforge.net/projects/ezmorph/files/ezmorph/

commons-lang下载地址:http://commons.apache.org/lang/download_lang.cgi

commons-collections下载地址:https://archive.apache.org/dist/commons/collections/binaries/

commons-beanutils下载地址:http://commons.apache.org/beanutils/download_beanutils.cgi

commons-logging下载地址:http://commons.apache.org/logging/download_logging.cgi

后端返回JSON

<html>
<head>
    <title>Title</title>
    <!-- 引入jquery -->
    <script type="text/javascript" src="js/jquery-3.5.1.min.js"></script>
    <script type="text/javascript">
        $(function (){
            //获取id为btn的dom节点
            var btn = $("#btn");
            //为btn绑定点击事件
            btn.click(function (){
                $.ajax({
                    url: "/test", //请求地址
                    type: "post", //请求方式
                    dataType: "json", //服务端返回格式
                    success: function (data){   //请求成功
                        //给属性赋值
                        $("#id").val(data.id);
                        $("#name").val(data.name);
                        $("#score").val(data.score);
                    },error: function (){
                        alert("失败了");
                    },complete: function (){
                        alert("请求完成");
                    }
                });
            });
        })
    </script>
</head>
<body>
    编号:<input id="id" type="text"><br/>
    姓名:<input id="name" type="text"><br/>
    分数:<input id="score" type="text"><br/>
    <input id="btn" type="button" value="提交">
</body>
</html>
@WebServlet("/test")
public class TestServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        User user  = new User(1, "张三", 98.9);
        //解决中文乱码
        resp.setCharacterEncoding("UTF-8");
        //将java对象转换为json格式
        JSONObject jsonObject = JSONObject.fromObject(user);
        //响应回去
        resp.getWriter().write(jsonObject.toString());
    }
}

上面的代码界面如下:
在这里插入图片描述
点击提交按钮后会将数据显示。
在这里插入图片描述
AJAX实现省市区三级联动

<%--
  Created by IntelliJ IDEA.
  User: xxx
  Date: 2023/12/4
  Time: 20:45
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <script type="text/javascript" src="js/jquery-3.5.1.min.js"></script>
    <script type="text/javascript">
      $(function (){
          //修改省份
          $("#province").change(function (){
              var id = $(this).val();
              $.ajax({
                  url: "/location",
                  type: "post",
                  data: "id="+id+"&type=province",   //添加type属性,区分是切换省份还是切换城市
                  dataType: "JSON",
                  success: function (data){
                      var content = "";
                      var cities = data.cities;
                      for(var i=0; i<cities.length; i++){
                          //将data拼到content中
                          content += "<option>"+cities[i]+"</option>";
                      }
                      //使用content来替换area
                      $("#city").html(content);

                      var content = "";
                      var areas = data.areas;
                      for(var i=0; i<areas.length; i++){
                          //将data拼到content中
                          content += "<option>"+areas[i]+"</option>";
                      }
                      //使用content来替换area
                      $("#area").html(content);
                  }
              })
            }
          )
          //修改城市
        //绑定city dom节点的change方法,用于监听city的变化
        $("#city").change(function (){
          //获取当前city所选option对应的value值,this对应的是option
          var id = $(this).val();
          //我们想要做菜单的联动,即市变化了,对应的区跟着变。
          //这里我们已经获取到city对应的value值了,接下来通过ajax来实现
          $.ajax({
            url: "/location",
            type: "post",
            data: "id="+id+"&type=city",
            dataType: "JSON",
            success: function (data){
                //用后端返回的data来替换area对应的option值
                //我们现在要把返回的data拼成option标签
                //先定义一个空标签
                var content = "";
                for(var i=0; i<data.length; i++){
                    //将data拼到content中
                    content += "<option>"+data[i]+"</option>";
                }
                //使用content来替换area
                $("#area").html(content);
            }
          });
        })
      });
    </script>
</head>
<body>
省:<select id="province">
    <option value="陕⻄省">陕⻄省</option>
    <option value="河南省">河南省</option>
    <option value="江苏省">江苏省</option>
</select>
市:<select id="city">
    <option value="⻄安市">⻄安市</option>
    <option value="宝鸡市">宝鸡市</option>
    <option value="渭南市">渭南市</option>
</select>
区:<select id="area">
    <option>雁塔区</option>
    <option>莲湖区</option>
    <option>新城区</option>
</select>
</body>
</html>
package com.southwind.servlet;

import com.southwind.entity.Location;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@WebServlet("/location")
public class LocationServlet extends HttpServlet {
    private static Map<String, List<String>> cityMap;
    private static Map<String, List<String>> provinceMap;
    static {
        cityMap = new HashMap<>();
        List<String> areas = new ArrayList<>();
        //⻄安
        areas.add("雁塔区");
        areas.add("莲湖区");
        areas.add("新城区");
        cityMap.put("⻄安市",areas);
        //宝鸡
        areas = new ArrayList<>();
        areas.add("陈仓区");
        areas.add("渭宾区");
        areas.add("新城区");
        cityMap.put("宝鸡市",areas);
        //渭南
        areas = new ArrayList<>();
        areas.add("临渭区");
        areas.add("⾼新区");
        cityMap.put("渭南市",areas);
        //郑州
        areas = new ArrayList<>();
        areas.add("郑州A区");
        areas.add("郑州B区");
        cityMap.put("郑州市",areas);
        //洛阳
        areas = new ArrayList<>();
        areas.add("洛阳A区");
        areas.add("洛阳B区");
        cityMap.put("洛阳市",areas);
        provinceMap = new HashMap<>();
        List<String> cities = new ArrayList<>();
        cities.add("⻄安市");
        cities.add("宝鸡市");
        cities.add("渭南市");
        provinceMap.put("陕⻄省",cities);
        cities = new ArrayList<>();
        cities.add("郑州市");
        cities.add("洛阳市");
        cities.add("开封市");
        provinceMap.put("河南省",cities);
        cities = new ArrayList<>();
        cities.add("南京市");
        cities.add("苏州市");
        cities.add("南通市");
        provinceMap.put("江苏省",cities);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //设置字符集
        resp.setCharacterEncoding("UTF-8");
        String type = req.getParameter("type");
        String id = req.getParameter("id");
        switch (type){
            case "city":
                List<String> areas = cityMap.get(id);
                //将list封装为json
                JSONArray jsonArray = JSONArray.fromObject(areas);
                resp.getWriter().write(jsonArray.toString());
                break;
            case "province":
                List<String> cities = provinceMap.get(id);
                String city = cities.get(0);
                List<String> cityAreas = cityMap.get(city);
                Location location = new Location();
                location.setCities(cities);
                location.setAreas(cityAreas);
                JSONObject  jsonObject = JSONObject.fromObject(location);
                resp.getWriter().write(jsonObject.toString());
                break;
        }
    }
}

JDBC

Java DataBase Connectivity 是⼀个独⽴于特定数据库的管理系统,通⽤的 SQL 数据库存取和操作的公共接⼝。
定义了⼀组标准,为访问不同数据库提供了统⼀的途径。
在这里插入图片描述
有一个java应用,开始是针对mysql数据库开发的,现在要迁移到oracle,如果没有jdbc,所有针对mysql的代码都要重写。
在这里插入图片描述
JDBC是位于应用和数据库之间的。和JAVA跨平台特性类似,有了JDBC之后,我们只需要针对JDBC接口进行编程,而迁移数据库,只需要更改连接数据库的相关配置即可。

JDBC 体系结构
JDBC 接⼝包括两个层⾯:

  • ⾯向应⽤的 API,供程序员调⽤

  • ⾯向数据库的 API,供⼚商开发数据库的驱动程序
    在这里插入图片描述
    JDBC API
    提供者:Java 官⽅
    内容:供开发者调⽤的接⼝
    java.sql 和 javax.sql

  • DriverManager 类

  • Connection 接⼝

  • Statement 接⼝

  • ResultSet 接⼝

DriverManager
提供者:Java 官⽅
作⽤:管理不同的 JDBC 驱动
JDBC 驱动
提供者:数据库⼚商
作⽤:负责连接不同的数据库

JDBC 的使⽤

1、加载数据库驱动,Java 程序和数据库之间的桥梁。
2、获取 Connection,Java 程序与数据库的⼀次连接。
3、创建 Statement 对象,由 Connection 产⽣,执⾏ SQL 语句。
4、如果需要接收返回值,创建 ResultSet 对象,保存 Statement 执⾏之后所查询到的结果。

看一个例子,这里我们用mysql数据库,需要导入mysql驱动。

mysql驱动下载地址:https://downloads.mysql.com/archives/c-j/

选择版本为5,下载即可。

package com.southwind.test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class Test {
    public static void main(String[] args) {
        try {
            //加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            //获取连接,设置连接本地数据库test,同时解决中文乱码
            String url = "jdbc:mysql://localhost:3306/sell?useUnicode=true&characterEncoding=UTF-8";
            String username = "root";
            String password = "root";
            Connection connection = DriverManager.getConnection(url, username, password);
            //创建sql语句新增数据
            String sql = "insert into book(name, author) values('java', 'zhangsan')";
            Statement statement = connection.createStatement();
            int result = statement.executeUpdate(sql);
            System.out.println(result);
            //创建sql语句查询数据
            String sql1 = "select * from book";
            Statement statement1 = connection.createStatement();
            ResultSet resultSet = statement1.executeQuery(sql1);
            while(resultSet.next()){
                Integer id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                String score = resultSet.getString("author");
                System.out.println(id+"-"+name+"-"+score);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

在实际开发者不会使用Statement,而是使用它的子类PreparedStatement。

PreparedStatement
Statement 的⼦类,提供了 SQL 占位符的功能
使⽤ Statement 进⾏开发有两个问题:
1、需要频繁拼接 String 字符串,出错率较⾼。
2、存在 SQL 注⼊的⻛险。
SQL 注⼊:利⽤某些系统没有对⽤户输⼊的信息进⾏充分检测,在⽤户输⼊的数据中注⼊⾮法的 SQL语句,从⽽利⽤系统的 SQL 引擎完成恶意⾏为的做法。

sql注入例子。这里我们使用一个非法用户名aaa和密码999就可以成功登录系统

package com.southwind.test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class Login {
    public static void main(String[] args) {
        try {
            //加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            //获取连接,设置连接本地数据库test,同时解决中文乱码
            String url = "jdbc:mysql://localhost:3306/sell?useUnicode=true&characterEncoding=UTF-8";
            String username = "root";
            String password = "root";
            Connection connection = DriverManager.getConnection(url, username, password);
            String myUsername = "aaa' or '1'='1";
            System.out.println(myUsername);
            String myPassword = "999' or '1'='1";
            System.out.println(myPassword);
            String  sql = "select * from user where username='"+myUsername+"' and password='"+myPassword+"'";
            System.out.println(sql);
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery(sql);
            if(resultSet.next()){
                System.out.println("登录成功");
            }else{
                System.out.println("登录失败");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
结果:
登录成功
package com.southwind.test;

import java.sql.*;

public class Login {
    public static void main(String[] args) {
        try {
            //加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            //获取连接,设置连接本地数据库test,同时解决中文乱码
            String url = "jdbc:mysql://localhost:3306/sell?useUnicode=true&characterEncoding=UTF-8";
            String username = "root";
            String password = "root";
            Connection connection = DriverManager.getConnection(url, username, password);
            String myUsername = "aaa' or '1'='1";
            String myPassword = "999' or '1'='1";
            //使用占位符
            String sql = "select * from user where username=? and  password=?";
            PreparedStatement  preparedStatement =  connection.prepareStatement(sql);
            //替换用户名和密码
            preparedStatement.setString(1, myUsername);
            preparedStatement.setString(2, myPassword);
            ResultSet resultSet = preparedStatement.executeQuery();
            if(resultSet.next()){
                System.out.println("登录成功");
            }else{
                System.out.println("登录失败");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
登录失败

数据库连接池

JDBC 开发流程

  • 加载驱动(只需要加载⼀次)
  • 建⽴数据库连接(Connection)
  • 执⾏ SQL 语句(Statement)
  • ResultSet 接收结果集(查询)
  • 断开连接,释放资源

数据库连接对象是通过 DriverManager 来获取的,每次获取都需要向数据库申请获取连接,验证⽤户名和密码,执⾏完 SQL 语句后断开连接,这样的⽅式会造成资源的浪费,数据连接资源没有得到很好的重复利⽤。
可以使⽤数据库连接池解决这⼀问题。

数据库连接池的基本思想就是为数据库建⽴⼀个缓冲池,预先向缓冲池中放⼊⼀定数量的连接对象,当需要获取数据库连接的时候,只需要从缓冲池中取出⼀个对象,⽤完之后再放回到缓冲池中,供下⼀次请求使⽤,做到了资源的重复利⽤,允许程序重复使⽤⼀个现有的数据库连接对象,⽽不需要重新创建。

当数据库连接池中没有空闲的连接时,新的请求就会进⼊等待队列,等待其他线程释放连接。

DriverManager.getConnection()是直接去数据库请求连接的,没有连接池这一步。

数据库连接池实现

JDBC 的数据库连接池使⽤ javax.sql.DataSource 接⼝来完成的,DataSource 是 Java 官⽅提供的接⼝,使⽤的时候开发者并不需要⾃⼰来实现该接⼝,可以使⽤第三⽅的⼯具,C3P0 是⼀个常⽤的第三⽅实现,实际开发中直接使⽤ C3P0 即可完成数据库连接池的操作。

1、导⼊ jar 包。下载地址:https://sourceforge.net/projects/c3p0/

注意:C3P0高版本还需要导入mchange-commons-java-0.2.19.jar,在下载好的C3P0的lib目录下有

2、代码实现

package com.southwind.test;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import java.sql.Connection;

public class DataSourceTest {
    public static void main(String[] args) {
        try {
            //创建C3P0
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
            dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/sell?useUnicode=true&characterEncoding=UTF-8");
            dataSource.setUser("root");
            dataSource.setPassword("root");
            //设置初始化连接个数
            dataSource.setInitialPoolSize(20);
            //设置最大连接数
            dataSource.setMaxPoolSize(40);
            //当连接对象不够的时候,再次申请的连接对象个数
            dataSource.setAcquireIncrement(5);
            //设置最小连接数。当数据库连接池中只有2个空闲的连接时,会向数据库重新申请一批连接
            dataSource.setMinPoolSize(2);
            Connection connection = dataSource.getConnection();
            System.out.println(connection);
            //将连接换回到连接池
            connection.close();
        }catch (Exception  e){
            e.printStackTrace();
        }
    }
}

实际开发,将 C3P0 的配置信息定义在 xml ⽂件中,Java 程序只需要加载配置⽂件即可完成数据库连接池的初始化操作。
1、配置⽂件的名字必须是 c3p0-config.xml
2、初始化 ComboPooledDataSource 时,传⼊的参数必须是 c3p0-config.xml 中 named-config 标签
的 name 属性值。

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
    <named-config name="testc3p0">
        <!-- 指定连接数据源的基本属性 -->
        <property name="user">root</property>
        <property name="password">root</property>
        <property name="driverClass">com.mysql.cj.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/sell?useUnicode=true&amp;characterEncoding=UTF-8</property>
        <!-- 若数据库中连接数不⾜时, ⼀次向数据库服务器申请多少个连接 -->
        <property name="acquireIncrement">5</property>
        <!-- 初始化数据库连接池时连接的数量 -->
        <property name="initialPoolSize">20</property>
        <!-- 数据库连接池中的最⼩的数据库连接数 -->
        <property name="minPoolSize">2</property>
        <!-- 数据库连接池中的最⼤的数据库连接数 -->
        <property name="maxPoolSize">40</property>
    </named-config>
</c3p0-config>
package com.southwind.test;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import java.sql.Connection;

public class DataSourceTest {
    public static void main(String[] args) {
        try {
            //创建C3P0,传入配置文件里面的C3P0对应的name
            ComboPooledDataSource dataSource = new ComboPooledDataSource("testc3p0");
            Connection connection = dataSource.getConnection();
            System.out.println(connection);
            //将连接换回到连接池
            connection.close();
        }catch (Exception  e){
            e.printStackTrace();
        }
    }
}

DBUtils

package com.southwind.test;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.southwind.entity.Book;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DBUtilsTest {
    public static void main(String[] args) {
        PreparedStatement preparedStatement = null;
        Connection connection = null;
        ResultSet resultSet = null;
        Book book = null;
        try {
            //创建C3P0,传入配置文件里面的C3P0对应的name
            ComboPooledDataSource dataSource = new ComboPooledDataSource("testc3p0");
            connection = dataSource.getConnection();
            String sql = "select * from book";
            preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            while (resultSet.next()){
                Integer id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                String author = resultSet.getString("author");
                book = new Book(id, name, author);
                System.out.println(book);
            }
            //将连接换回到连接池
            connection.close();
        }catch (Exception  e){
            e.printStackTrace();
        }finally {
            try {
                connection.close();
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

上面的代码,在getInt、getString方法,如果表里的字段比较多,那么就要写好几行。可以使用DBUtils来优化。

DBUtils 可以帮助开发者完成数据的封装(结果集到 Java 对象的映射)

1、导入jar包

下载地址:https://commons.apache.org/proper/commons-dbutils/download_dbutils.cgi

ResultHandler 接⼝是⽤来处理结果集,可以将查询到的结果集转换成 Java 对象,提供了 4 种实现类。

  • BeanHandler 将结果集映射成 Java 对象 Student
  • BeanListHandler 将结果集映射成 List 集合 List
  • MapHandler 将结果集映射成 Map 对象
  • MapListHandler 将结果集映射成 MapList 结合
package com.southwind.test;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import com.southwind.entity.Book;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

public class DBUtilsTest {
    public static void main(String[] args) {
        findStdDBUtils();
    }

    public static void findStdDBUtils(){
        ComboPooledDataSource dataSource = new ComboPooledDataSource("testc3p0");
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            String sql = "select * from book ";
            QueryRunner queryRunner =  new QueryRunner();
            //注意,这里要把Book传进去,这样就可以把结果和我们的Book进行数据绑定了
            List<Book> books = queryRunner.query(connection, sql, new BeanListHandler<>(Book.class));
            System.out.println(books);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

HTTP状态码

在这里插入图片描述

在这里插入图片描述

mybatis占位符

推荐使用#,可以解决SQL注入问题,同时提供预编译功能,提升查询效率。

解决SQL注入:使用#会将对应的SQL语句的参数预编译为?,然后将传入的参数整个作为字符串,替换?。而使用$则不同,它是之间拼接SQL,会导致SQL注入

提升查询效率:使用#会对SQL语句在数据库端进行预编译,编译之后存入缓存,下次遇到同样的SQL直接从缓存中去取就可以,提升效率。
在这里插入图片描述

获取新增后的数据id

我们通过insert新增一条数据后,如果想要获取到对应的主键ID,该怎么做呢?一种是再执行一次SELECT,另外一种就是在INSERT语句进行配置。

@Options(keyProperty = "id",  useGeneratedKeys = true)
@Insert("insert into book(name, author) value (#{name}, #{author})")
public Integer insert(Book book);

上述代码useGeneratedKeys = true表示支持使用新增后的值,keyProperty = "id"表示将主键ID封装到Book类的id属性。

配置mybatis的执行SQL打印控制台

在application.yml中添加

mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

解决启动tomcat控制台中文乱码

找到tomcat的/conf/logging.properties文件,修改字符集为GBK

java.util.logging.ConsoleHandler.encoding = GBK

文件上传简单实现

  • 使用源文件名保存
public void(MultipartFile  image) throw Exception{
	String name - image.getOriginalFilename();
	image.transferTo(new File("E:/images/+name));
}
  • 使用UUID生成新文件名保存
public void(MultipartFile  image) throw Exception{
	String name - image.getOriginalFilename();
	String newName = UUID.randowUUID().toString()+name.substring(name.lastIndexOf("."));
	image.transferTo(new File("E:/images/+newName));
}

上述代码,上传大文件(超过1M)会报错:
在这里插入图片描述
这是因为SpringBoot中,文件上传默认单个文件的最大大小为1M。如果要进行大文件上传,需要进行配置:

#配置单个文件最大上传大小
spring.servlet.multipart.max-file-sie=10MB

#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-sie=100MB

JWT

JWT全称:JSON Web Token

定义了一种简洁、自包含的格式,用于在通信双方以JSON数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
在这里插入图片描述
应用场景:登录认证。
在这里插入图片描述

  • JWT令牌生成和校验
    1、引入pom依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2、生成jwt代码

@Test
public void test(){
    //用户自定义信息
    Map<String, Object> claims = new HashMap<>();
    claims.put("id", 1);
    claims.put("name", "tom");

    String jwt = Jwts.builder()
            .signWith(SignatureAlgorithm.HS256, "hello") //指定签名算法和密钥
            .setClaims(claims)  //自定义信息
            .setExpiration(new Date(System.currentTimeMillis() + 3600*1000)) //指定令牌有效期,这里指定当前时间加一小时,即令牌有效期为1小时
            .compact();
    System.out.println(jwt);
}

输出结果:

eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcxMTg3NzkwM30.TBjg61y-Ob4M0TuFU0Mdj_fgvKgKiMZO8oS2aXhOFw4

我们将上述jwt复制,然后粘贴到jwt官网(https://jwt.io/),就可以解析出来正常的内容。
在这里插入图片描述
3、解析jwt代码

@Test
public void testParseJwt(){
    Claims claims  = Jwts.parser()
            .setSigningKey("hello") //指定密钥
            .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTcxMTg3NzkwM30.TBjg61y-Ob4M0TuFU0Mdj_fgvKgKiMZO8oS2aXhOFw4")
            .getBody();
    System.out.println(claims);
}

输出结果:

{name=tom, id=1, exp=1711877903}

4、解析过期令牌

上面的令牌过期时间为1小时,如果我们解析一个过期的令牌
在这里插入图片描述
5、登录后下发JWT令牌。同上述代码一样,把生成代码复制一下就可以。

6、登录后校验JWT令牌,我们可以添加一个过滤器filter来统一处理。校验代码和解析令牌的代码类似。

还可以通过spring提供的interceptor实现。

Interceptor拦截器

  • 概述

拦截器是一致动态方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。

  • 作用

拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

  • 使用

1、定义拦截器,实现HandlerInterceptor接口,并重写其所有方法。

2、注册拦截器

拦截器代码

@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override // 目标资源方法(controller)执行前执行,返回true-放行;返回false-不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle");
        return true;
    }

    @Override // 目标资源方法(controller)执行后执行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle");
    }

    @Override // 视图渲染完毕后执行,最后执行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion");
    }
}

注册拦截器

addPathPatterns:表示当前拦截器要拦截哪些资源

excludePathPatterns:表示当前拦截器不拦截哪些资源
在这里插入图片描述

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckIntercepto;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // **表示拦截所有请求
        registry.addInterceptor(loginCheckIntercepto).addPathPatterns("/**").excludePathPatterns("/login");
    }
}

测试:我们去调用controller,输出结果:

preHandle
hello world
postHandle
afterCompletion

分析:

hello world是controller中输出的。输出顺序是preHandle、controller、postHandle和afterCompletion。

如果我们把preHandle的返回值改为false,再次调用controller,不会有hello world输出的,只有preHandle输出,因为被拦截了。

拦截器也可以配置拦截器链,类似filter链

  • 拦截器执行流程
    在这里插入图片描述
    浏览器发生请求,会先被过滤器拦截,执行放行前逻辑和doFilter()放行方法,然后去访问spring容器(注意:Filter是Servlet中的组件,是独立于Spring容器的)。请求会先经过前置控制器DispatcherServlet,然后到拦截器,在拦截器中执行preHandle后,请求才到controller。执行完成controller之后,执行拦截器的postHandle和afterCompletion,然后给到DispatcherServlet,再给到filter放行之后的逻辑,最后才到浏览器。

  • 拦截器和过滤器的区别

归属不同:过滤器属于Servlet(Java WEB),拦截器属于Spring

接口规范不同:过滤器需要实现Filter接口,拦截器需要实现HandlerInterceptor接口

拦截范围不同:过滤器Filter会拦截所有的资源,而拦截器只会拦截Spring环境中的资源。

全局异常处理

每个类执行的时候都有可能抛出异常,如果在每个类中都加上try-catch捕获异常,就会显得代码重复。我们可以把这部分代码抽取出来,形成全局异常处理器。

我们声明一个类,在类上面加上@RestControllerAdvice这个注解,就变成全局异常处理器了。接着,我们定义方法,在方法上面加上注解@ExceptionHandler(Exception.class),Exception.class表示,我们要捕获所有类型的异常。如果要捕获特定的异常,只需要改为特定的异常类即可。

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public void ex(Exception e){
        e.printStackTrace();
    }
}

事务管理

Spring中事务管理

注解:@Transactional

位置:service层的方法、类、接口上

作用:将当前方法交给spring进行事务管理,方法执行前开启事务;成功执行完毕,提交事务;出现异常,回滚事务

  • 加到方法上
@Transactional
public void delete(Integer id){
	//1、删除部门
	deptMapper.delete(id);
	//模拟异常
	int i  = 1/0;
	//删除部门下的员工
	empMapper.delete(id);
}
  • 加到接口
@Transactional
public void delete(Integer id){}
  • 加到类上
@Transactional
@Service
public class DeptServiceImpl implements DeptService{

}

事务进阶

我们改下上面模拟异常的代码:

@Transactional
public void delete(Integer id) throws Exception{
	//1、删除部门
	deptMapper.delete(id);
	//模拟异常
	if(true){
		throw new Exception("出错了");
	}
	
	//删除部门下的员工
	empMapper.delete(id);
}

再次执行上面的delete方法,我们发现抛异常情况下,部门竟然被删除了,而没有回滚。

这是因为,默认情况下,只有出现RuntimeException才回滚异常。

  • rollbackFor

该属性可以解决上面的问题,它是用于控制出现何种类型异常,回滚事务。写法是在@Transactional注解写明要回滚的异常类型。

@Transactional(rollbackFor = Exception.class)
public void delete(Integer id) throws Exception{
	//1、删除部门
	deptMapper.delete(id);
	//模拟异常
	if(true){
		throw new Exception("出错了");
	}
	
	//删除部门下的员工
	empMapper.delete(id);
}
  • propagation

事务传播行为:指定是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。

@Transactional
public void a(){
	b();
}

@Transactional
public void b(){
	
}

上述代码,我们定义了两个方法,并且在两个方法上面都加了开启事务注解。注意到,我们在a方法中调用b方法。那么,a、b两个方法用的是一个事务,还是说b方法的事务加入到a方法?
在这里插入图片描述
用法:

@Transactional(propagation =  Propagation.REQUIRED)
public void b(){
	
}

上述代码是默认的传播行为。a方法开启了事务,调用b方法。b方法默认是加入事务,即加入a方法的事务。

日志技术

  • 概述

可以将系统执行的信息,方便的记录到指定的位置(控制台、文件中、数据库中);可以随时以开关的形式控制日志的启停,无需侵入到源代码中去进行修改。

  • 日志技术的体系结构
    在这里插入图片描述
    日志接口JCL是Java官方提供的,JUL是对应接口的实现。SLF4J是一套新的日志接口,对应实现是Log4j和Logback。

因为有人对Commons Logging接口不满意,于是有了Log4j;因为对Log4j性能不满意,有了Logback。目前流行的是Logback日志框架。

  • Logback

在这里插入图片描述

  • Logback快速入门

1、pom.xml

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
</dependency>

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
</dependency>

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
</dependency>

2、在/src/main/resources目录下面创建logback核心配置文件logback.xml

<?xml version="1.0" encoding="utf-8" ?>
<!-- 级别从高到低 OFF 、 FATAL 、 ERROR 、 WARN 、 INFO 、 DEBUG 、 TRACE 、 ALL -->
<!-- 日志输出规则 根据当前ROOT 级别,日志输出时,级别高于root默认的级别时 会输出 -->
<!-- 以下 每个配置的 filter 是过滤掉输出文件里面,会出现高级别文件,依然出现低级别的日志信息,通过filter 过滤只记录本级别的日志 -->
<!-- scan 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。 -->
<!-- scanPeriod 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 动态日志级别 -->
    <jmxConfigurator />
    <!-- 定义日志文件 输出位置 -->
    <!-- <property name="log_dir" value="C:/test" />-->
    <property name="log_dir" value="C:/Users/xxx/Desktop/code/spring/spring-demo/src/main/resources/log" />
    <!-- 日志最大的历史 30天 -->
    <property name="maxHistory" value="30" />

    <!-- ConsoleAppender 控制台输出日志 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 输出流对象,默认System.out输出控制台,可以改为System.err-->
        <target>System.out</target>
        <encoder>
            <pattern>
                <!-- 设置日志输出格式 -->
                %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n
            </pattern>
        </encoder>
    </appender>

    <!-- ERROR级别日志 -->
    <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 RollingFileAppender -->
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤器,只记录WARN级别的日志 -->
        <!-- 如果日志级别等于配置级别,过滤器会根据onMatch 和 onMismatch接收或拒绝日志。 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 设置过滤级别 -->
            <level>ERROR</level>
            <!-- 用于配置符合过滤条件的操作 -->
            <onMatch>ACCEPT</onMatch>
            <!-- 用于配置不符合过滤条件的操作 -->
            <onMismatch>DENY</onMismatch>
        </filter>
        <!-- 指定日志拆分和压缩规则 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--指定压缩文件名称,确定分割文件方式 -->
            <fileNamePattern>
                ${log_dir}/error/%d{yyyy-MM-dd}/error-log.log.gz
            </fileNamePattern>
            <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件假设设置每个月滚动,且<maxHistory>是6, 则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除 -->
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>
                <!-- 设置日志输出格式 -->
                %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n
            </pattern>
        </encoder>
    </appender>

    <!-- WARN级别日志 appender -->
    <appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 过滤器,只记录WARN级别的日志 -->
        <!-- 果日志级别等于配置级别,过滤器会根据onMath 和 onMismatch接收或拒绝日志。 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 设置过滤级别 -->
            <level>WARN</level>
            <!-- 用于配置符合过滤条件的操作 -->
            <onMatch>ACCEPT</onMatch>
            <!-- 用于配置不符合过滤条件的操作 -->
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志输出位置 可相对、和绝对路径 -->
            <fileNamePattern>${log_dir}/warn/%d{yyyy-MM-dd}/warn-log.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- INFO级别日志 appender -->
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log_dir}/info/%d{yyyy-MM-dd}/info-log.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- DEBUG级别日志 appender -->
    <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log_dir}/debug/%d{yyyy-MM-dd}/debug-log.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- TRACE级别日志 appender -->
    <appender name="TRACE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>TRACE</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log_dir}/trace/%d{yyyy-MM-dd}/trace-log.log</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- root级别,可以指定level属性,ALL表示开启日志,OFF表示取消日志 -->
    <root>
        <!-- 打印debug级别日志及以上级别日志 -->
        <level value="debug" />
        <!-- 控制台输出 -->
        <appender-ref ref="console" />
        <!-- 文件输出 -->
        <appender-ref ref="ERROR" />
        <appender-ref ref="INFO" />
        <appender-ref ref="WARN" />
        <appender-ref ref="DEBUG" />
        <appender-ref ref="TRACE" />
    </root>
</configuration>

3、创建Logback框架提供的Logger对象,然后调用方法记录日志

//创建日志对象,名字随便起
public static final Logger LOGGER = LoggerFactory.getLogger("LogbackTest");


@Test
public void test2(){
	try {
		LOGGER.info("除法开始执行");
		int a = 10 / 0;
		LOGGER.info("除法执行成功");
	}catch (Exception e){
		LOGGER.error("除法执行出错了");
	}
}

运行结果:

控制台会输出:

2024-04-07 21:38:49.073 INFO  LogbackTest - 除法开始执行
2024-04-07 21:38:49.073 ERROR LogbackTest - 除法执行出错了

同时C:/Users/xxx/Desktop/code/学习/spring/spring-demo/src/main/resources/log目录下会生成info、error等其他日志文件,点开这些日志文件,会看到info日志和ERROR日志被保存下来了。

info日志
在这里插入图片描述
error日志
在这里插入图片描述
4、Logback日志级别

日志级别指的是日志信息的类型,常见日志级别(优先级依次升高)

在这里插入图片描述
为什么要学习日志级别?

如果我们的需求只是记录INFO以及比INFO更高级别的日志,可以这样改:只有大于或等于核心配置文件的日志级别的日志才会被记录

<root level="info">
    <!--控制台输出-->
    <appender-ref ref="console" />
    <!-- 文件输出 -->
    <appender-ref ref="INFO" />
    <appender-ref ref="WARN" />
    <appender-ref ref="DEBUG" />
    <appender-ref ref="TRACE" />
</root>

解决IDEA终端输出中文乱码

1、找到tomcat的配置
在这里插入图片描述
2、找到Startup/Connection,在下面配置环境变即可。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值