Java经典框架之SpringBoot

SpringBoot

Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机,Java 仍是企业和开发人员的首选开发平台。

课程内容的介绍

  1. SpringBoot基础
  2. SpringBoot高级

一、SpringBoot基础

1. SpringBoot概念

官网:https://spring.io/
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
SpringBoot是由Pivotal团队在2013年开始研发、2014年4月发布第一个版本的全新开源的轻量级框架。它基于Spring4.0设计,不仅继承了Spring框架原有的优秀特性,而且还通过简化配置来进一步简化了Spring应用的整个搭建和开发过程。另外SpringBoot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。
SpringBoot所具备的特征有:
(1)可以创建独立的Spring应用程序,并且基于其Maven或Gradle插件,可以创建可执行的JARs和WARs;
(2)内嵌Tomcat或Jetty等Servlet容器;
(3)提供自动配置的"starter"项目对象模型(POMS)以简化Maven配置;
(4)尽可能自动配置Spring容器;
(5)提供准备好的特性,如指标、健康检查和外部化配置;
(6)绝对没有代码生成,不需要XML配置。
SpringBoot框架中还有两个非常重要的策略:开箱即用和约定优于配置。开箱即用,Outofbox,是指在开发过程中,通过在MAVEN项目的pom文件中添加相关依赖包,然后使用对应注解来代替繁琐的XML配置文件以管理对象的生命周期。这个特点使得开发人员摆脱了复杂的配置工作以及依赖的管理工作,更加专注于业务逻辑。约定优于配置,Convention over configuration,是一种由SpringBoot本身来配置目标结构,由开发者在结构中添加信息的软件设计范式。这一特点虽降低了部分灵活性,增加了BUG定位的复杂性,但减少了开发人员需要做出决定的数量,同时减少了大量的XML配置,并且可以将代码编译、测试和打包等工作自动化。
SpringBoot应用系统开发模板的基本架构设计从前端到后台进行说明:前端常使用模板引擎,主要有FreeMarker和Thymeleaf,它们都是用Java语言编写的,渲染模板并输出相应文本,使得界面的设计与应用的逻辑分离,同时前端开发还会使用到Bootstrap、AngularJS、JQuery等;在浏览器的数据传输格式上采用Json,非xml,同时提供RESTfulAPI;SpringMVC框架用于数据到达服务器后处理请求;到数据访问层主要有Hibernate、MyBatis、JPA等持久层框架;数据库常用MySQL;开发工具推荐IntelliJIDEA。

2. SpringBoot项目构建

SpringBoot项目的构建方式本身是非常简单的,实现的方式也有多种。

2.1 手动创建

我们通过基本的Maven项目来手动配置一个SpringBoot。

2.1.1 创建MAVEN项目

创建一个普通的Maven项目即可。

2.1.2 添加依赖

创建一个SpringBoot的Web项目,我们需要添加对应的依赖。

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.bobo</groupId>
    <artifactId>SpringBootDemo01</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- 1.添加SpringBoot的依赖 -->
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.3.8.RELEASE</version>
    </parent>

    <dependencies>
        <!-- 2. 添加SpringMVC的支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>


</project>
2.1.3 创建启动类

我们要启动当前项目需要创建一个Java启动类。

java 复制代码
package com.bobo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * SpringBoot项目的启动类
 */
@SpringBootApplication
public class AppStart {

    /**
     * 程序启动的入口
     * @param args
     */
    public static void main(String[] args) {
        SpringApplication.run(AppStart.class,args);
    }
}
2.1.4 启动程序

执行我们的主方法即可。

访问出现404,说明服务启动成功,只是请求访问的资源不存在。

2.2 在线构建

我们也可以通过SpringBoot提供的在线地址创建我们的SpringBoot项:https://start.spring.io/

在线生成我们的SpringBoot项目,解压后可以直接导入。

2.3 IDEA直接创建

IDEA工具可以直接通过在线创建的工具来直接生成,帮助我们简化了下载解压缩的步骤。




到此创建完成。

3. SpringBoot基本使用
3.1 自定义控制器

我们创建的是一个基于SpringBoot的WEB项目,那么怎么处理客户端提交的请求呢?这时我们可以直接在启动器所在的子目录下创建对应的Controller即可。

java 复制代码
package com.bobo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/*@Controller
@ResponseBody*/
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        System.out.println("hello ...);
        return "Hello ...";
    }
}

访问即可

为什么能够扫描到这个@Controller注解,根本原因是在我们的启动器中的那个@SpringBootApplication注解,这个注解本身是一个组合注解。

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 以上四个是JAVA中提供的元注解
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
3.2 静态资源

在SpringBoot项目中默认的静态资源【html,css,js,图片....】是放置在resource/static目录中。

效果

3.3 定制Banner

如果我们想要修改服务启动时的那个banner图标,我们只需要在resource目录下创建一个banner.txt文件,然后将我们要显示的信息写入即可http://patorjk.com/software/taag/#p=display&f=Graffiti&t=HELLO


当然我们也可以不显示Banner信息。

java 复制代码
package com.bobo;

import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * @SpringBootApplication 组合注解
 * @ComponentScan 可以直接扫描路径
 *                如果没有指定要扫描的特定的路径,
 *                那么默认的是会把当前注解所在的类的包及其子包作为扫描路径
 */
@SpringBootApplication
public class SpringBootDemo03Application {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(SpringBootDemo03Application.class);
        app.setBannerMode(Banner.Mode.OFF); // 关闭掉Banner
        app.run(args);
    }

}
3.4 属性文件

在resource目录下的application.properties文件。

3.4.1 默认设置

我们可以通过application.properties文件来修改系统默认的属性,比如修改Tomcat相关配置信息。

XML 复制代码
server.port=8082
server.servlet.context-path=/springboot
3.4.2 自定义属性

我们可以在application.properties文件中自定义属性,供我们在代码中使用。
自定义属性

XML 复制代码
# 默认属性修改
server.port=8082
server.servlet.context-path=/springboot
# 自定义属性
user.userName=admin
user.realName=波波
user.address=湖南长沙

获取自定义属性

java 复制代码
package com.bobo.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/*@Controller
@ResponseBody*/
@RestController
public class HelloController {

    @Value(value = "${user.userName}")
    private String userName;
    @Value(value = "${user.realName}")
    private String realName;
    @Value(value = "${user.address}")
    private String address;

