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失效时 |
ServletContextListener | web应用启动时 | 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&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,在下面配置环境变即可。