[Java Web]Cookie,Session,Filter,Listener,Thymeleaf模板

news/2024/11/22 22:39:31/

文章目录

    • Cookie
    • Session
    • Filter
    • Listener
    • 了解JSP页面与加载规则
    • 使用Thymeleaf模板引擎
      • Thymeleaf语法基础
        • 为标签添加内容
      • Thymeleaf流程控制语法
      • Thymeleaf模板布局
        • 提取重复内容
        • 参数传递
    • 探讨Tomcat类加载机制

Cookie

它可以在浏览器中保存一些信息,并且在下次请求时,请求头中会携带这些信息

我们可以编写一个测试用例来看看:

Cookie cookie = new Cookie("test", "yyds");
resp.addCookie(cookie);
resp.sendRedirect("time");

会发现多了一个cookie

image-20230523211226698

点开之后会发现有一个cookie被保存到了浏览器当中

image-20230523211449253

Domain表示作用域,只要访问该作用域内,都会携带该cookie进行请求

for (Cookie cookie : req.getCookies()) {System.out.println(cookie.getName() + ": " + cookie.getValue());
}

查看输出

image-20230523212113020

我们可以观察一下,在HttpServletResponse中添加Cookie之后,浏览器的响应头中会包含一个Set-Cookie属性,同时,在重定向之后,我们的请求头中,会携带此Cookie作为一个属性,同时,我们可以直接通过HttpServletRequest来快速获取有哪些Cookie信息。

image-20230523203845213

一个Cookie包含哪些信息:

  • name - Cookie的名称,Cookie一旦创建,名称便不可更改
  • value - Cookie的值,如果值为Unicode字符,需要为字符编码。如果为二进制数据,则需要使用BASE64编码
  • maxAge - Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒后失效。如果为负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。如果为0,表示删除该Cookie。默认为-1。
  • secure - 该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。
  • path - Cookie的使用路径。如果设置为“/sessionWeb/”,则只有contextPath为“/sessionWeb”的程序可以访问该Cookie。如果设置为“/”,则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为“/”。
  • domain - 可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”。
  • comment - 该Cookie的用处说明,浏览器显示Cookie信息的时候显示该说明。
  • version - Cookie使用的版本号。0表示遵循Netscape的Cookie规范,1表示遵循W3C的RFC 2109规范

最关键的其实是namevaluemaxAgedomain属性。

修改一下maxAge来看看失效时间:

cookie.setMaxAge(20);

设定为20秒,我们可以直接看到,响应头设定了20秒的过期时间。20秒内访问都会携带此Cookie,而超过20秒,Cookie消失。

既然了解了Cookie的作用们就可以通过使用Cookie来实现记住我功能,我们可以将用户名和密码全部保存在Cookie中,如果访问我们的首页时携带了这些Cookie,那么我们就可以直接为用户进行登陆,如果登陆成功则直接跳转到首页,如果登陆失败,则清理浏览器中的Cookie。

那么首先,我们先在前端页面的表单中添加一个勾选框:

<div><label><input type="checkbox" placeholder="记住我" name="remember-me">记住我</label>
</div>

接着,在登陆成功时进行判断,如果用户勾选了记住我,那么就讲Cookie存储到本地:

if(map.containsKey("remember-me")){   //若勾选了勾选框,那么会此表单信息Cookie cookie_username = new Cookie("username", username);cookie_username.setMaxAge(30);Cookie cookie_password = new Cookie("password", password);cookie_password.setMaxAge(30);resp.addCookie(cookie_username);resp.addCookie(cookie_password);
}

输出

image-20230524162343110

然后,我们修改一下默认的请求地址,现在一律通过http://localhost:8080/yyds/login进行登陆,那么我们需要添加GET请求的相关处理:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Cookie[] cookies = req.getCookies();if(cookies != null){String username = null;String password = null;for (Cookie cookie : cookies) {if(cookie.getName().equals("username")) username = cookie.getValue();if(cookie.getName().equals("password")) password = cookie.getValue();}if(username != null && password != null){//登陆校验try (SqlSession sqlSession = factory.openSession(true)){UserMapper mapper = sqlSession.getMapper(UserMapper.class);User user = mapper.getUser(username, password);if(user != null){resp.sendRedirect("time");return;   //直接返回}}}}req.getRequestDispatcher("/").forward(req, resp);   //正常情况还是转发给默认的Servlet帮我们返回静态页面
}