    @RequestMapping("/hello")
    public String hello(){
        System.out.println("hello ..."+ userName + "  " + realName + " " + address);
        return "Hello ...";
    }
}

出现乱码的情况

解决方法

修改了乱码后的效果

3.4.3 yml文件

yml是我们在配置系统属性或者自定义属性的另外一种方式。

java 复制代码
user.hello.username=a
user.hello.password=123
user.hello.address=cs
user.hello.age=1

改成yml

java 复制代码
user:
    hello:
        username:a
        password:123
        address:cs
        age:1
3.5 日志

SpringBoot中支持 JavaUtil Logging, Log4J, Log4J2和Logback作为日志框架,而在SpringBoot中默认支持的是Logback作为日志框架。
简单配置

java 复制代码
# 日志配置
#logging.file.path=d:/tools/log
logging.file.name=d:/tools/log/log.log
logging.level.org.springframework.web=DEBUG


引入日志文件的扩展配置文件

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--日志文件主目录:这里${user.home}为当前服务器用户主目录-->
    <property name="LOG_HOME" value="${user.home}/log"/>
    <!--日志文件名称:这里spring.application.name表示工程名称-->
    <springProperty scope="context" name="APP_NAME" source="spring.application.name"/>

    <!--默认配置-->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <!--配置控制台(Console)-->
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <!--配置日志文件(File)-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--设置策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件路径:这里%d{yyyyMMdd}表示按天分类日志-->
            <FileNamePattern>${LOG_HOME}/%d{yyyyMMdd}/${APP_NAME}.log</FileNamePattern>
            <!--日志保留天数-->
            <MaxHistory>15</MaxHistory>
        </rollingPolicy>
        <!--设置格式-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <!-- 或者使用默认配置 -->
            <!--<pattern>${FILE_LOG_PATTERN}</pattern>-->
            <charset>utf8</charset>
        </encoder>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>100MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- 多环境配置 按照active profile选择分支 -->
    <springProfile name="dev">
        <!--root节点 全局日志级别,用来指定最基础的日志输出级别-->
        <root level="DEBUG">
            <appender-ref ref="FILE"/>
            <appender-ref ref="CONSOLE"/>
        </root>

        <!-- 子节点向上级传递 局部日志级别-->
        <logger level="WARN" name="org.springframework"/>
        <logger level="WARN" name="com.netflix"/>
        <logger level="DEBUG" name="org.hibernate.SQL"/>
    </springProfile>
    <springProfile name="prod">
        <!--root节点 全局日志级别,用来指定最基础的日志输出级别-->
        <root level="INFO">
            <appender-ref ref="FILE"/>
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
</configuration>

引入

3.6 Profile

项目开发中会出现开发环境的切换,为了更好的处理我们可以通过Profile来实现,之前在Spring的阶段就已经给大家介绍过了Profile,但是实现相对复杂了点,在SpringBoot中也提供了对Profile的支持,而且更加简化,创建对应的属性文件。
开发环境

XML 复制代码
user.host=192.168.100.120

生产环境

XML 复制代码
user.host=192.168.111.123

文件名称的命名规则是 application-环境.properties

要让哪个文件生效,我们只需要在application.properties文件中指定即可。

测试效果

3.7 静态资源文件

在SpringBoot项目中默认的存放路径是在static目录下,但是实际开发的时候有可能我们需要调整资源的目录。
在main目录下创建一个webapp目录 设置类型为 ResourceRoot。

可以直接访问

自定义目录
有些情况下我们需要将特定的目录作为我们存放静态资源文件的目录。

