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 {@Overridepublic void init(ServletConfig servletConfig) throws ServletException {}@Overridepublic ServletConfig getServletConfig() {return null;}@Overridepublic 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("你好,客户端");}@Overridepublic String getServletInfo() {return null;}@Overridepublic 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");}@Overridepublic void init(ServletConfig servletConfig) throws ServletException {System.out.println("init");}@Overridepublic ServletConfig getServletConfig() {return null;}@Overridepublic 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");}@Overridepublic String getServletInfo() {return null;}@Overridepublic 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: xxxDate: 2023/11/20Time: 21:37To 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: xxxDate: 2023/11/20Time: 21:37To 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: xxxDate: 2023/11/20Time: 21:37To 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: xxxDate: 2023/11/20Time: 21:37To 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: xxxDate: 2023/11/24Time: 21:53To 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: xxxDate: 2023/11/20Time: 21:37To 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 {@Overridepublic 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 {@Overridepublic 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 {@Overrideprotected 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 {@Overrideprotected 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 {@Overridepublic 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 {@Overridepublic void requestDestroyed(ServletRequestEvent servletRequestEvent) {System.out.println("销毁了request对象");}@Overridepublic 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 {@Overridepublic void sessionCreated(HttpSessionEvent httpSessionEvent) {System.out.println("create session");}@Overridepublic 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 {@Overridepublic void contextInitialized(ServletContextEvent servletContextEvent) {System.out.println("create application");}@Overridepublic 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 {@Overridepublic void attributeAdded(ServletRequestAttributeEvent servletRequestAttributeEvent) {System.out.println("add attribute");}@Overridepublic void attributeRemoved(ServletRequestAttributeEvent servletRequestAttributeEvent) {System.out.println("remove attribute");}@Overridepublic 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 {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {this.doPost(req, resp);}@Overrideprotected 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 {@Overrideprotected 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.txtString path = req.getServletContext().getRealPath("file/copy.txt");//创建输出字节流OutputStream outputStream = new FileOutputStream(path);//创建输出字符流Writer writer = new OutputStreamWriter(outputStream);//创建BufferedWriterBufferedWriter 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 {@Overrideprotected 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 {@Overrideprotected 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 {@Overrideprotected 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 {@Overrideprotected 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", //服务端返回格式,这里服务端返回文本类型,使用textsuccess: function (data){ //请求成功var text = $("#text");//在text对应dom节点前加上datatext.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 {@Overrideprotected 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", //服务端返回格式,这里服务端返回文本类型,使用textsuccess: function (data){ //请求成功var text = $("#text");//在text对应dom节点前加上datatext.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 {@Overrideprotected 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: xxxDate: 2023/12/4Time: 20:45To 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对应的是optionvar 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);}@Overrideprotected 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封装为jsonJSONArray 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 {//创建C3P0ComboPooledDataSource 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对应的nameComboPooledDataSource 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对应的nameComboPooledDataSource 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 {@Autowiredprivate LoginCheckInterceptor loginCheckIntercepto;@Overridepublic 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,在下面配置环境变即可。