现在,30秒内都不需要登陆,访问登陆页面后,会直接跳转到time页面

现在已经离我们理想的页面越来越接近了,但是仍然有一个问题,就是我们的首页,无论是否登陆,所有人都可以访问,那么,如何才可以实现只有登陆之后才能访问呢?这就需要用到Session了

Session

由于HTTP是无连接的,那么如何能够辨别当前的请求是来自哪个用户发起的呢?Session就是用来处理这种问题的,每个用户的会话都会有一个自己的Session对象,来自同一个浏览器的所有请求,就属于同一个会话。

但是HTTP协议是无连接的呀,那Session是如何做到辨别是否来自同一个浏览器呢?Session实际上是基于Cookie实现的,前面我们了解了Cookie,我们知道,服务端可以将Cookie保存到浏览器,当浏览器下次访问时,就会附带这些Cookie信息。

Session也利用了这一点,它会给浏览器设定一个叫做JSESSIONID的Cookie,值是一个随机的排列组合,而此Cookie就对应了你属于哪一个对话,只要我们的浏览器携带此Cookie访问服务器,服务器就会通过Cookie的值进行辨别,得到对应的Session对象,因此,这样就可以追踪到底是哪一个浏览器在访问服务器。

image-20230523203837404

那么现在,我们在用户登录成功之后,将用户对象添加到Session中,只要是此用户发起的请求,我们都可以从HttpSession中读取到存储在会话中的数据:

HttpSession session = req.getSession();
session.setAttribute("user", user);

同时,如果用户没有登录就去访问首页,那么我们将发送一个重定向请求,告诉用户,需要先进行登录才可以访问:

HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
if(user == null) {resp.sendRedirect("login");return; //一定得加,不然就进行接下来的操作了
}

在访问的过程中,注意观察Cookie变化。

Session并不是永远都存在的,它有着自己的过期时间,默认时间为30分钟,若超过此时间,Session将丢失,我们可以在配置文件中修改过期时间:

<session-config><session-timeout>1</session-timeout>
</session-config>

我们也可以在代码中使用invalidate方法来使Session立即失效:

session.invalidate();

现在,通过Session,我们就可以更好地控制用户对于资源的访问,只有完成登陆的用户才有资格访问首页。

Filter

有了Session之后,就可以很好地控制用户的登陆验证了,只有授权的用户,才可以访问一些页面,但是我们需要一个一个去进行配置,还是太过复杂,能否一次性地过滤掉没有登录验证的用户呢?

过滤器相当于在所有访问前加了一堵墙,来自浏览器的所有访问请求都会首先经过过滤器,只有过滤器允许通过的请求,才可以顺利地到达对应的Servlet,而过滤器不允许的通过的请求,我们可以自由地进行控制是否进行重定向或是请求转发。并且过滤器可以添加很多个,就相当于添加了很多堵墙,我们的请求只有穿过层层阻碍,才能与Servlet相拥,像极了爱情。

image-20230523203830158

添加一个过滤器非常简单,只需要实现Filter接口,并添加@WebFilter注解即可:

@WebFilter("/*")   //路径的匹配规则和Servlet一致,这里表示匹配所有请求
public class TestFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {}
}

成功地添加了一个过滤器,那么添加一句打印语句看看,是否所有的请求都会经过此过滤器:

HttpServletRequest request = (HttpServletRequest) servletRequest;
System.out.println(request.getRequestURL());

我们发现,现在我们发起的所有请求,一律需要经过此过滤器,并且所有的请求都没有任何的响应内容。

那么如何让请求可以顺利地到达对应的Servlet,也就是说怎么让这个请求顺利通过呢?我们只需要在最后添加一句:

filterChain.doFilter(servletRequest, servletResponse);

那么这行代码是什么意思呢?

由于我们整个应用程序可能存在多个过滤器,那么这行代码的意思实际上是将此请求继续传递给下一个过滤器,当没有下一个过滤器时,才会到达对应的Servlet进行处理,我们可以再来创建一个过滤器看看效果:

@WebFilter("/*")
public class TestFilter2 implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("我是2号过滤器");filterChain.doFilter(servletRequest, servletResponse);}
}

由于过滤器的过滤顺序是按照类名的自然排序进行的,因此我们将第一个过滤器命名进行调整