XML 复制代码
## 设置自定义的路径
spring.mvc.static-path-pattern=/**
## 覆盖掉默认的配置录
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,class path:/sfile/


3.8 Servlet操作

我们在项目开发过程中可以要碰到直接操作Servlet的情况,这时我们应该怎么去实现。

3.8.1 Servlet

第一种方式
定义Servlet

java 复制代码
package com.bobo.servlet;

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.io.PrintWriter;

@WebServlet(name="firstServlet",urlPatterns = "/first")
public class FirstServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("--firstServlet -- doGet 方法");
        PrintWriter writer = resp.getWriter();
        writer.write("success");
        writer.flush();
        writer.close();
    }
}

在启动类中添加扫描的注解。

java 复制代码
@SpringBootApplication
// 在SpringBoot项目启动的时候会扫描 @WebServlet注解
@ServletComponentScan
public class SpringbootDemo06Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemo06Application.class, args);
    }
}

测试访问

第二种方式
创建Servlet,不用添加WebServlet注解。

java 复制代码
package com.bobo.servlet;

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.io.PrintWriter;


public class SecondServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("--secondServlet -- doGet 方法");
        PrintWriter writer = resp.getWriter();
        writer.write("success");
        writer.flush();
        writer.close();
    }
}

在启动类中注入ServletRegistrationBean对象。

java 复制代码
@SpringBootApplication
// 在SpringBoot项目启动的时候会扫描 @WebServlet注解
@ServletComponentScan
public class SpringbootDemo06Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemo06Application.class, args);
    }

    @Bean
    public ServletRegistrationBean servletRegistrationBean(){
        ServletRegistrationBean bean = new ServletRegistrationBean(new SecondServlet());
        bean.addUrlMappings("/second");
        return bean;
    }
}

测试

3.8.2 Filter

第一种方式
创建过滤器

java 复制代码
@WebFilter(urlPatterns = "/first")
public class FirstFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("FirstFilter before");
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("FirstFilter end");
    }
}

在启动器中添加注解。

测试

第二种方式
在该过滤器中我们不用添加对应的注解。

java 复制代码
package com.bobo.filter;

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


public class SecondFilter implements Filter {

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

启动器中注入 注册器。

java 复制代码
    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean bean = new FilterRegistrationBean(new SecondFilter());
        bean.addUrlPatterns("/second");
        return bean;
    }

测试

3.8.3 Listener

第一种方式

java 复制代码
package com.bobo.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

@WebListener
public class FirstListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("FirstListener ... 初始化");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("FirstListener ... 销毁");
    }
}

启动器中注解

测试效果

第二种方式

java 复制代码
package com.bobo.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;


public class SecondListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("SecondListener ... 初始化");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("SecondListener ... 销毁");
    }
}
java 复制代码
    @Bean
    public ServletListenerRegistrationBean servletListenerRegistrationBean(){
        return new ServletListenerRegistrationBean(new SecondListener());
    }

测试

3.9 文件上传
3.9.1 表单页面
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户管理</title>
</head>
<body>

    <h1>文件上传案例:</h1>
    <form action="/user/upload" method="post" enctype="multipart/form-data" >
        <label>账号:</label><input type="text" name="username"><br>
        <label>头像:</label><input type="file" name="upload"><br>
        <input type="submit" value="提交">
    </form>
</body>
</html>
3.9.2 控制器
java 复制代码
package com.bobo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

@RestController
@RequestMapping("/user")
public class UserContoller {

    @RequestMapping("/upload")
    public String fileUpload(String username, MultipartFile upload) throws IOException {
        System.out.println(username + " " + upload.getOriginalFilename());
        upload.transferTo(new File("d:/tools/",upload.getOriginalFilename()));
        return "success";
    }


}
3.9.3 属性文件设置
XML 复制代码
server.port=8082

spring.servlet.multipart.enabled=true
# 设置单个文件上传的大小
spring.servlet.multipart.max-file-size=20MB
# 设置一次请求上传文件的总的大小
spring.servlet.multipart.max-request-size=200MB
3.9.4 测试



上传成功的文件

4.SpringBoot基本应用
4.1 Freemaker

FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
http://freemarker.foofun.cn/
JavaEE中的两种开发方式
前后端不分离
要求程序员要掌握js,为了简化页面开发,引入页面模板,页面模板整体上来说又可以分为两大类。
前端模板
前端模板就是后缀为html的模板,代表就是Thymeleaf,这种模板有一个好处就是不需要服务端解析就能直接在浏览器中打开。
后端模板
必须经过服务端解析才能被浏览器展示出来的模板。
JSP
Freemarker
velocity
前后端分离
前后端分离的时候,后端纯粹只是接口,没有任何页面。所有的页面由前端完成,前端会使用相关的模板。
Vue
AngularJS
React

4.2 整合Freemaker
4.2.1 添加依赖
XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
4.2.2 配置

我们在属性文件中设置视图解析器的前后缀。

java 复制代码
spring.freemarker.suffix=.ftl
4.2.3 创建Freemaker文件

然后我们在系统的模板文件中创建Freemaker文件,注意该文件为一个后缀为 .ftl 的文件。

XML 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>Hello Freemark ...</h1>
    </body>
</html>
4.2.4 控制器

因为在 template 目录下的文件是没法直接访问的,而且我们也需要先在服务端获取数据绑定数据后再在页面模板文件中呈现,所以请求先到控制器,然后通过模板引擎解析模板文件生成具体的HTML页面响应客户。

java 复制代码
@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/query")
    public String query(){
        System.out.println("query ....");
        return "user";
    }
}
4.2.5 测试

直接启动服务,访问看效果。


这就表示整合成功了。

4.3 Freemaker的基本应用

接下来介绍下如果在Freemaker中绑定数据。

4.3.1 绑定单个数据

我们在Model中绑定的是单个数据,怎么在ftl文件中绑定呢?

java 复制代码
    /**
     * 基本数据类型
     * 自定义数据类型
     * 数据容器
     * @param model
     * @return
     */
    @RequestMapping("/query")
    public String query(Model model){
        System.out.println("query ....");
        model.addAttribute("userName","波波老师");
        model.addAttribute("age",18);
        model.addAttribute("address","湖南长沙");
        model.addAttribute("flag",true);
        model.addAttribute("birth",new Date());
        return "user";
    }

字符和数字类型我们可以通过EL表达式直接取出来。

html 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>Hello Freemark ...</h1>
        ${userName}<br>
        ${age}<br>
    </body>
</html>

boolean不能直接转换为string类型。

这时我们要通过内部的转换函数来处理。

html 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>Hello Freemark ...</h1>
        ${userName}<br>
        ${age}<br>
        ${address}<br>
        ${flag?string("真","假")}<br>
    </body>
</html>
html 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>Hello Freemark ...</h1>
        ${userName}<br>
        ${age}<br>
        ${address}<br>
        ${flag?string("真","假")}<br>
        ${birth}<br>
    </body>
</html>

时间类型也需要转换。

java 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>Hello Freemark ...</h1>
        ${userName}<br>
        ${age}<br>
        ${address}<br>
        ${flag?string("真","假")}<br>
        ${birth?string("yyyy-MM-dd")}<br>
    </body>
</html>
4.3.2 单个数据处理

我们服务端绑定的单个数据,比如字符串或者数字,我们可能需要对这些数据做出调整,比如数字要四舍五入,字符串我们需要截取等操作。

html 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>Hello Freemark ...</h1>
        ${userName}<br>
        ${age}<br>
        ${address}<br>
        ${flag?string("真","假")}<br>
        ${birth?string("yyyy-MM-dd")}<br>
        <hr>
        <#-- 注释符 -->
        <#assign x=3.1415>
        <#assign y=6>
        <!--
            mN:小数部分最小N位
            MN:小数部分最大N位
         -->
        x=${x}<br>
        y=${y}<br>
        #{x;M2}<br><!-- 3.14 -->
        #{x;m2}<br><!-- 3.14 -->
        #{y;M2}<br><!-- 6 -->
        #{y;m2}<br><!-- 6.00 -->
    <body>
</html>


字符串拼接处理

html 复制代码
<#assign hello="hello freemarker" >
<#-- 字符串拼接 -->
HELLO-${hello}<br>
<#-- EL表达式中的常量表示 -->
${'HELLO|'+hello}<br>
<#-- 常量中使用数据 -->
${'HELLO*${hello}'}<br>
${userName}----${hello}<br>
${userName+'-->' + hello}<br>


字符串截取

html 复制代码
${hello}<br>
${hello[1]}<br>
${hello[4]}<br>
${hello[1..6]}<br>
${hello[3..]}<br>
4.3.3 自定义对象
java 复制代码
@RequestMapping("/query1")
public String query1(Model model){
    User user = new User(666,"admin","123456");
    model.addAttribute("user",user);
    return "user1";
}
html 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <#-- 自定义对象 -->
        ${user.id}<br>
        --> <#--${user[id]}<br>-->
        ${user.userName}<br>
        -->${user['userName']}<br>
        ${user.password}<br>
    </body>
</html>
4.3.4 集合对象
java 复制代码
    @RequestMapping("/query1")
    public String query1(Model model){
        User user = new User(666,"admin","123456");
        model.addAttribute("user",user);
        Map<String,Object> map = new HashMap<>();
        map.put("user",user);
        List list = Arrays.asList("张三","李四","王五");
        List list1 = Arrays.asList("1111","2222","3333");
        model.addAttribute("list",list);
        model.addAttribute("list1",list1);
        model.addAttribute("map",map);
        return "user1";
    }
4.3.5 算数运算

算数运算包含基本的四则运算和求模运算,运算符有:

java 复制代码
加法: +
减法: -
乘法: *
除法: /
求模 (求余): %
java 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>算术运算符:</h1><br>
        ${99+100*30}<br>
        ${99/7}<br>
        ${(99/7)?int}<br>
        ${55%3}<br>
    </body>
</html>
4.3.6 比较运算符
4.3.7 逻辑操作

常用的逻辑操作符:

html 复制代码
逻辑 或: ||
逻辑 与: &&
逻辑 非: !

逻辑操作符仅仅在布尔值之间有效,若用在其他类型将会产生错误导致模板执行中止。

4.3.8 内置函数

内建函数就像FreeMarker在对象中添加的方法一样。 要防止和实际方法和其它子变量的命名冲突,则不能使用点 (.),这里使用问号 (?)来和父对象分隔开。 比如,想要保证 path 有起始的 / ,那么可以这么来写: path?ensure_starts_with('/')。 path 后的Java对象(通常就是 String) 并没有这样的方法,这是FreeMarker添加的。为了简洁,如果方法没有参数, 那么就可以忽略 (),比如想要获取 path 的长度,就可以写作:path?length, 而不是 path?length()。

更多内置函数见此:http://freemarker.foofun.cn/ref_builtins.html

html 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
    <h1>算术运算符:</h1><br>
        ${99+100*30}<br>
        ${99/7}<br>
        ${(99/7)?int}<br>
        ${55%3}<br>
    <h1>内建函数:</h1>
    <#assign hello="Hello FreeMarker">
    <#assign page="<span style='color:red'>HELLO</span>">
    ${hello}<br>
    ${page}<br>
    ${page?html}<br>
    ${hello?upper_case}<br>
    ${hello?lower_case}<br>
    ${now?date}<br>
    ${now?datetime}<br>
    ${now?time}<br>
    </body>
</html>
4.3.9 分支和循环

if语句,switch语句已经循环语句,基本的语法格式和我们在Java中使用的是一样的。我们只需要注意下在具体的使用格式上。

html 复制代码
<html>
    <head>
        <title>Freemaker</title>
        <meta charset="UTF-8">
    </head>
    <body>
    <h1>算术运算符:</h1><br>
        ${99+100*30}<br>
        ${99/7}<br>
        ${(99/7)?int}<br>
        ${55%3}<br>
    <h1>内建函数:</h1>
    <#assign hello="Hello FreeMarker">
    <#assign page="<span style='color:red'>HELLO</span>">
    ${hello}<br>
    ${page}<br>
    ${page?html}<br>
    ${hello?upper_case}<br>
    ${hello?lower_case}<br>
    ${now?date}<br>
    ${now?datetime}<br>
    ${now?time}<br>
    <hr>
        <#assign age = 18 >
        <#if age == 18>
            等于18
            <#elseif age gt 18 >
            大于18
            <#else >
            小于18
        </#if>

        null的判断:<br>
        <#assign  mypage="a">
    <#-- ?? 检测值是否存在 -->
        <#if mypage??>
            mypage存在
            <#else >
            mypage不存在
        </#if>
        <br>
        <#assign i=3>
        <#switch i>
            <#case 1>
               ok
                <#break >
            <#case 2>
               ok2
                <#break >
            <#case 3>
                ok3
                <#break >
            <#default >
                ok4

        </#switch>

    <#list list as obj>
        <#if obj=='李四'>
            <#break >
        </#if>
        ${obj}<br>
    </#list>
    <#assign  aaa=555>
    <#-- !的使用--><br>
    ${aaa!"666"}<!-- 如果aaa存在就显示aaa本来的值,如果aaa不存在就显示666 --><br>

    </body>
</html>
5.综合案例

前面介绍了Freemaker,这节介绍SpringBoot整合MyBatis,同时结合Freemaker展现数据。

5.1 项目创建

添加相关的依赖

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bobo</groupId>
    <artifactId>springboot-demo09</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-demo09</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.14</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

添加相关的配置文件

XML 复制代码
server.port=8082

# 配置JDBC的相关信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/logistics?
characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456

# 配置连接池
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 配置MyBatis的package 设置别名
mybatis.type-aliases-package=com.bobo.pojo

创建实体对象

java 复制代码
package com.bobo.pojo;

public class User {
    private String user_id    ;
    private String user_name  ;
    private String real_name  ;
    private String password   ;
    private String email      ;
    private String phone      ;
    private String u1         ;
    private String u2         ;

    public String getUser_id() {
        return user_id;
    }

    public void setUser_id(String user_id) {
        this.user_id = user_id;
    }

    public String getUser_name() {
        return user_name;
    }

    public void setUser_name(String user_name) {
        this.user_name = user_name;
    }

    public String getReal_name() {
        return real_name;
    }

    public void setReal_name(String real_name) {
        this.real_name = real_name;
    }

    public String getPassword() {
        return password;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getU1() {
        return u1;
    }

    public void setU1(String u1) {
        this.u1 = u1;
    }

    public String getU2() {
        return u2;
    }

    public void setU2(String u2) {
        this.u2 = u2;
    }
}
5.2 查询用户信息

创建接口。

java 复制代码
package com.bobo.mapper;

import com.bobo.pojo.User;

import java.util.List;

public interface UserMapper {

    List<User> query();

}

创建映射文件。

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bobo.mapper.UserMapper">
    <select id="query" resultType="user">
        select * from t_user
    </select>
</mapper>

属性文件中添加Mapper映射文件的路径。

创建Service

java 复制代码
package com.bobo.service;

import com.bobo.pojo.User;

import java.util.List;

public interface IUserService {

    List<User> query();

}
java 复制代码
package com.bobo.service.impl;

import com.bobo.mapper.UserMapper;
import com.bobo.pojo.User;
import com.bobo.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserMapper mapper;

    @Override
    public List<User> query() {
        return mapper.query();
    }

}

创建控制器

java 复制代码
package com.bobo.controller;

import com.bobo.pojo.User;
import com.bobo.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService service;

    @RequestMapping("/query")
    public String query(Model model){
        List<User> list = service.query();
        model.addAttribute("list",list);
        return "/user";
    }

}