在经过第一个过滤器之后,会继续前往第二个过滤器,只有两个过滤器全部经过之后,才会到达我们的Servlet中。

image-20230523203814794

实际上,当doFilter方法调用时,就会一直向下直到Servlet,在Servlet处理完成之后,又依次返回到最前面的Filter,类似于递归的结构,我们添加几个输出语句来判断一下:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("我是2号过滤器");filterChain.doFilter(servletRequest, servletResponse);System.out.println("我是2号过滤器,处理后");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("我是1号过滤器");filterChain.doFilter(servletRequest, servletResponse);System.out.println("我是1号过滤器,处理后");
}

最后验证我们的结论

image-20230524215125214

同Servlet一样,Filter也有对应的HttpFilter专用类,它针对HTTP请求进行了专门处理,因此可以直接使用HttpFilter来编写:

public abstract class HttpFilter extends GenericFilter {private static final long serialVersionUID = 7478463438252262094L;public HttpFilter() {}public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {this.doFilter((HttpServletRequest)req, (HttpServletResponse)res, chain);} else {throw new ServletException("non-HTTP request or response");}}protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {chain.doFilter(req, res);}
}

那么现在,我们就可以给我们的应用程序添加一个过滤器,用户在未登录情况下,只允许静态资源和登陆页面请求通过,登陆之后畅行无阻:

@WebFilter("/*")
public class MainFilter extends HttpFilter {@Overrideprotected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {String url = req.getRequestURL().toString();//判断是否为静态资源if(!url.endsWith(".js") && !url.endsWith(".css") && !url.endsWith(".png")){HttpSession session = req.getSession();User user = (User) session.getAttribute("user");//判断是否未登陆if(user == null && !url.endsWith("login")){res.sendRedirect("login");return;}}//交给过滤链处理chain.doFilter(req, res);}
}

现在,我们的页面已经基本完善为我们想要的样子了。


Listener

如果我们希望,在应用程序加载的时候,或是Session创建的时候,亦或是在Request对象创建的时候进行一些操作,那么这个时候,我们就可以使用监听器来实现。

image-20230523203803325

默认为我们提供了很多类型的监听器,我们这里就演示一下监听Session的创建即可:

@WebListener
public class TestListener implements HttpSessionListener {@Overridepublic void sessionCreated(HttpSessionEvent se) {System.out.println("有一个Session被创建了");}
}

了解JSP页面与加载规则

前端静态页面并没有与后端相结合,我们前端页面所需的数据全部需要单独向后端发起请求获取,并动态进行内容填充,这是一种典型的前后端分离写法,前端只负责要数据和显示数据,后端只负责处理数据和提供数据,这也是现在更流行的一种写法,让前端开发者和后端开发者各尽其责,更加专一,这才是我们所希望的开发模式。

JSP并不是需要重点学习的内容,因为它已经过时了,使用JSP会导致前后端严重耦合,因此这里只做了解即可。

JSP其实就是一种模板引擎,那么何谓模板引擎呢?顾名思义,它就是一个模板,而模板需要我们填入数据,才可以变成一个页面,也就是说,我们可以直接在前端页面中直接填写数据,填写后生成一个最终的HTML页面返回给前端。

首先我们来创建一个新的项目,项目创建成功后,删除Java目录下的内容,只留下默认创建的jsp文件,我们发现,在webapp目录中,存在一个index.jsp文件,现在我们直接运行项目,会直接访问这个JSP页面。

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head><title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>

但是我们并没有编写对应的Servlet来解析啊,那么为什么这个JSP页面会被加载呢?

实际上,我们一开始提到的两个Tomcat默认的Servlet中,一个是用于请求静态资源,还有一个就是用于处理jsp的:

<!-- The mappings for the JSP servlet --><servlet-mapping><servlet-name>jsp</servlet-name><url-pattern>*.jsp</url-pattern><url-pattern>*.jspx</url-pattern></servlet-mapping>

那么,JSP和普通HTML页面有什么区别呢,我们发现它的语法和普通HTML页面几乎一致,我们可以直接在JSP中编写Java代码,并在页面加载的时候执行,我们随便找个地方插入:

<%System.out.println("JSP页面被加载");
%>

我们发现,请求一次页面,页面就会加载一次,并执行我们填写的Java代码。也就是说,我们可以直接在此页面中执行Java代码来填充我们的数据,这样我们的页面就变成了一个动态页面,使用<%= %>来填写一个值:

<h1><%= new Date() %></h1>

现在访问我们的网站,每次都会创建一个新的Date对象,因此每次访问获取的时间都不一样,我们的网站已经算是一个动态的网站的了。

虽然这样在一定程度上上为我们提供了便利,但是这样的写法相当于整个页面既要编写前端代码,也要编写后端代码,随着项目的扩大,整个页面会显得难以阅读,并且现在都是前后端开发人员职责非常明确的,如果要编写JSP页面,那就必须要招一个既会前端也会后端的程序员,这样显然会导致不必要的开销

那么我们来研究一下,为什么JSP页面能够在加载的时候执行Java代码呢?

首先我们将此项目打包,并在Tomcat服务端中运行,生成了一个文件夹并且可以正常访问。

看work目录,发现这个里面多了一个index_jsp.javaindex_jsp.class`,反编译:

public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase  //继承自HttpServletimplements org.apache.jasper.runtime.JspSourceDependent,org.apache.jasper.runtime.JspSourceImports {...public void _jspService(final jakarta.servlet.http.HttpServletRequest request, final jakarta.servlet.http.HttpServletResponse response)throws java.io.IOException, jakarta.servlet.ServletException {if (!jakarta.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {final java.lang.String _jspx_method = request.getMethod();if ("OPTIONS".equals(_jspx_method)) {response.setHeader("Allow","GET, HEAD, POST, OPTIONS");return;}if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {response.setHeader("Allow","GET, HEAD, POST, OPTIONS");response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSP 只允许 GET、POST 或 HEAD。Jasper 还允许 OPTIONS");return;}}final jakarta.servlet.jsp.PageContext pageContext;jakarta.servlet.http.HttpSession session = null;final jakarta.servlet.ServletContext application;final jakarta.servlet.ServletConfig config;jakarta.servlet.jsp.JspWriter out = null;final java.lang.Object page = this;jakarta.servlet.jsp.JspWriter _jspx_out = null;jakarta.servlet.jsp.PageContext _jspx_page_context = null;try {response.setContentType("text/html; charset=UTF-8");pageContext = _jspxFactory.getPageContext(this, request, response,null, true, 8192, true);_jspx_page_context = pageContext;application = pageContext.getServletContext();config = pageContext.getServletConfig();session = pageContext.getSession();out = pageContext.getOut();_jspx_out = out;out.write("\n");out.write("\n");out.write("<!DOCTYPE html>\n");out.write("<html>\n");out.write("<head>\n");out.write("    <title>JSP - Hello World</title>\n");out.write("</head>\n");out.write("<body>\n");out.write("<h1>");out.print( new Date() );out.write("</h1>\n");System.out.println("JSP页面被加载");out.write("\n");out.write("<br/>\n");out.write("<a href=\"hello-servlet\">Hello Servlet</a>\n");out.write("</body>\n");out.write("</html>");} catch (java.lang.Throwable t) {if (!(t instanceof jakarta.servlet.jsp.SkipPageException)){out = _jspx_out;if (out != null && out.getBufferSize() != 0)try {if (response.isCommitted()) {out.flush();} else {out.clearBuffer();}} catch (java.io.IOException e) {}if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);else throw new ServletException(t);}} finally {_jspxFactory.releasePageContext(_jspx_page_context);}}
}

它是继承自HttpJspBase类,我们可以反编译一下jasper.jar(它在tomcat的lib目录中)来看看:

package org.apache.jasper.runtime;import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.jsp.HttpJspPage;
import java.io.IOException;
import org.apache.jasper.compiler.Localizer;public abstract class HttpJspBase extends HttpServlet implements HttpJspPage {private static final long serialVersionUID = 1L;protected HttpJspBase() {}public final void init(ServletConfig config) throws ServletException {super.init(config);this.jspInit();this._jspInit();}public String getServletInfo() {return Localizer.getMessage("jsp.engine.info", new Object[]{"3.0"});}public final void destroy() {this.jspDestroy();this._jspDestroy();}public final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this._jspService(request, response);}public void jspInit() {}public void _jspInit() {}public void jspDestroy() {}protected void _jspDestroy() {}public abstract void _jspService(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException;
}

实际上,Tomcat在加载JSP页面时,会将其动态转换为一个java类并编译为class进行加载,而生成的Java类,正是一个Servlet的子类,而页面的内容全部被编译为输出字符串,这便是JSP的加载原理,因此,JSP本质上依然是一个Servlet!

使用Thymeleaf模板引擎

虽然JSP为我们带来了便捷,但是其缺点也是显而易见的,那么有没有一种既能实现模板,又能兼顾前后端分离的模板引擎呢?

Thymeleaf(百里香叶)是一个适用于Web和独立环境的现代化服务器端Java模板引擎,:官方文档

那么它和JSP相比,好在哪里呢,我们来看官网给出的例子:

<table><thead><tr><th th:text="#{msgs.headers.name}">Name</th><th th:text="#{msgs.headers.price}">Price</th></tr></thead><tbody><tr th:each="prod: ${allProducts}"><td th:text="${prod.name}">Oranges</td><td th:text="${#numbers.formatDecimal(prod.price, 1, 2)}">0.99</td></tr></tbody>
</table>

在前端页面中填写占位符,后端提供占位符的实际值

那么我们来创建一个例子感受一下,首先还是新建一个项目,注意,在创建时,勾选Thymeleaf依赖。

首先编写一个前端页面,名称为test.html,注意,是放在resource目录下,在html标签内部添加xmlns:th="http://www.thymeleaf.org"引入Thymeleaf定义的标签属性:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><div th:text="${title}"></div>
</body>
</html>

接着编写一个Servlet作为默认页面:

@WebServlet("/index")
public class HelloServlet extends HttpServlet {TemplateEngine engine;@Overridepublic void init() throws ServletException {engine = new TemplateEngine();ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();engine.setTemplateResolver(r);}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Context context = new Context();context.setVariable("title", "我是标题");engine.process("test.html", context, resp.getWriter());}
}

我们发现,浏览器得到的页面,就是已经经过模板引擎解析好的页面,而我们的代码依然是后端处理数据,前端展示数据,因此使用Thymeleaf就能够使得当前Web应用程序的前后端划分更加清晰。

虽然Thymeleaf在一定程度上分离了前后端,但是其依然是在后台渲染HTML页面并发送给前端,并不是真正意义上的前后端分离。

Thymeleaf语法基础

首先我们看看后端部分,我们需要通过TemplateEngine对象来将模板文件渲染为最终的HTML页面:

TemplateEngine engine;
@Override
public void init() throws ServletException {engine = new TemplateEngine();//设定模板解析器决定了从哪里获取模板文件,这里直接使用ClassLoaderTemplateResolver表示加载内部资源文件ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();engine.setTemplateResolver(r);
}

由于此对象只需要创建一次,之后就可以一直使用了。接着我们来看如何使用模板引擎进行解析:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//创建上下文,上下文中包含了所有需要替换到模板中的内容Context context = new Context();context.setVariable("title", "<h1>我是标题</h1>");//通过此方法就可以直接解析模板并返回响应engine.process("test.html", context, resp.getWriter());
}

操作非常简单,只需要简单几步配置就可以实现模板的解析。接下来我们就可以在前端页面中通过上下文提供的内容,来将Java代码中的数据解析到前端页面

为标签添加内容

接着来了解Thymeleaf如何为普通的标签添加内容,比如示例中编写的:

<div th:text="${title}"></div>

使用了th:text来为当前标签指定内部文本,注意任何内容都会变成普通文本,即使传入了一个HTML代码,如果希望向内部添加一个HTML文本,可以使用th:utext属性:

<div th:utext="${title}"></div>

并且,传入的title属性,不仅仅只是一个字符串的值,而是一个字符串的引用,我们可以直接通过此引用调用相关的方法:

<div th:text="${title.toLowerCase()}"></div>

这样看来,Thymeleaf既能保持JSP为我们带来的便捷,也能兼顾前后端代码的界限划分。

除了替换文本,它还支持替换一个元素的任意属性,我们发现,th:能够拼接几乎所有的属性,一旦使用th:属性名称,那么属性的值就可以通过后端提供了,比如现在想替换一个图片的链接:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Context context = new Context();context.setVariable("url", "http://n.sinaimg.cn/sinakd20121/600/w1920h1080/20210727/a700-adf8480ff24057e04527bdfea789e788.jpg");context.setVariable("alt", "图片就是加载不出来啊");engine.process("test.html", context, resp.getWriter());
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><img width="700" th:src="${url}" th:alt="${alt}">
</body>
</html>

现在访问我们的页面,就可以看到替换后的结果了。

Thymeleaf还可以进行一些算术运算,几乎Java中的运算它都可以支持:

<div th:text="${value % 2}"></div>

同样的,它还支持三元运算:

<div th:text="${value % 2 == 0 ? 'yyds' : 'hahaha'}"></div>

多个属性也可以通过+进行拼接,就像Java中的字符串拼接一样,这里要注意一下,字符串不能直接写,要添加单引号:

<div th:text="${name}+' 我是文本 '+${value}"></div>

Thymeleaf流程控制语法

除了一些基本的操作,也可以使用Thymeleaf来处理流程控制语句,当然,不是直接编写Java代码的形式,而是添加属性

if判断语句

如果if条件满足,则此标签留下,若if条件不满足,则此标签自动被移除:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Context context = new Context();context.setVariable("eval", true);engine.process("test.html", context, resp.getWriter());
}
<div th:if="${eval}">我是判断条件标签</div>

th:if会根据其中传入的值或是条件表达式的结果进行判断,只有满足的情况下,才会显示此标签,具体的判断规则如下:

  • 如果值不是空的:
    • 如果值是布尔值并且为true
    • 如果值是一个数字,并且是非零
    • 如果值是一个字符,并且是非零
    • 如果值是一个字符串,而不是“错误”、“关闭”或“否”
    • 如果值不是布尔值、数字、字符或字符串。
  • 如果值为空,th:if将计算为false

th:if还有一个相反的属性th:unless,效果完全相反,这里就不演示了。

接着来看多分支条件判断,可以使用th:switch属性来实现:

<div th:switch="${eval}"><div th:case="1">我是1</div><div th:case="2">我是2</div><div th:case="3">我是3</div>
</div>

只不过没有default属性,但是可以使用th:case="*"来代替:

<div th:case="*">我是Default</div>

最后再来看看,它如何实现遍历,假如有一个存放书籍信息的List需要显示,那么如何快速生成一个列表呢?我们可以使用th:each来进行遍历操作:

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Context context = new Context();context.setVariable("list", Arrays.asList("1号", "2号", "3号", "5号"));engine.process("test.html", context, resp.getWriter());
}
<ul><li th:each="title : ${list}" th:text="''+${title}+''"></li>
</ul>

th:each中需要填写 “单个元素名称 : ${列表}”,这样,所有的列表项都可以使用遍历的单个元素,只要使用了th:each,都会被循环添加。因此最后生成的结果为:

<ul><li>《1号》</li><li>《2号》</li><li>《3号》</li><li>《5号》</li></ul>

获取当前循环的迭代状态

只需要在最后添加iterStat即可,从中可以获取很多信息,比如当前的顺序:

<ul><li th:each="title, iterStat : ${list}" th:text="${iterStat.index}+'.《'+${title}+''"></li>
</ul>

状态变量在th:each属性中定义,并包含以下数据:

  • 当前迭代索引,以0开头。这是index属性。
  • 当前迭代索引,以1开头。这是count属性。
  • 迭代变量中的元素总量。这是size属性。
  • 每个迭代的迭代变量。这是current属性。
  • 当前迭代是偶数还是奇数。这些是even/odd布尔属性。
  • 当前迭代是否是第一个迭代。这是first布尔属性。
  • 当前迭代是否是最后一个迭代。这是last布尔属性。

通过了解了流程控制语法,现在我们就可以很轻松地使用Thymeleaf来快速替换页面中的内容了。

Thymeleaf模板布局

在某些网页中,我们会发现,整个网站的页面,除了中间部分的内容会随着我们的页面跳转而变化外,有些部分是一直保持一个状态的,比如打开小破站,我们翻动评论或是切换视频分P的时候,变化的仅仅是对应区域的内容,实际上,其他地方的内容会无论内部页面如何跳转,都不会改变。

Thymeleaf就可以轻松实现这样的操作,只需要将不会改变的地方设定为模板布局,并在不同的页面中插入这些模板布局,就无需每个页面都去编写同样的内容了。现在我们来创建两个页面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><div class="head"><div><h1>我是标题内容,每个页面都有</h1></div><hr></div><div class="body"><ul><li th:each="title, iterStat : ${list}" th:text="${iterStat.index}+'.《'+${title}+''"></li></ul></div>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><div class="head"><div><h1>我是标题内容,每个页面都有</h1></div><hr></div><div class="body"><div>这个页面的样子是这样的</div></div>
</body>
</html>

接着将模板引擎写成工具类的形式:

public class ThymeleafUtil {private static final TemplateEngine engine;static  {engine = new TemplateEngine();ClassLoaderTemplateResolver r = new ClassLoaderTemplateResolver();engine.setTemplateResolver(r);}public static TemplateEngine getEngine() {return engine;}
}

在设置一个servlet类

@WebServlet("/index2")
public class HelloServlet2 extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Context context = new Context();ThymeleafUtil.getEngine().process("test2.html", context, resp.getWriter());}
}

现在就有两个Servlet分别对应两个页面了,但是这两个页面实际上是存在重复内容的,我们要做的就是将这些重复内容提取出来

提取重复内容

我们单独编写一个head.html来存放重复部分:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<body><div class="head" th:fragment="head-title"><div><h1>我是标题内容,每个页面都有</h1></div><hr></div>
</body>
</html>

使用下列语句替代重复部分

<div th:include="head.html::head-title"></div>

我们可以使用th:insertth:replaceth:include这三种方法来进行页面内容替换,那么th:insertth:replace(和th:include,自3.0年以来不推荐)有什么区别?

  • th:insert最简单:它只会插入指定的片段作为标签的主体。
  • th:replace实际上将标签直接替换为指定的片段。
  • th:includeth:insert相似,但它没有插入片段,而是只插入此片段的内容

参数传递

支持参数传递,比如现在希望插入二级标题,并且由我们的子页面决定:

<div class="head" th:fragment="head-title"><div><h1>我是标题内容,每个页面都有</h1><h2>我是二级标题</h2></div><hr>
</div>

稍加修改,就像JS那样添加一个参数名称:

<div class="head" th:fragment="head-title(sub)"><div><h1>我是标题内容,每个页面都有</h1><h2 th:text="${sub}"></h2></div><hr>
</div>

现在直接在替换位置添加一个参数即可:

<div th:include="head.html::head-title('这个是第1个页面的二级标题')"></div>
<div class="body"><ul><li th:each="title, iterStat : ${list}" th:text="${iterStat.index}+'.《'+${title}+''"></li></ul>
</div>

探讨Tomcat类加载机制

有关JavaWeb的内容,我们就聊到这里,在最后,我们还是来看一下Tomcat到底是如何加载和运行我们的Web应用程序的。

Tomcat服务器既然要同时运行多个Web应用程序,那么就必须要实现不同应用程序之间的隔离,也就是说,Tomcat需要分别去加载不同应用程序的类以及依赖,还必须保证应用程序之间的类无法相互访问,而传统的类加载机制无法做到这一点,同时每个应用程序都有自己的依赖,如果两个应用程序使用了同一个版本的同一个依赖,那么还有必要去重新加载吗,带着诸多问题,Tomcat服务器编写了一套自己的类加载机制。

image-20230523203730725

首先我们要知道,Tomcat本身也是一个Java程序,它要做的是去动态加载我们编写的Web应用程序中的类,而要解决以上提到的一些问题,就出现了几个新的类加载器,我们来看看各个加载器的不同之处:

  • Common ClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Web应用程序访问。
  • Catalina ClassLoader:Tomcat容器私有的类加载器,加载路径中的class对于Web应用程序不可见。
  • Shared ClassLoader:各个Web应用程序共享的类加载器,加载路径中的class对于所有Web应用程序可见,但是对于Tomcat容器不可见。
  • Webapp ClassLoader:各个Web应用程序私有的类加载器,加载路径中的class只对当前Web应用程序可见,每个Web应用程序都有一个自己的类加载器,此加载器可能存在多个实例。
  • JasperLoader:JSP类加载器,每个JSP文件都有一个自己的类加载器,也就是说,此加载器可能会存在多个实例。

通过这样进行划分,就很好地解决了我们上面所提到的问题,但是我们发现,这样的类加载机制,破坏了JDK的双亲委派机制(在JavaSE阶段讲解过),比如Webapp ClassLoader,它只加载自己的class文件,它没有将类交给父类加载器进行加载,也就是说,我们可以随意创建和JDK同包同名的类,岂不是就出问题了?

image-20230523203743398

实际上,WebAppClassLoader的加载机制是这样的:WebAppClassLoader 加载类的时候,绕开了 AppClassLoader,直接先使用 ExtClassLoader 来加载类。这样的话,如果定义了同包同名的类,就不会被加载,而如果是自己定义 的类,由于该类并不是JDK内部或是扩展类,所有不会被加载,而是再次回到WebAppClassLoader进行加载,如果还失败,再使用AppClassloader进行加载。


http://www.ppmy.cn/news/127846.html

相关文章

学什么牌子的PLC比较好?

学习PLC应当从基础内容开始学习&#xff0c;PLC的基础包括&#xff1a;电工基础&#xff08;直流电、交流电、开关按钮&#xff0c;继电器等元器件&#xff09;&#xff0c;计算机基础&#xff08;基本计算机原理&#xff0c;进制转换、字节占位等常识&#xff09;以及机械、液…

小孩护眼灯什么牌子的好?分享四款最好的台灯品牌

最近发现&#xff0c;在接送我家神兽上下学时&#xff0c;小朋友们会揉眼睛&#xff0c;眼睛始终没睁开的感觉&#xff0c;还有不少小学就戴上了眼镜&#xff0c;我深知戴眼镜&#xff0c;真的很麻烦&#xff0c;所以更加看重孩子的护眼工作。 市面上越来越多护眼灯&#xff0c…

无线蓝牙耳机什么牌子的好?好的无线蓝牙耳机推荐

无线蓝牙耳机因为携带更便捷&#xff0c;连接也越来越迅速而受到了不少用户的喜爱。在目前的耳机市场中&#xff0c;蓝牙耳机所占的比例越来越高&#xff0c;无线蓝牙耳机牌子也逐渐增多。那么&#xff0c;什么牌子的无线蓝牙耳机好&#xff1f;下面&#xff0c;我给大家盘点了…

学生台灯护眼灯哪个牌子好性价比高?学生护眼台灯十大牌子

现在学生的近视概率是越来越大了&#xff0c;据统计&#xff0c;近视人群最多在3-15岁这个年龄段&#xff0c;说明孩子开始上学了&#xff0c;就很可能因为不好的用眼习惯&#xff0c;导致近视了&#xff0c;所以买个护眼台灯真的可以预防近视的&#xff0c;现在有很多做得不错…

学生护眼灯什么牌子好推荐?五款平价实用的台灯推荐

我国近视人数越来越多&#xff0c;经常可见眼镜店&#xff0c;关于视力问题&#xff0c;很多人都归咎于幼时学习时的用眼时间过长&#xff0c;根据数据表明中国近视患者人数已达到7亿&#xff0c;而儿童及中小学生居多&#xff0c;长时间在荧光灯下学习对人眼伤害很大的&#x…

护眼灯哪些牌子好?2023热门护眼灯品牌推荐

护眼灯可以说是书桌的标配&#xff0c;不管是办公桌、书桌上&#xff0c;基本上有一盏辅助台灯&#xff0c;主要是就是补足室内环境亮度&#xff0c;市面上可以选择的护眼灯&#xff0c;可谓是琳琅满目&#xff0c;让人挑花眼&#xff0c;如果选择品质不好的台灯可能对眼睛造成…

什么牌子投影仪好?投影仪买什么牌子的好

最近几年投影仪行业发展很快&#xff0c;除了几个传统的品牌&#xff0c;几个新兴的品牌也很受关注。概括起来国内有极米、坚果、大眼橙、明基等&#xff0c;国外有索尼、松下、爱普生。备选一多就容易纠结&#xff0c;很多人问什么牌子投影仪好&#xff0c;下面就分享一下自己…

什么牌子的投影仪好家用的?什么牌子投影仪好

不知道从什么时候起&#xff0c;互联网上刮起了一股家庭影院风&#xff0c;不管是租房党还是大家庭&#xff0c;都想买一个好用的家用投影仪&#xff0c;情侣周末可以一起在家看电影&#xff0c;老人小孩也可以看看动画片和电视节目什么的&#xff0c;因为投影仪运用了漫反射原…