属性文件中配置Freemaker的后缀。

创建Freemaker模板文件,并且展示数据。

html 复制代码
<html>
    <head>
        <title>用户管理</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>用户管理</h1>
        <table>
            <tr>
                <th>编号</th>
                <th>账号</th>
                <th>姓名</th>
                <th>邮箱</th>
                <th>电话</th>
                <th>操作</th>
            </tr>
            <#list list as user>
                <tr>
                    <td>${user.user_id}</td>
                    <td>${user.user_name}</td>
                    <td>${user.real_name!""}</td>
                    <td>${user.email!""}</td>
                    <td>${user.phone!""}</td>
                    <td>...</td>
                </tr>
            </#list>
        </table>
    </body>
</html>

启动操作之前我们需要添加 MyBatis接口的扫描路径。

访问测试

5.3 添加用户
html 复制代码
<html>
    <head>
        <title>用户管理</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>用户管理</h1>
        <form action="/user/userUpdate" method="post" >
            <input type="hidden" name="user_id" value="${user.user_id}">
            <label>账号</label><input type="text" name="user_name" value="${user.user_name}"><br>
            <label>姓名</label><input type="text" name="real_name" value="${user.real_name!""}"><br>
            <label>邮箱</label><input type="text" name="email" value="${user.email!""}"><br>
            <label>电话</label><input type="text" name="phone" value="${user.phone!""}"><br>
            <input type="submit" value="提交">
        </form>
    </body>
</html>
5.4 更新用户
html 复制代码
<html>
    <head>
        <title>用户管理</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>用户管理</h1>
        <form action="/user/userUpdate" method="post" >
            <input type="hidden" name="user_id" value="${user.user_id}">
            <label>账号</label><input type="text" name="user_name"value="${user.user_name}"><br>
            <label>姓名</label><input type="text" name="real_name"value="${user.real_name!""}"><br>
            <label>邮箱</label><input type="text" name="email"value="${user.email!""}"><br>
            <label>电话</label><input type="text" name="phone"value="${user.phone!""}"><br>
            <input type="submit" value="提交">
        </form>
    </body>
</html>
5.5 删除用户
html 复制代码
<html>
    <head>
        <title>用户管理</title>
        <meta charset="UTF-8">
    </head>
    <body>
        <h1>用户管理</h1>
        <h2>
            <a href="/user/dispatchUpdate">添加用户</a>
        </h2>
        <table>
            <tr>
                <th>编号</th>
                <th>账号</th>
                <th>姓名</th>
                <th>邮箱</th>
                <th>电话</th>
                <th>操作</th>
            </tr>
            <#list list as user>
                <tr>
                    <td>${user.user_id}</td>
                    <td>${user.user_name}</td>
                    <td>${user.real_name!""}</td>
                    <td>${user.email!""}</td>
                    <td>${user.phone!""}</td>
                    <td>
                        <a href="/user/dispatchUpdate?id=${user.user_id}">更新</a>
                        <a href="/user/deleteUser?id=${user.user_id}">删除</a>
                    </td>
                </tr>
            </#list>
        </table>
    </body>
</html>
6. Thymeleaf

Thymeleaf是SpringBoot中推荐使用的前端模板框架。所以比较重要。

6.1 SpringBoot整合

创建一个SpringBoot项目,然后添加对应的依赖。

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bobo</groupId>
    <artifactId>springboot-demo10</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-demo10</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- 使用Thymeleaf需要添加的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

创建Thymeleaf文件,Thymeleaf的后缀就是html,我在template目录下直接创建一个html页面即可,但是为了能够使用Thymeleaf中的标签提示,我们添加对应的xmlns即可。

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf介绍</title>
</head>
<body>
    <h1>Hello Thymeleaf</h1>

</body>
</html>

添加跳转的控制器。

java 复制代码
@Controller
public class UserController {

    @RequestMapping("/hello")
    public String hello(){
        System.out.println("hello ....");
        return "/user";
    }
}

启动服务测试

访问成功,说明整合搞定。

6.2 Thymeleaf基本使用

Thymeleaf表达式只能放置在Thymeleaf的自定义属性中(html标签中)。

6.2.1 变量输出

控制器中绑定数据。

java 复制代码
@RequestMapping("/hello")
public String hello(Model model){
    System.out.println("hello ....");
    model.addAttribute("hello","Hello Thymeleaf");
    model.addAttribute("msg","hahaha");
    model.addAttribute("now",new Date());
    model.addAttribute("flag",true);
    model.addAttribute("age",18);
    return "/user";
}

模板文件

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf介绍</title>
</head>
<body>
    <h1>Hello Thymeleaf</h1>
    <label th:text="hello"></label><br>
    <label th:text="${hello}"></label><br>
    <label th:text="${now}"></label><br>
    <label th:text="${flag}"></label><br>
    <label th:text="${age}"></label><br>
    <h2>th:value的使用</h2>
    <input type="text" value="aaa"><br>
    <input type="text" th:value="${msg}"><br>
</body>
</html>

显示效果

6.2.2 内置函数

我们通过上面的案例发现显示Model中的数据很方便,但是显示的数据的格式可能不满足我们的需求,这时我们需要调整就需要借助内置的函数来帮助我们实现,我们主要介绍字符串和时间相关的函数。
注意点

  1. 调用内置函数对象一定要使用#
  2. 大部分的内置函数都以 s 结尾, 比如 strings numbers dates
    字符串的处理
html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf介绍</title>
</head>
<body>
    <h1>string类型介绍</h1>
    hello:<span th:text="${hello}"></span><br>
    hello是否为空:<span th:text="${#strings.isEmpty(hello)}"></span><br>
    hello字符串是否包含"th":<span th:text="${#strings.contains(hello,'th')}">
</span><br>
    hello字符串是否包含"Th":<span th:text="${#strings.contains(hello,'Th')}">
</span><br>
    hello以H开头:<span th:text="${#strings.startsWith(hello,'H')}"></span><br>
    hello以a开头:<span th:text="${#strings.startsWith(hello,'a')}"></span><br>
    hello以H结尾:<span th:text="${#strings.endsWith(hello,'H')}"></span><br>
    hello以a结尾:<span th:text="${#strings.endsWith(hello,'a')}"></span><br>
    hello的长度:<span th:text="${#strings.length(hello)}"></span><br>
    hello都大写:<span th:text="${#strings.toUpperCase(hello)}"></span><br>
    hello都小写:<span th:text="${#strings.toLowerCase(hello)}"></span><br>
</body>
</html>


日期时间类型的处理

html 复制代码
<h1>日期时间处理</h1>
时间:<span th:text="${now}"></span><br>
时间:<span th:text="${#dates.format(now)}"></span><br>
时间:<span th:text="${#dates.format(now,'yy/MM/dd')}"></span><br>
时间:<span th:text="${#dates.format(now,'yy/MM/dd hh:ss:mm')}"></span><br>
时间:<span th:text="${#dates.format(now,'yy/MM/dd HH:ss:mm')}"></span><br>
年份:<span th:text="${#dates.year(now)}"></span><br>
月份:<span th:text="${#dates.month(now)}"></span><br>
日期:<span th:text="${#dates.day(now)}"></span><br>
本周的第几天:<span th:text="${#dates.dayOfWeek(now)}"></span><br>
小时:<span th:text="${#dates.hour(now)}"></span><br>
6.2.3 条件判断
html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf介绍</title>
</head>
<body>

    <h1>条件判断</h1>
    <h2>if语句</h2>
    <span th:if="${sex} == '男'">男</span>
    <span th:unless="${sex} =='男'">女</span>
    <br>
    <!-- and or not -->
    <span th:if="${flag or false}">
        or的使用11
    </span>
    <span th:unless="${flag or false}">
        or的使用12
    </span>
    <br>
    <span th:if="${flag and false}">
        and的使用21
    </span>
    <span th:unless="${flag and false}">
        and的使用22
    </span>
    <br>
    <span th:if="${not flag}">
        not的使用11
    </span>
    <span th:unless="${not flag}">
        not的使用22
    </span>
    <br>

    <!-- 三木运算符 -->
    <span th:text="true?'A':'B'"></span><br>

    <!-- switch语句 -->
    <hr>
    <div th:switch="${age}">
        <div th:case="17">
            17岁
        </div>
        <div th:case="18">
            18岁
        </div>
        <div th:case="19">
            19岁
        </div>
        <div th:case="*">其他...</div>
    </div>
</body>
</html>
6.2.4 循环语句
html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf介绍</title>
</head>
<body>

    <h1>循环判断</h1>
    <div th:each="c : ${list1}">
        <span th:text="${c}"></span><br>
    </div>
    <hr>
    <div th:each="user : ${list2}">
        <span th:text="${user.id}"></span>&nbsp;&nbsp;
        <span th:text="${user.userName}"></span>&nbsp;&nbsp;
        <span th:text="${user.address}"></span><br>
    </div>
    <hr>
    <div th:each="m : ${map}">
        <!-- 每次循环获取的是一个KV对 -->
        <span th:text="${m.getKey() + ':' + m.getValue().getId()}"></span>
        <span th:text="${m.getKey() + ':' + m.getValue().getUserName()}"></span>
        <span th:text="${m.getKey() + ':' + m.getValue().getAddress()}"></span>
    </div>
    <hr>
    <div th:each="user,iter : ${list2}">
        <span th:text="${iter.count}"></span>&nbsp;&nbsp;
        <span th:text="${iter.index}"></span>&nbsp;&nbsp;
        <span th:text="${user.id}"></span>&nbsp;&nbsp;
        <span th:text="${user.userName}"></span>&nbsp;&nbsp;
        <span th:text="${user.address}"></span><br>
    </div>
</body>
</html>
6.2.5 域对象的操作

也就是我们怎么在Thymeleaf中获取三大作用域中绑定的数据。

java 复制代码
@RequestMapping("/hello4")
public String hello4(HttpServletRequest request){
    request.setAttribute("req","request msg ...");
    request.getSession().setAttribute("sess","session msg ....");
    request.getServletContext().setAttribute("app","application msg ....");
    return "/user4";
}
html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf介绍</title>
</head>
<body>
    <h1>域对象使用</h1>
    <h2>request:</h2>
    <span th:text="${#httpServletRequest.getAttribute('req')}"></span><br>
    <span th:text="${#request.getAttribute('req')}"></span><br>
    <span th:text="${req}"></span><br>
    <h2>session:</h2>
    <span th:text="${#httpSession.getAttribute('sess')}"></span><br>
    <span th:text="${#session.getAttribute('sess')}"></span><br>
    <h2>servletContext:</h2>
    <span th:text="${#servletContext.getAttribute('app')}"></span><br>
</body>
</html>

效果

6.2.6 URL表达式
html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf介绍</title>
</head>
<body>

    <h1>URL使用</h1>
    <a href="http://www.baidu.com">百度</a><br>
    <a th:href="@{http://www.baidu.com}">百度</a><br>
    <hr>
    <a th:href="@{/show}">相对路径</a><br>
    <a th:href="@{~/project2/app1}">相对于服务器的根</a><br>
    <a th:href="@{/show(id=1,name=aaa)}">相对路径--参数传递</a><br>
    <a th:href="@{/path/{id}/show(id=66,name=123)}">RestFul支持</a>
</body>
</html>
6.2.7 整合案例改造

我们可以将前面介绍的SpringBoot+MyBatis+Freemaker的案例改为SpringBoot+MyBatis+Thymeleaf的案例,涉及到的页面代码如下

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>用户管理</h1>
<h2>
    <a th:href="@{/user/dispatchUpdate}">添加用户</a>
</h2>
    <table>
        <tr>
            <th>编号</th>
            <th>账号</th>
            <th>姓名</th>
            <th>邮箱</th>
            <th>电话</th>
            <th>操作</th>
        </tr>

        <tr th:each="user : ${list}">
            <td th:text="${user.user_id}"></td>
            <td th:text="${user.user_name}"></td>
            <td th:text="${user.real_name}"></td>
            <td th:text="${user.email}"></td>
            <td th:text="${user.phone}"></td>
            <td>
                <a th:href="@{/user/dispatchUpdate(id=${user.user_id})}">更新</a>
                <a th:href="@{/user/deleteUser(id=${user.user_id})}">删除</a>
            </td>
        </tr>

    </table>
</body>
</html>
html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form th:action="@{/user/userUpdate}" method="post" >
        <span th:if="${user}">
            <input type="hidden" name="user_id" th:value="${user.user_id}">
        </span>

        <label>账号</label><input type="text" name="user_name" th:value="${ user==null ?'':user.user_name}"><br>
        <label>姓名</label><input type="text" name="real_name" th:value="${user==null ?'':user.real_name}"><br>
        <label>邮箱</label><input type="text" name="email" th:value="${user==null ?'':user.email}"><br>
        <label>电话</label><input type="text" name="phone" th:value="${user==null ?'':user.phone}"><br>
        <input type="submit" value="提交">
    </form>
</body>
</html>

二、SpringBoot高级

1.热部署

为了提高我们的开发效率,我们可以放开IDEA中的SpringBoot项目的热部署操作。

1.1 放开配置

在IDEA中默认是没有放开热部署操作的,我们需要手动的放开设置。

1.2 注册

Control+shift+Alt+/ 会出现一个弹出界面。

然后选择Registry

1.3 添加devtools
html 复制代码
<!--devtools 热部署的支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
2. 异常处理
2.1 自定义错误页面

SpringBoot默认的处理异常的机制:一旦程序出现了异常SpringBoot会想 /error 的url发送请求,在SpringBoot中提供了一个 BasicExceptionController来处理 /error 请求,然后跳转到默认显示异常的页面来展示异常信息。

如果我们需要将所有的异常统一跳转到我们自定义的错误页面,需要在src/main/resources/template 目录下创建一个 error.html页面,注意名称必须是 error.html。

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>系统出错,请联系管理员....</h1>
    <span th:text="${exception}"></span>
</body>
</html>


2.2 @ExceptionHandle注解
java 复制代码
package com.bobo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class UserController {

    @RequestMapping("/show1")
    public String showInfo1(){
        String name = null;
        // 模拟 空指针异常
        name.length();
        return "index";
    }

    @RequestMapping("/show2")
    public String showInfo2(){
        int a = 1/0; // 默认算术异常
        return "index";
    }

    @ExceptionHandler(value = {NullPointerException.class})
    public ModelAndView nullPointerExceptionHandler(Exception e){
        ModelAndView mm = new ModelAndView();
        mm.addObject("error",e.toString());
        mm.setViewName("error1");
        return mm;
    }

    @ExceptionHandler(value = {ArithmeticException.class})
    public ModelAndView arithmeticException(Exception e){
        ModelAndView mm = new ModelAndView();
        mm.addObject("error",e.toString());
        mm.setViewName("error2");
        return mm;
    }
}

error1.html

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>系统出错,请联系管理员....nullPointerExceptionHandler</h1>
    <span th:text="${error}"></span>
</body>
</html>

error2.html

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>系统出错,请联系管理员....arithmeticException</h1>
    <span th:text="${error}"></span>
</body>
</html>

效果

2.3 @ControllerAdvice注解

上面的实现将控制器和异常处理的方法写在了一块,显然不太合理,这时我们可以通过@ControllerAdvice注解来实现解耦。
专门的异常处理类

java 复制代码
package com.bobo.exception;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

// @ControllerAdvice
public class GlobalException {
    //@ExceptionHandler(value = {NullPointerException.class})
    public ModelAndView nullPointerExceptionHandler(Exception e){
        ModelAndView mm = new ModelAndView();
        mm.addObject("error",e.toString());
        mm.setViewName("error1");
        return mm;
    }

    //@ExceptionHandler(value = {ArithmeticException.class})
    public ModelAndView arithmeticException(Exception e){
        ModelAndView mm = new ModelAndView();
        mm.addObject("error",e.toString());
        mm.setViewName("error2");
        return mm;
    }
}

控制器代码

java 复制代码
package com.bobo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class UserController {

    @RequestMapping("/show1")
    public String showInfo1(){
        String name = null;
        // 模拟 空指针异常
        name.length();
        return "index";
    }
    @RequestMapping("/show2")
    public String showInfo2(){
        int a = 1/0; // 默认算术异常
        return "index";
    }
}
2.4 SimpleMappingExceptionResolver

我们还可以通过SimpleMappingExceptionResolver来简化我们的异常处理。

java 复制代码
package com.bobo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.Properties;

@SpringBootApplication
public class SpringbootDemo11Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemo11Application.class, args);
    }

    /**
     * 通过SimpleMappingExceptionResolver 设置 特定异常和 处理器的映射关系
     * @return
     */
    // @Bean
    public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver(){
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        properties.put("java.lang.NullPointerException","error1");
        properties.put("java.lang.ArithmeticException","error2");
        resolver.setExceptionMappings(properties);
        return  resolver;
    }
}
2.5 HandleExceptionResolver处理

我们上面讲的SimpleMappingExceptionResolver本质上就是实现HandleExceptionResolver的。

所以我们也可以自己来实现HandleExceptionResolver接口。

java 复制代码
package com.bobo.exception;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class MyHandleExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        ModelAndView mm = new ModelAndView();
        if(e instanceof NullPointerException){
            mm.setViewName("error1");
        }else if(e instanceof  ArithmeticException){
            mm.setViewName("error2");
        }else{
            mm.setViewName("error");
        }
        return mm;
    }
}
3. 单元测试

为了提高在开发过程中的效率,我们可以通过SpringBoot中提供的单元测试来快速测试service和dao的业务逻辑。

html 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

业务逻辑

java 复制代码
package com.bobo.service.impl;

import com.bobo.service.IUserService;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Service
public class UserServiceImpl implements IUserService {


    @Override
    public List<String> query() {

        return Arrays.asList("张三","李四","王五");
    }
}

单元测试

java 复制代码
package com.bobo;

import com.bobo.service.IUserService;
import net.bytebuddy.asm.Advice;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringbootDemo12ApplicationTests {

    @Autowired
    private IUserService service;


    @Test
    void contextLoads() {
        System.out.println("---->" + service.query());
    }

    @BeforeEach
    void before(){
        System.out.println("before ...");
    }

    @AfterEach
    void after(){
        System.out.println("after ...");
    }

}
4. 整合Shiro
4.1 项目准备

创建一个SpringBoot项目整合MyBatis,Thymeleaf,SpringMVC等并创建相关的配置文件和Service逻辑。

html 复制代码
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.8</version>
        </dependency>
        <!--devtools 热部署的支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
    <dependencies>

属性配置文件

html 复制代码
server.port=8082

# 配置JDBC的相关信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/logistics?
characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456

# 配置连接池
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 配置MyBatis的package 设置别名
mybatis.type-aliases-package=com.bobo.pojo

# 指定映射文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml

通过MyBatis Generator自动生成持久层的相关的代码(t_user表)或者从之前的货运系统中拷贝对应的代码。

Service的逻辑实现

java 复制代码
package com.bobo.service;

import com.bobo.pojo.User;

import java.util.List;

public interface IUserService {

    public User login(String userName);

    public List<User> query(User user);
}
java 复制代码
package com.bobo.service.impl;

import com.bobo.mapper.UserMapper;
import com.bobo.pojo.User;
import com.bobo.pojo.UserExample;
import com.bobo.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserMapper mapper;

    @Override
    public User login(String userName) {
        User user =  new User();
        user.setUserName(userName);
        List<User> list = this.query(user);
        if(list != null && list.size() == 1){
            return list.get(0);
        }
        return null;
    }

    @Override
    public List<User> query(User user) {
        UserExample example = new UserExample();
        UserExample.Criteria criteria = example.createCriteria();
        if(user != null){
            if(!"".equals(user.getUserName()) && user.getUserName() != null){
                criteria.andUserNameEqualTo(user.getUserName());
            }
        }
        return mapper.selectByExample(example);
    }
}

到此准备完成。

4.2 Shiro整合
4.2.1 Shiro的依赖
html 复制代码
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
4.2.2 自定义Realm
java 复制代码
package com.bobo.realm;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private IUserService service;

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
       return null;
    }
}
4.2.3 Shiro的配置
java 复制代码
package com.bobo.config;

import com.bobo.realm.MyRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.crypto.hash.Hash;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.apache.shiro.mgt.SecurityManager;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    /**
     * 配置凭证匹配器
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5");
        matcher.setHashIterations(1024);
        return matcher;
    }

    /**
     * 注册自定义的Realm
     * @param hashedCredentialsMatcher
     * @return
     */
    @Bean
    public MyRealm myRealm(CredentialsMatcher hashedCredentialsMatcher){
       MyRealm realm = new MyRealm();
       realm.setCredentialsMatcher(hashedCredentialsMatcher);
       return realm;
    }

    /**
     * 注册SecurityManager对象
     * @return
     */
    @Bean
    public SecurityManager securityManager(Realm myRealm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm);
        return manager;
    }

    /**
     * 注册ShiroFilterFactoryBean
     * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager manager){
        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
        filter.setSecurityManager(manager);
        filter.setLoginUrl("/login.do");
        filter.setSuccessUrl("/success.html");
        filter.setUnauthorizedUrl("/refuse.html");
        // 设置过滤器
        Map<String,String> map = new HashMap<>();
        map.put("/css/**","anon");
        map.put("/img/**","anon");
        map.put("/js/**","anon");
        map.put("/login","anon");
        map.put("/login.do","authc");
        map.put("/**","authc");
        filter.setFilterChainDefinitionMap(map);
        return filter;
    }
}
4.2.4 测试

添加对应的测试文件

java 复制代码
package com.bobo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    @RequestMapping("/login")
    public String goLoginPage(){
        return "login";
    }
}
java 复制代码
package com.bobo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/query")
    public String query(){
        System.out.println("----user query----");
        return "user";
    }
}
4.3 认证实现
4.3.1 自定义Realm

在自定义Realm中完成认证逻辑。

java 复制代码
package com.bobo.realm;

import com.bobo.pojo.User;
import com.bobo.service.IUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.SimpleByteSource;
import org.springframework.beans.factory.annotation.Autowired;

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private IUserService service;

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String userName = token.getUsername();
        User user = new User();
        user.setUserName(userName);
        // 账号验证
        user = service.login(userName);
        if(user == null){
            return null;
        }
        return new SimpleAuthenticationInfo(user,user.getPassword(),new SimpleByteSource(user.getU1()) ,"myRealm");
    }

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
}
4.3.2 控制器

在控制器中完成认证失败的处理。

java 复制代码
package com.bobo.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;

@Controller
public class LoginController {

    @RequestMapping("/login")
    public String goLoginPage(){
        return "login";
    }
    
    @RequestMapping("/login.do")
    public String login(HttpServletRequest request){
        Object obj = request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
        System.out.println("认证错误的信息:" + obj);
        return "/login";
    }

    @RequestMapping("/logout")
    public String logout(){
        SecurityUtils.getSubject().logout();
        return "/login";
    }
}
4.3.3 登录页面
html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>登录页面</h1>
    <form th:action="@{/login.do}" method="post">
        账号:<input type="text" name="username" th:value="'admin1'"><br>
        密码:<input type="password" name="password" th:value="'123'"><br>
        <input type="submit" value="提交">
    </form>
</body>
</html>
5. 授权操作
5.1 注解的使用

我们需要开启SpringMVC对注解的支持。

java 复制代码
/**
* 开启对Shiro授权注解的支持
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    return advisor;
}

在自定义Realm中添加权限。

java 复制代码
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    User user = (User) principalCollection.getPrimaryPrincipal();
    System.out.println("获取授权的账号:" + user.getUserName());
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.addRole("role1");
    return info;
}
html 复制代码
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

启动访问


5.2 标签的使用

添加相关的依赖。

html 复制代码
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

注入对象

java 复制代码
@Bean
public ShiroDialect shiroDialect(){
    return new ShiroDialect();
}

在页面中实现处理

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
    xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>用户管理</h1>
    <shiro:authenticated>
        已登录:<shiro:principal property="userName"></shiro:principal>
    </shiro:authenticated>
    <a href="#" shiro:hasRole="role1">用户查询</a>
    <a href="#" shiro:hasRole="role1">用户添加</a>
    <a href="#" shiro:hasRole="role2">用户修改</a>
    <a href="#" shiro:hasRole="role2">用户删除</a>
</body>
</html>

访问效果

相关推荐
希忘auto4 分钟前
详解MySQL安装
java·mysql
冰淇淋烤布蕾15 分钟前
EasyExcel使用
java·开发语言·excel
拾荒的小海螺21 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
Jakarta EE38 分钟前
正确使用primefaces的process和update
java·primefaces·jakarta ee
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
java—大象1 小时前
基于java+springboot+layui的流浪动物交流信息平台设计实现
java·开发语言·spring boot·layui·课程设计
问道飞鱼1 小时前
【微服务知识】开源RPC框架Dubbo入门介绍
微服务·rpc·开源·dubbo
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
布川ku子2 小时前
[2024最新] java八股文实用版(附带原理)---Mysql篇
java·mysql·面试