SpringBoot

一、入门

1.1、第一个springboot程序

pom.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.6.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ywb</groupId>
    <artifactId>springboot-01-helloworld</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-01-helloworld</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-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

</project>

controller

@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "hello world";
    }

}

1.2、自动装配原理

自动配置

pom.xml

  • spring-boot-dependencies:核心依赖在父工程中
  • 我们在写或者引入一些springboot依赖的时候,不需要指定版本,就是因为有这些版本仓库

启动器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

说白了就是springboot的启动场景

比如spring-boot-starter-web,就会帮我们自动导入web环境所有的依赖

springboot会将所有的功能场景,都变成一个个的启动器

我们要使用什么功能,就只需要找到对应的启动器就可以了starter

主程序

//@SpringBootApplication:标注这个类是一个Springboot的应用
@SpringBootApplication
public class Springboot01HelloworldApplication {

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

}
  • 注解

    • @SpringBootConfiguration:springboot的配置
      	@Configuration:spring配置类
      	@Component:说明这也是一个spring的组件
      
      @EnableAutoConfiguration:自动配置
      	@AutoConfigurationPackage:自动配置包
      	@Import(AutoConfigurationPackages.Registrar.class):自动配置 包注册
      	@Import(AutoConfigurationImportSelector.class):自动导入选择
      	
      

结论:springboot所有自动配置都是在启动的时候扫描并加载:spring.factories所有的自动配置类都在这里面,但是不一定生效,要判断条件是否成立,只要导入了对应的start,就有对应的启动器了,有了启动器,我们自动装配就会生效,然后就配置成功!

1、springboot在启动的时候,从类路径下/META_INF/spring.factories获取指定的值

2、将这些自动配置的类导入容器,自动配置类就会生效,帮我们进行自动配置

3、以前我们需要自动配置的东西,现在springboot帮我们做了

4、整合javaEE,解决方案和自动配置的东西都在spring-boot-autoconfigure-2.6.4.jar下

5、它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器

6、容器中也会存在非常多的xxxAutoConfiguration的文件,就是这些类给容器中导入了这个场景需要的所有组件,并自动配置@Configuration

7、有了自动配置类,免去了我们手动编写配置文件的工作

全面接管SpringMVC的配置

1、springboot启动会加载大量的自动配置类

2、我们看我们需要的功能有没有在springboot默认写好的自动配置类当中

3、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在其中,我们就不需要再手动配置了)

4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可

xxxAutoConfiguration:自动配置类,给容器中添加组件

xxxProperties:封装配置文件中相关属性

1.3、YAMl

# 对象
student:
  name: xiaoming
  age: 3

# 行内写法
sutdent: {name: xiaohong,age: 3}

# 数组
pets:
  - cat
  - dog
  - pig

pets1: [cat,dog,pig]

给实体类赋值

@ConfigurationProperties(prefix = "xxxx")

实体类:

@Component
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class Dog {

    private String name;
    private Integer age;

}

@Component
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
@ConfigurationProperties(prefix = "person")
public class Person {

    private String name;
    private Integer age;
    private boolean happy;
    private Date birthday;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
}

application.yml:

person:
  name: xiaoming
  age: 18
  #age: ${random.int}
  #age: ${random.uuid}
  happy: false
  birthday: 2022/03/22
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - music
    - basketball
  dog:
    name: wangcai
    age: 3

JSR303校验

导入依赖

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.2.0.Final</version>
</dependency>
@Null 限制只能为null
@NotNull 限制必须不为null
@AssertFalse 限制必须为false
@AssertTrue 限制必须为true
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Future 限制必须是一个将来的日期
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@Past 限制必须是一个过去的日期
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在min到max之间
@Past 验证注解的元素值(日期类型)比当前时间早
@NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Validated
public class Person {

    @Email(message = "报错提示信息")
    private String name;
    private Integer age;
    private boolean happy;
    private Date birthday;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
}

多环境配置

方式一:

image-20220322103715586

方式二:

image-20220322104001574

二、springboot web开发

自动装配

springboot到底帮我们配置了什么,能不能进行修改,能修改哪些东西,能不能扩展

  • xxxAutoConfiguration…向容器中自动配置组件
  • xxxProperties:自动配置类,装配配置文件中自定义的一些内容

要解决的问题:

  • 导入静态资源
  • 首页
  • jsp,模板引擎Thymeleaf
  • 装配扩展springmvc
  • CRUD
  • 拦截器
  • 国际化

2.1、静态资源

image-20220322192612077

三个目录都可以放静态资源

优先级:resources > static > public

2.2、首页定制

将index.html放在static目录下

放在templates目录下的所有页面,只能通过controller来跳转,需要thymeleaf

2.3、Thymeleaf模板引擎

导入依赖

<dependency>
	<groupId>org.thymeleaf</groupId>
	<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
	<groupId>org.thymeleaf.extras</groupId>
	<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

结论:想要使用thymeleaf,只需要导入对应的依赖就可以了!我们将html放在我们的templates目录下

<html lang="en" xmlns:th="http://www.thymeleaf.org">

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

<div th:text="${msg}"></div>
<div th:utext="${msg}"></div>
<hr>

<div th:each="user:${users}" th:text="${user}"></div>
<hr>

<div th:each="user:${users}">[[ ${user} ]]</div>

</body>
</html>

2.4、MVC配置原理

image-20220322212401969

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/yang").setViewName("test");
    }
}

在springboot中,有非常多的xxxxConfiguration会帮助我们进行扩展配置,只要看见了这个东西,我们就要注意了

2.5、项目实战

准备工作

设置请求映射首页

image-20220322235208889

html中的路径需要用th标签,路径用@{}来取,且静态资源放在static下

image-20220322235120645

国际化

在resources目录下建目录i18n

新建login.properties,login_zh_CN.properties文件,会自动合并

在properties文件的下方可以选择可视化配置

image-20220323000035349

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7fGVtcY2-1648660959342)(C:/Users/杨维彬的电脑/AppData/Roaming/Typora/typora-user-images/image-20220324222227112.png)]

image-20220324222235098

image-20220324222247806

配置类:

/**
 * @author ywb
 * 国际化地区解析器
 */
public class MyLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        //获取请求中的语言参数连接
        String lang = request.getParameter("lang");
        Locale locale = Locale.getDefault();
        if(!StringUtils.isEmpty(lang)){
            String[] split = lang.split("_");
            //国家,地区
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

在配置中加入组件:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/templates/index.html").setViewName("index");
    }

    //自定义的国际化组件生效
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }

}

带有国际化的主页界面:

在thymeleaf中使用#{}引用

正常文本用th:text

按钮用th:value

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<meta name="description" content="">
		<meta name="author" content="">
		<title>Signin Template for Bootstrap</title>
		<!-- Bootstrap core CSS -->
		<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
		<!-- Custom styles for this template -->
		<link th:href="@{/css/signin.css}" rel="stylesheet">
	</head>

	<body class="text-center">
		<form class="form-signin" action="dashboard.html">
			<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
			<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
			<input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
			<input type="password" class="form-control" th:placeholder="#{login.password}" required="">
			<div class="checkbox mb-3">
				<label>
          <input type="checkbox" value="remember-me"> [[#{login.remember}]]
        </label>
			</div>
			<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
			<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
			<a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a>
			<a class="btn btn-sm" th:href="@{/index.html(lang='en_US')}">English</a>
		</form>

	</body>

</html>

登录验证

Controller

@Controller
public class HelloController {

    @RequestMapping("/user/login")
    public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model){
        //具体的业务
        if (username!=null && "123456".equals(password)){
            //为了不显示真实的请求地址和暴露参数(用户名,密码)
            //注意加一个斜杆"/"
            return "redirect:/main.html";
        }else {
            model.addAttribute("msg","用户名或者密码错误");
            return "index";
        }
    }

}

设置映射路径:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
        registry.addViewController("/main.html").setViewName("dashboard");
    }

}

index的form表单中增加用户名密码错误提示信息

<p style="color: red" th:text="${msg}" th:if="${msg}!=null"></p>

<form class="form-signin" th:action="@{/user/login}">
			<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
			<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
			<p style="color: red" th:text="${msg}" th:if="${msg}!=null"></p>
			<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
			<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
			<div class="checkbox mb-3">
				<label>
          <input type="checkbox" value="remember-me"> [[#{login.remember}]]
        </label>
			</div>
			<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
			<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
			<a class="btn btn-sm" th:href="@{/index.html(lang='zh_CN')}">中文</a>
			<a class="btn btn-sm" th:href="@{/index.html(lang='en_US')}">English</a>
		</form>

拦截器

自定义拦截器类:

/**
 * @author ywb
 *  拦截器类
 */
public class LoginHandlerInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //登录成功之后应该有用户的session
        Object loginUser = request.getSession().getAttribute("loginUser");
        if (loginUser==null){
            //将在主页中显示
            request.setAttribute("msg","没有权限,请先登录");
            request.getRequestDispatcher("/index.html").forward(request,response);
            //不登陆,不放行
            return false;
        }
        return true;
    }

}

在webConfig中配置拦截器需要拦截哪些请求

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
        registry.addViewController("/main.html").setViewName("dashboard");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //     /** 是拦截所有的文件夹及里面的子文件夹
        registry.addInterceptor(new LoginHandlerInterceptor()).
                addPathPatterns("/**").
                excludePathPatterns("/","/index.html","/user/login","/css/*","/js/*","/img/**");
    }
}

在html中取用户名,从session中取得

<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0">
    <!--[[${session.loginUser}]] 用来取值-->
    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
    <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
        </li>
    </ul>
</nav>

image-20220324232733421

头部栏和侧边栏抽象

抽取:th:fragment="xxxx"

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.themeleaf.org">

<!--头部栏-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
    <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
        </li>
    </ul>
</nav>

<!--侧边栏-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
    <div class="sidebar-sticky">
        <ul class="nav flex-column">
            <li class="nav-item">
                <a th:class="${active}=='main.html'?'nav-link active':'nav-link'" th:href="@{/index.html}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                        <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                        <polyline points="9 22 9 12 15 12 15 22"></polyline>
                    </svg>
                    Dashboard <span class="sr-only">(current)</span>
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
                        <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                        <polyline points="13 2 13 9 20 9"></polyline>
                    </svg>
                    Orders
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart">
                        <circle cx="9" cy="21" r="1"></circle>
                        <circle cx="20" cy="21" r="1"></circle>
                        <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                    </svg>
                    Products
                </a>
            </li>
            <li class="nav-item">
                <a th:class="${active}=='list.html'?'nav-link active':'nav-link'" th:href="@{/emp/list}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
                        <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                        <circle cx="9" cy="7" r="4"></circle>
                        <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                        <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                    </svg>
                    Customers
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2">
                        <line x1="18" y1="20" x2="18" y2="10"></line>
                        <line x1="12" y1="20" x2="12" y2="4"></line>
                        <line x1="6" y1="20" x2="6" y2="14"></line>
                    </svg>
                    Reports
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers">
                        <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
                        <polyline points="2 17 12 22 22 17"></polyline>
                        <polyline points="2 12 12 17 22 12"></polyline>
                    </svg>
                    Integrations
                </a>
            </li>
        </ul>

        <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
            <span>Saved reports</span>
            <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
            </a>
        </h6>
        <ul class="nav flex-column mb-2">
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    Current month
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    Last quarter
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    Social engagement
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    Year-end sale
                </a>
            </li>
        </ul>
    </div>
</nav>


</html>

使用:<div th:insert="~{commons/commons::topbar}"></div>

<div th:insert="~{commons/commons::topbar}"></div>
<div th:insert="~{commons/commons::sidebar(active='list.html')}"></div>

thymeleaf传参(xxx='xxx')

三元运算符判断条件显示高亮

<div th:insert="~{commons/commons::sidebar(active='list.html')}"></div>



<a th:class="${active}=='main.html'?'nav-link active':'nav-link'" th:href="@{/index.html}">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
        <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
        <polyline points="9 22 9 12 15 12 15 22"></polyline>
    </svg>
    Dashboard <span class="sr-only">(current)</span>
</a>

<a th:class="${active}=='list.html'?'nav-link active':'nav-link'" th:href="@{/emp/list}">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
        <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
        <circle cx="9" cy="7" r="4"></circle>
        <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
        <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
    </svg>
    Customers
</a>

可将公共部分抽出

image-20220327143213295

重定向后Model的数据被清空

@Controller
public class EmployeeController {

    @Autowired
    EmployeeDao employeeDao;

    @RequestMapping("/emp/list")
    public String list(Model  model){
        Collection<Employee> employees = employeeDao.getEmployees();
        model.addAttribute("employees",employees);
        return "redirect:/list.html";
        //return "emp/list";
    }
}
/*
	使用redirect重定向之后,model的数据会被清空
*/

Model改为RedirectAttributesModelMap,同时把addAttribute改成addFlashAttribute

@Controller
public class EmployeeController {

    @Autowired
    EmployeeDao employeeDao;

    @RequestMapping("/emp/list")
    public String list(RedirectAttributesModelMap  model){
        Collection<Employee> employees = employeeDao.getEmployees();
        model.addFlashAttribute("employees",employees);
        return "redirect:/list.html";
        //return "emp/list";
    }
}

若直接使用请求转发,则可以直接使用Model

但改成RedirectAttributesModelMap后,在html页面的代码依然会报红

<table class="table table-striped table-sm">
    <thead>
        <tr>
            <th>id</th>
            <th>name</th>
            <th>email</th>
            <th>gender</th>
            <th>department</th>
            <th>birth</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="emp:${employees}">
            <td th:text="${emp.getId()}"></td>
            <td th:text="${emp.getName()}"></td>
            <td th:text="${emp.getEmail()}"></td>
            <td th:text="${emp.getGender()}"></td>
            <td th:text="${emp.getDepartment().getDepartmentName()}"></td>
            <td th:text="${emp.getBirth()}"></td>
        </tr>
    </tbody>
</table>

性别转换

<td th:text="${emp.getGender()==0 ? '' : ''}"></td>

日期格式转换

<td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>

添加员工

html页面:

<!DOCTYPE html>
<!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">

    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">

        <title>Dashboard Template for Bootstrap</title>
        <!-- Bootstrap core CSS -->
        <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

        <!-- Custom styles for this template -->
        <link th:href="@{/css/dashboard.css}" rel="stylesheet">
        <style type="text/css">
            /* Chart.js */

            @-webkit-keyframes chartjs-render-animation {
                from {
                    opacity: 0.99
                }
                to {
                    opacity: 1
                }
            }

            @keyframes chartjs-render-animation {
                from {
                    opacity: 0.99
                }
                to {
                    opacity: 1
                }
            }

            .chartjs-render-monitor {
                -webkit-animation: chartjs-render-animation 0.001s;
                animation: chartjs-render-animation 0.001s;
            }
        </style>
    </head>

    <body>
        <div th:insert="~{commons/commons::topbar}"></div>

        <div class="container-fluid">
            <div class="row">
                <div th:insert="~{commons/commons::sidebar(active='list.html')}"></div>

                <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                    <form th:action="@{/emp}" method="post">
                        <div class="form-group">
                            <label>LastName</label>
                            <input type="text" name="name" class="form-control" placeholder="lastname:zsr">
                        </div>
                        <div class="form-group">
                            <label>Email</label>
                            <input type="email" name="email" class="form-control" placeholder="email:xxxxx@qq.com">
                        </div>
                        <div class="form-group">
                            <label>Gender</label><br/>
                            <div class="form-check form-check-inline">
                                <input class="form-check-input" type="radio" name="gender" value="1">
                                <label class="form-check-label"></label>
                            </div>
                            <div class="form-check form-check-inline">
                                <input class="form-check-input" type="radio" name="gender" value="0">
                                <label class="form-check-label"></label>
                            </div>
                        </div>
                        <div class="form-group">
                            <label>department</label>
                            <!--注意这里的name是department.id,因为传入的参数为id-->
                            <select class="form-control" name="department.id">
                                <option th:each="department:${departments}" th:text="${department.getDepartmentName()}"
                                        th:value="${department.getId()}"></option>
                            </select>
                        </div>
                        <div class="form-group">
                            <label>Birth</label>
                            <!--springboot默认的日期格式为yy/MM/dd,可修改格式-->
                            <input type="text" name="birth" class="form-control" placeholder="birth:yyyy-MM-dd">
                        </div>
                        <button type="submit" class="btn btn-primary">添加</button>
                    </form>
                </main>
            </div>
        </div>

        <!-- Bootstrap core JavaScript
================================================== -->
        <!-- Placed at the end of the document so the pages load faster -->
        <script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script>
        <script type="text/javascript" th:src="@{/js/popper.min.js}"></script>
        <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>

        <!-- Icons -->
        <script type="text/javascript" th:src="@{/js/feather.min.js}"></script>
        <script>
            feather.replace()
        </script>

        <!-- Graphs -->
        <script type="text/javascript" th:src="@{/js/Chart.min.js}"></script>
        <script>
            var ctx = document.getElementById("myChart");
            var myChart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
                    datasets: [{
                        data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
                        lineTension: 0,
                        backgroundColor: 'transparent',
                        borderColor: '#007bff',
                        borderWidth: 4,
                        pointBackgroundColor: '#007bff'
                    }]
                },
                options: {
                    scales: {
                        yAxes: [{
                            ticks: {
                                beginAtZero: false
                            }
                        }]
                    },
                    legend: {
                        display: false,
                    }
                }
            });
        </script>

    </body>

</html>

注意:

department的显示以及选法,传参的时候传的是id,因此字段的name是department.id,

Controller:

此处使用Restful风格,同一个请求路径,用不同的请求方法

@Controller
public class EmployeeController {

    @Autowired
    EmployeeDao employeeDao;

    @Autowired
    DepartmentDao departmentDao;

    @RequestMapping("/emp/list")
    public String list(Model  model){
        Collection<Employee> employees = employeeDao.getEmployees();
        model.addAttribute("employees",employees);
        //return "redirect:/list.html";
        return "emp/list";
    }

    @GetMapping("/emp")
    public String toAddPage(Model model){
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments",departments);
        //直接使用return时,地址栏会直接变成请求路径
        return "emp/add";
    }

    @PostMapping("/emp")
    public String addEmployee(Employee employee){
        employeeDao.saveEmployee(employee);
        //使用redirect时,地址栏会变成路径,/emp/list
        return "redirect:/emp/list";
    }

}

日期格式配置:

spring:
  messages:
    basename: i18n.login
  thymeleaf:
    cache: false
  mvc:
    format:
      date: yyyy-MM-dd

修改员工

html页面

<!DOCTYPE html>
<!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ -->
<html lang="en" xmlns:th="http://www.thymeleaf.org">

	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
		<meta name="description" content="">
		<meta name="author" content="">

		<title>Dashboard Template for Bootstrap</title>
		<!-- Bootstrap core CSS -->
		<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">

		<!-- Custom styles for this template -->
		<link th:href="@{/css/dashboard.css}" rel="stylesheet">
		<style type="text/css">
			/* Chart.js */
			
			@-webkit-keyframes chartjs-render-animation {
				from {
					opacity: 0.99
				}
				to {
					opacity: 1
				}
			}
			
			@keyframes chartjs-render-animation {
				from {
					opacity: 0.99
				}
				to {
					opacity: 1
				}
			}
			
			.chartjs-render-monitor {
				-webkit-animation: chartjs-render-animation 0.001s;
				animation: chartjs-render-animation 0.001s;
			}
		</style>
	</head>

	<body>
		<div th:insert="~{commons/commons::topbar}"></div>

		<div class="container-fluid">
			<div class="row">
				<div th:insert="~{commons/commons::sidebar(active='list.html')}"></div>

				<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
					<form th:action="@{/updateEmp}" method="post">
						<input type="hidden" name="id" th:value="${employee.getId()}">
						<div class="form-group">
							<label>LastName</label>
							<input th:value="${employee.getName()}" type="text" name="name" class="form-control" placeholder="lastname:zsr">
						</div>
						<div class="form-group">
							<label>Email</label>
							<input th:value="${employee.getEmail()}" type="email" name="email" class="form-control" placeholder="email:xxxxx@qq.com">
						</div>
						<div class="form-group">
							<label>Gender</label><br/>
							<div class="form-check form-check-inline">
								<input class="form-check-input" type="radio" name="gender" value="1" th:checked="${employee.getGender()}==1">
								<label class="form-check-label"></label>
							</div>
							<div class="form-check form-check-inline">
								<input class="form-check-input" type="radio" name="gender" value="0" th:checked="${employee.getGender()}==0">
								<label class="form-check-label"></label>
							</div>
						</div>
						<div class="form-group">
							<label>department</label>
							<!--注意这里的name是department.id,因为传入的参数为id-->
							<select class="form-control" name="department.id">
								<option th:each="department:${departments}" th:text="${department.getDepartmentName()}"
										th:value="${department.getId()}" th:selected="${department.getId()==employee.getDepartment().getId()}"></option>
							</select>
						</div>
						<div class="form-group">
							<label>Birth</label>
							<!--springboot默认的日期格式为yy/MM/dd,可修改格式-->
							<input type="text" name="birth" class="form-control" placeholder="birth:yyyy-MM-dd" th:value="${#dates.format(employee.getBirth(),'yyyy-MM-dd HH:mm:ss')}">
						</div>
						<button type="submit" class="btn btn-primary">修改</button>
					</form>
				</main>
			</div>
		</div>

		<!-- Bootstrap core JavaScript
    ================================================== -->
		<!-- Placed at the end of the document so the pages load faster -->
		<script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script>
		<script type="text/javascript" th:src="@{/js/popper.min.js}"></script>
		<script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>

		<!-- Icons -->
		<script type="text/javascript" th:src="@{/js/feather.min.js}"></script>
		<script>
			feather.replace()
		</script>

		<!-- Graphs -->
		<script type="text/javascript" th:src="@{/js/Chart.min.js}"></script>
		<script>
			var ctx = document.getElementById("myChart");
			var myChart = new Chart(ctx, {
				type: 'line',
				data: {
					labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
					datasets: [{
						data: [15339, 21345, 18483, 24003, 23489, 24092, 12034],
						lineTension: 0,
						backgroundColor: 'transparent',
						borderColor: '#007bff',
						borderWidth: 4,
						pointBackgroundColor: '#007bff'
					}]
				},
				options: {
					scales: {
						yAxes: [{
							ticks: {
								beginAtZero: false
							}
						}]
					},
					legend: {
						display: false,
					}
				}
			});
		</script>

	</body>

</html>

注意:

需要带上id隐藏域

<input type="hidden" name="id" th:value="${employee.getId()}">

显示修改前的值使用th:value标签

注意单选和下拉框的默认选择

th:checked和th:selected

传递需要修改的员工id

<td><a class="btn btn-sm btn-primary" th:href="@{/emp/{id}(id=${emp.getId()})}">编辑</a></td>

Controller:

@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id") Integer id, Model model){
    //查出原来的数据
    Employee employeeById = employeeDao.getEmployeeById(id);
    model.addAttribute("employee",employeeById);
    Collection<Department> departments = departmentDao.getDepartments();
    model.addAttribute("departments",departments);
    return "emp/update";
}

@PostMapping("/updateEmp")
public String updateEmp(Employee employee){
    employeeDao.saveEmployee(employee);
    return "redirect:/emp/list";
}

修改员工

html页面:

<td><a class="btn btn-sm btn-danger" th:href="@{/deleteEmp/{id}(id=${emp.getId()})}">删除</a></td>

Controller:

@GetMapping("/deleteEmp/{id}")
public String deleteEmp(@PathVariable("id") Integer id){
    employeeDao.delete(id);
    return "redirect:/emp/list";
}

404处理

在templates下建一个文件夹error,把404.html放在下面,就会自动找到该页面

注销功能

页面:

<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
    <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="nav-link" th:href="@{/user/logout}">Sign out</a>
        </li>
    </ul>
</nav>

Controller:

@GetMapping("/user/logout")
public String logout(HttpSession session){
    session.invalidate();
    return "redirect:/";
}

2.6、前端页面

  • Bootstrap
  • Layui
  • semantic-ui
  • x-admin

三、连接数据库

3.1、整合JDBC使用

在yaml文件中配置数据库信息

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver

服务时间、是否使用Unicode编码,characterEncoding编码

jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8

在配置数据库之后spring就会自动生成JdbcTemplate类

我们可以通过这个类的对象使用数据库的相关操作

@RestController
public class JdbcController {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @GetMapping("/userList")
    public List<Map<String,Object>> userList(){
        String sql = "select * from user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }

    @GetMapping("/addUser")
    public String addUser(){
        String sql = "insert into `user`(`id`,`name`,`pwd`) values(7,'xiaoming','123456')";
        jdbcTemplate.update(sql);
        return "add-ok";
    }

    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") Integer id){
        String sql = "update user set name = ?, pwd = ? where id = "+id;
        Object[] objects = new Object[2];
        objects[0] = "xiaohong";
        objects[1] = "654321";
        jdbcTemplate.update(sql,objects);
        return "update-ok";
    }

    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") Integer id){
        String sql = "delete from user where id = "+id;
        jdbcTemplate.update(sql);
        return "delete-ok";
    }
}

3.2、Druid数据源

1、导入依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2、配置数据源

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

3、编写配置类

@Configuration
public class DruidConfig {

    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }

    /**
     * 后台监控:web.xml    ServletRegistrationBean
     * 因为springboot内置了servlet容器,所以没有web.xml,替代方法:ServletRegistrationBean
     * @return
     */
    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(),"/druid/*");
        //后台需要有人登录,账号密码配置
        HashMap<String, String> initParameters = new HashMap<>();
        //设置账号和密码,两个key是固定的
        initParameters.put("loginUser","admin");
        initParameters.put("loginPassword","1234567");
        //允许谁访问,如果value为空,就是允许所有人访问
        initParameters.put("allow","");

        //设置初始化参数
        bean.setInitParameters(initParameters);
        return bean;
    }

    /**
     * 过滤器
     * @return
     */
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new WebStatFilter());
        HashMap<String, String> initParameters = new HashMap<>();
        //这些东西不进行统计
        initParameters.put("exclusions","*.js,*.css,/druid/*");
        //可以过滤哪些请求
        bean.setInitParameters(initParameters);
        return bean;
    }
}

访问监控页面:localhost:8080/druid

3.3、整合Mybatis框架

1、导入依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.1</version>
</dependency>

2、配置数据源和整合mybatis

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&serverTimezone=UTC&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  type-aliases-package: com.ywb.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml

3、编写mapper接口

/**
 * @author ywb
 * 注解表示了这是一个mybatis的mapper类
 */
@Mapper
@Repository
public interface UserMapper {

    /**
     * 查找所有用户
     * @return
     */
    List<User> queryUserList();

    /**
     * 通过id查找用户
     * @param id 用户id
     * @return
     */
    User queryUserById(Integer id);

    /**
     * 添加用户
     * @param user
     * @return
     */
    Integer addUser(User user);

    /**
     * 更新用户
     * @param user
     * @return
     */
    Integer updateUser(User user);

    /**
     * 删除用户
     * @param id
     * @return
     */
    Integer deleteUser(Integer id);
}

4、编写mapper.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.ywb.mapper.UserMapper">

    <select id="queryUserList" resultType="com.ywb.pojo.User">
        select * from user
    </select>

    <select id="queryUserById" resultType="com.ywb.pojo.User" parameterType="Integer">
        select  * from user where id = #{id}
    </select>

    <insert id="addUser" parameterType="com.ywb.pojo.User">
        insert into `user`(id,`name`,pwd) values(#{id},#{name},#{pwd})
    </insert>

    <update id="updateUser" parameterType="com.ywb.pojo.User">
        update user set name=#{name},pwd=#{pwd} where id = #{id}
    </update>

    <delete id="deleteUser" parameterType="Integer">
        delete from user where id = #{id}
    </delete>

</mapper>

5、编写Controller

@RestController
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping("/queryUserList")
    public List<User> queryUserList(){
        List<User> users = userMapper.queryUserList();
        for (User user : users) {
            System.out.println(user);
        }
        return users;
    }

    @GetMapping("/queryUserById/{id}")
    public User queryUserById(@PathVariable("id") Integer id){
        return userMapper.queryUserById(id);
    }

    @GetMapping("/addUser")
    public String addUser(){
        userMapper.addUser(new User(7,"小明","123456"));
        return "ok";
    }

    @GetMapping("/updateUser")
    public String updateUser(){
        userMapper.updateUser(new User(7,"小明","989787"));
        return "ok";
    }

    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") Integer id){
        userMapper.deleteUser(id);
        return "ok";
    }
}

四、SrpingSecurity

4.1、授权和认证

image-20220329012232041

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、编写跳转路由

@Controller
public class RouterController {

    @GetMapping({"/","/index"})
    public String index(){
        return "index";
    }

    @GetMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }

    @GetMapping("/level1/{id}")
    public String level1(@PathVariable("id") Integer id){
        return "views/level1/"+id;
    }

    @GetMapping("/level2/{id}")
    public String level2(@PathVariable("id") Integer id){
        return "views/level2/"+id;
    }

    @GetMapping("/level3/{id}")
    public String level3(@PathVariable("id") Integer id){
        return "views/level3/"+id;
    }
}

3、编写配置类

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 授权
     * 链式编程
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //首页所有人都可以访问,功能页只有对应权限才可以访问
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3");

        //没有权限默认到登录页
        http.formLogin();
    }

    /**
     * 内存中的认证,密码需要编码,在Security5.0+新增了很多的加密方法
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("vip3").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
                .and().withUser("vip1").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
                .and().withUser("vip2").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
    }
}

如果使用数据库查询,则按照下图

image-20220329012202317

4.2、注销及权限控制

1、注销

@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有人都可以访问,功能页只有对应权限才可以访问
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasRole("vip1")
        .antMatchers("/level2/**").hasRole("vip2")
        .antMatchers("/level3/**").hasRole("vip3");

    //没有权限默认到登录页
    http.formLogin();
    //开启注销功能
    //默认跳到登录页面
    http.logout();
    //指定跳到指定路径
    http.logout().logoutSuccessUrl("/");
    //关闭csrf功能,登出失败可能的原因
    http.csrf().disable();
}

2、权限控制,根据用户的不同权限动态显示不同的内容

导入依赖

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
    <version>3.1.0.M1</version>
</dependency>

在html中导入命名空间

xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <title>首页</title>
        <!--semantic-ui-->
        <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
        <link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet">
    </head>
    <body>

        <!--主容器-->
        <div class="ui container">

            <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
                <div class="ui secondary menu">
                    <a class="item"  th:href="@{/index}">首页</a>

                    <!--登录注销-->
                    <div class="right menu">
                        <!--未登录-->
                        <div sec:authorize="!isAuthenticated()">
                            <a class="item" th:href="@{/toLogin}">
                                <i class="address card icon"></i> 登录
                            </a>
                        </div>
                        <!--如果登录,显示用户名-->
                        <div sec:authorize="isAuthenticated()">
                            <a class="item">
                                用户名:<span sec:authentication="name"></span>
                                角色:<span sec:authentication="principal.authorities"></span>
                            </a>
                        </div>
                        <!--注销-->
                        <div sec:authorize="isAuthenticated()">
                            <a class="item" th:href="@{/logout}">
                                <i class="sign-out icon"></i> 注销
                            </a>
                        </div>


                        <!--已登录
<a th:href="@{/usr/toUserCenter}">
<i class="address card icon"></i> admin
</a>
-->
                    </div>
                </div>
            </div>

            <div class="ui segment" style="text-align: center">
                <h3>Spring Security Study by me</h3>
            </div>

            <div>
                <br>
                <div class="ui three column stackable grid">
                    <!--把菜单根据用户的角色动态显示-->
                    <div class="column" sec:authorize="hasRole('vip1')">
                        <div class="ui raised segment">
                            <div class="ui">
                                <div class="content">
                                    <h5 class="content">Level 1</h5>
                                    <hr>
                                    <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                                    <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                                    <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="column" sec:authorize="hasRole('vip2')">
                        <div class="ui raised segment">
                            <div class="ui">
                                <div class="content">
                                    <h5 class="content">Level 2</h5>
                                    <hr>
                                    <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                                    <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                                    <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="column" sec:authorize="hasRole('vip3')">
                        <div class="ui raised segment">
                            <div class="ui">
                                <div class="content">
                                    <h5 class="content">Level 3</h5>
                                    <hr>
                                    <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                                    <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                                    <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                                </div>
                            </div>
                        </div>
                    </div>

                </div>
            </div>

        </div>


        <script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
        <script th:src="@{/qinjiang/js/semantic.min.js}"></script>

    </body>
</html>

主要部分:

<!--未登录-->
<div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLogin}">
        <i class="address card icon"></i> 登录
    </a>
</div>
<!--如果登录,显示用户名-->
<div sec:authorize="isAuthenticated()">
    <a class="item">
        用户名:<span sec:authentication="name"></span>
        角色:<span sec:authentication="principal.authorities"></span>
    </a>
</div>
<!--注销-->
<div sec:authorize="isAuthenticated()">
    <a class="item" th:href="@{/logout}">
        <i class="sign-out icon"></i> 注销
    </a>
</div>


<div class="column" sec:authorize="hasRole('vip1')">
    
</div>

<div class="column" sec:authorize="hasRole('vip2')">
    
</div>

但以上最多支持springboot2.0.9版本

4.3、记住我和首页定制

注意,自定义登录页面后,登录请求路径也要自定义

@Override
protected void configure(HttpSecurity http) throws Exception {
    //首页所有人都可以访问,功能页只有对应权限才可以访问
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasRole("vip1")
        .antMatchers("/level2/**").hasRole("vip2")
        .antMatchers("/level3/**").hasRole("vip3");

    //没有权限默认到登录页,设置登录页面的地址和登录请求的路径
    http.formLogin().loginPage("/toLogin").loginProcessingUrl("/usr/login");
    //开启注销功能
    //默认跳到登录页面
    http.logout();
    //指定跳到指定路径
    http.logout().logoutSuccessUrl("/");
    //关闭csrf功能,登出失败可能的原因
    http.csrf().disable();

    //开启记住我功能,设置了一个cookie,默认保存两周
    http.rememberMe().rememberMeParameter("remember");
}

登录页面:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <title>登录</title>
        <!--semantic-ui-->
        <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    </head>
    <body>

        <!--主容器-->
        <div class="ui container">

            <div class="ui segment">

                <div style="text-align: center">
                    <h1 class="header">登录</h1>
                </div>

                <div class="ui placeholder segment">
                    <div class="ui column very relaxed stackable grid">
                        <div class="column">
                            <div class="ui form">
                                <form th:action="@{/usr/login}" method="post">
                                    <div class="field">
                                        <label>Username</label>
                                        <div class="ui left icon input">
                                            <input type="text" placeholder="Username" name="username">
                                            <i class="user icon"></i>
                                        </div>
                                    </div>
                                    <div class="field">
                                        <label>Password</label>
                                        <div class="ui left icon input">
                                            <input type="password" name="password">
                                            <i class="lock icon"></i>
                                        </div>
                                    </div>
                                    <div class="field">
                                        <input type="checkbox" name="remember">记住我
                                    </div>
                                    <input type="submit" class="ui blue submit button"/>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>

                <div style="text-align: center">
                    <div class="ui label">
                        </i>注册
                </div>
                <br><br>
                <small>www.baidu.com</small>
            </div>
            <div class="ui segment" style="text-align: center">
                <h3>Spring Security Study by me</h3>
            </div>
        </div>


        </div>

    <script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
    <script th:src="@{/qinjiang/js/semantic.min.js}"></script>

    </body>
</html>

注意:使用自定义页面登录时,username,password,remember的name值需要和security中的参数对应

五、Shiro

5.1、简介

1、导入依赖

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.6.0</version>
</dependency>

2、配置文件 *.ini

3、功能列举

Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
currentUser.isAuthenticated()
currentUser.getPrincipal()
currentUser.hasRole("schwartz")
currentUser.isPermitted("lightsaber:wield")
currentUser.logout();

Shiro三大组件:

Subject 用户

SecurityManager 管理所有用户

Realm 连接数据

5.2、登录拦截

编写Realm类

public class UserRealm extends AuthorizingRealm {

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

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

配置类

@Configuration
public class ShiroConfig {

    /**
     * ShiroFilterFactoryBean,需要设置defaultWebSecurityManager
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        factoryBean.setSecurityManager(defaultWebSecurityManager);
        //添加shiro的内置过滤器
        /*
            anno:无需认证就可以访问
            authc:必须认证了才能访问
            user:必须拥有 记住我 功能才能访问
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色权限才能访问
            filterMap.put("/user/add","authc");
            filterMap.put("/user/update","authc");
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/*","authc");
        factoryBean.setFilterChainDefinitionMap(filterMap);
        //设置登录的请求路径
        factoryBean.setLoginUrl("/toLogin");
        return factoryBean;
    }

    /**
     * DefaultWebSecurityManager,需要绑定Realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联userRealm
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    /**
     * 创建Realm对象,需要自定义
     * @return
     */
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

}

Controller:

@Controller
public class MyController {

    @GetMapping({"/","index","index.html"})
    public String toIndex(Model model){
        model.addAttribute("msg","hello shiro");
        return "index";
    }

    @GetMapping("/user/add")
    public String add(){
        return "user/add";
    }

    @GetMapping("/user/update")
    public String update(){
        return "user/update";
    }

    @GetMapping("/toLogin")
    public String toLogin(){
        return "login";
    }

}

image-20220329203631205

5.3、用户认证

在UserRealm中添加认证

public class UserRealm extends AuthorizingRealm {

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("授权==>doGetAuthorizationInfo");
        return null;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("认证==>doGetAuthenticationInfo");
        //用户名,密码,数据库中取得
        String username = "admin",password = "123456";
        UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
        //比对用户名
        if (!userToken.getUsername().equals(username)){
            //抛出异常UnknownAccountException
            return null;
        }
        //密码认证由shiro来做
        return new SimpleAuthenticationInfo("",password,"");
    }
}

Controller:

@PostMapping("/login")
public String login(String username, String password, Model model){
    System.out.println("Debug==>"+username);
    System.out.println("Debug==>"+password);
    //获取当前的用户
    Subject subject = SecurityUtils.getSubject();
    //封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //执行登录方法,如果没有异常就ok了
    try {
        subject.login(token);
        return "index";
    }catch (UnknownAccountException e){
        model.addAttribute("msg","用户名不存在");
        return "login";
    }catch (IncorrectCredentialsException e){
        model.addAttribute("msg","密码错误");
        return "login";

    }
}

login.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.themeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h2>登录</h2>
<p th:text="${msg}" style="color: red"></p>
<form th:action="@{/login}" method="post">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="text" name="password"></p>
    <p><input type="submit"></p>
</form>

</body>
</html>

配置类:

@Configuration
public class ShiroConfig {

    /**
     * ShiroFilterFactoryBean,需要设置defaultWebSecurityManager
     * @param defaultWebSecurityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        factoryBean.setSecurityManager(defaultWebSecurityManager);
        //添加shiro的内置过滤器
        /*
            anno:无需认证就可以访问
            authc:必须认证了才能访问
            user:必须拥有 记住我 功能才能访问
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色权限才能访问
            filterMap.put("/user/add","authc");
            filterMap.put("/user/update","authc");
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/user/*","authc");
        factoryBean.setFilterChainDefinitionMap(filterMap);
        //设置登录的请求路径
        factoryBean.setLoginUrl("/toLogin");
        return factoryBean;
    }

    /**
     * DefaultWebSecurityManager,需要绑定Realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联userRealm
        securityManager.setRealm(userRealm);
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    /**
     * 会话管理
     * @return
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 去掉shiro登录时url里的JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    /**
     * 创建Realm对象,需要自定义
     * @return
     */
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }
}

5.4、连接数据库

@Repository
@Mapper
public interface UserMapper {

    public User queryUserByName(String name);
}
public interface UserService {

    public User queryUserByName(String name);
}
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User queryUserByName(String name) {
        return userMapper.queryUserByName(name);
    }
}
<?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.ywb.mapper.UserMapper">
    <select id="queryUserByName" resultType="User" parameterType="String">
        select * from user where name = #{name}
    </select>
</mapper>

使用数据库中的数据认证登录,UserRealm中

/**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("认证==>doGetAuthenticationInfo");
    //用户名,密码,数据库中取得
    UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
    User user = userService.queryUserByName(userToken.getUsername());
    //比对用户名
    if (user == null){
        //抛出异常UnknownAccountException
        return null;
    }
    //密码认证由shiro来做,加密,可以用MD5加密,MD5盐值加密
    return new SimpleAuthenticationInfo("",user.getPwd(),"");
}

5.5、请求授权实现

1、在数据库中增加权限字段

image-20220330091616518

2、在拦截器添加拦截路径和权限判断

/**
     * ShiroFilterFactoryBean,需要设置defaultWebSecurityManager
     * @param defaultWebSecurityManager
     * @return
     */
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
    ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
    //设置安全管理器
    factoryBean.setSecurityManager(defaultWebSecurityManager);
    //添加shiro的内置过滤器
    /*
            anno:无需认证就可以访问
            authc:必须认证了才能访问
            user:必须拥有 记住我 功能才能访问
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色权限才能访问
            filterMap.put("/user/add","authc");
            filterMap.put("/user/update","authc");
         */
    Map<String, String> filterMap = new LinkedHashMap<>();

    //授权,正常情况下,未授权会跳转到一个授权页面
    filterMap.put("/user/add","perms[user:add]");
    filterMap.put("/user/update","perms[user:update]");
    filterMap.put("/user/*","authc");

    factoryBean.setFilterChainDefinitionMap(filterMap);
    //设置登录的请求路径
    factoryBean.setLoginUrl("/toLogin");
    //设置未授权页面
    factoryBean.setUnauthorizedUrl("/noauth");
    return factoryBean;
}

3、设置未授权跳转的页面

@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
    return "未经授权无法访问此页面";
}

4、根据数据库字段给用户添加权限

public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("授权==>doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //拿到当前登录的对象
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User) subject.getPrincipal();
        String perms = currentUser.getPerms();
        if (perms==null){
            return info;
        }
        String[] permsList = perms.split(",");
        for (String perm : permsList) {
            info.addStringPermission(perm);
        }
        return info;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("认证==>doGetAuthenticationInfo");
        //用户名,密码,数据库中取得
        UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
        User user = userService.queryUserByName(userToken.getUsername());
        //比对用户名
        if (user == null){
            //抛出异常UnknownAccountException
            return null;
        }
        //密码认证由shiro来做,加密,可以用MD5加密,MD5盐值加密
        return new SimpleAuthenticationInfo(user,user.getPwd(),"");
    }
}

重点:

在认证代码中将用户返回给principal

return new SimpleAuthenticationInfo(user,user.getPwd(),"");

授权代码

SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到当前登录的对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();
String perms = currentUser.getPerms();
String[] permsList = perms.split(",");
for (String perm : permsList) {
    info.addStringPermission(perm);
}

5.6、Shiro整合Thymeleaf

1、导入依赖

<!--shiro-thymeleaf整合包-->
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

2、生成ShiroDialect类

//整合shiroDialect:用来整合shiro-thymeleaf
@Bean
public ShiroDialect shiroDialect(){
    return new ShiroDialect();
}

3、导入命名空间

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

4、使用shiro标签进行判断

<!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>

<h2>首页</h2>
<h3 th:text="${msg}"></h3>

<p>
    <div shiro:notAuthenticated="">
        <a th:href="@{/toLogin}">登录</a>
    </div>
</p>

<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a>
</div>

<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>


</body>
</html>

六、Swagger

6.1、简介

  • RestFul Api 文档在线自动生成工具=> Api文档和Api定义同步更新
  • 直接运行,可以在线测试api接口
  • 支持多种语言

在项目使用Swagger需要springfox

  • swagger2
  • ui

6.2、SpringBoot集成Swagger

1、新建一个SpringBoot

2、导入依赖

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

3、编写一个Hello工程

4、配置Swagger

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    
} 

5、重点!在yml文件中配置接口文档

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

6、测试运行

访问localhost:8080/swagger-ui.html

image-20220330162813642

6.3、配置swagger

Swagger的bean实例 Docket

@Configuration
@EnableSwagger2   //开启Swagger2
public class SwaggerConfig {


    /**
     * 配置了swagger的Docket的bean实例
     * @return
     */
    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo());
    }

    /**
     * 配置swagger的信息
     */
    private ApiInfo apiInfo(){

        //作者信息
        Contact contact = new Contact("ywb", "我的网站", "862819503@qq.com");

        return new ApiInfo(
                "ywb的swaggerApi文档",
                "文档描述",
                "v1.0",
                "我的网站",
                contact,
                "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE-2.0",
                new ArrayList());
    }
}

image-20220330164108814

6.4、Swagger配置扫描接口

Docket.select()

指定扫描包

/**
     * 配置了swagger的Docket的bean实例
     * @return
     */
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .select()
        //RequestHandlerSelectors配置要扫描的接口
        //basePackage指定要扫描的包
        .apis(RequestHandlerSelectors.basePackage("com.ywb.controller"))
        .build();
}

其他指定方法

/**
     * 配置了swagger的Docket的bean实例
     * @return
     */
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .select()
        //RequestHandlerSelectors配置要扫描的接口
        //basePackage指定要扫描的包
        //any():扫描全部
        //none():都不扫描
        //withClassAnnotation():扫描类上的注解,参数是一个注解的反射对象
        //withMethodAnnotation():扫描方法上的注解
        .apis(RequestHandlerSelectors.withMethodAnnotation(GetMapping.class))
        .build();
}

通过路径扫描

/**
     * 配置了swagger的Docket的bean实例
     * @return
     */
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .select()
        //过滤什么路径,只有/admin/**的路径才能被扫描
        .paths(PathSelectors.ant("/admin/**"))
        .build();
}

总结:

方法1:apis()

/**
     * 配置了swagger的Docket的bean实例
     * @return
     */
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .select()
        //RequestHandlerSelectors配置要扫描的接口
        //basePackage指定要扫描的包
        .apis(RequestHandlerSelectors.xxxxxxxxxx)
        .build();
}

方法2:paths()

/**
     * 配置了swagger的Docket的bean实例
     * @return
     */
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .select()
        //过滤什么路径,只有/admin/**的路径才能被扫描
        .paths(PathSelectors.ant("/admin/**"))
        .build();
}

6.5、配置是否启动Swagger

**enable(false) ** 关闭Swagger

/**
     * 配置了swagger的Docket的bean实例
     * @return
     */
@Bean
public Docket docket(){
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .enable(false)
        .select()
        .paths(PathSelectors.ant("/admin/**"))
        .build();
}

6.6、配置Swagger只在生产环境中使用

  • 判断是不是生产环境
  • 注入enable()
/**
     * 配置了swagger的Docket的bean实例
     * @return
     */
@Bean
public Docket docket(Environment environment){

    //设置要显示的swagger环境
    Profiles profiles = Profiles.of("dev");

    //通过environment.acceptsProfiles判断是否处在自己设定的环境中
    boolean flag = environment.acceptsProfiles(profiles);
    System.out.println(flag);
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .enable(flag)
        .select()
        .paths(PathSelectors.ant("/admin/**"))
        .build();
}

6.7、配置API分组

配置API分组

.gropuName("xxxx")
@Bean
public Docket docket(Environment environment){

    //设置要显示的swagger环境
    Profiles profiles = Profiles.of("dev");

    //通过environment.acceptsProfiles判断是否处在自己设定的环境中
    boolean flag = environment.acceptsProfiles(profiles);
    System.out.println(flag);
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        .enable(flag)
        .groupName("ywb")
        .select()
        .paths(PathSelectors.ant("/admin/**"))
        .build();
}

配置多个API分组,即配置多个Docket

各分组扫描的包会根据自己找个Docket里面的代码指定来扫描

@Bean
public Docket docket1(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("A");
}

@Bean
public Docket docket2(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("B");
}

@Bean
public Docket docket3(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("C");
}

6.8、实体类配置

在controller接口中返回值有实体类的,能够在Swagger中扫描到该实体类

/**
 * @author ywb
 */
@Api(tags = "Hello控制类")
@RestController
public class HelloController {

    @ApiOperation("Hello控制方法")
    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }

    //只要在我们的接口中,返回值存在实体类,就会被扫描到Swagger中
    @GetMapping("/user")
    public User user(){
        return new User();
    }

    @ApiOperation("hello2控制方法")
    @GetMapping("/hello2")
    public String hello2(@ApiParam("用户名") String username){
        return "hello2"+username;
    }
}

注解方法:@ApiOperation()

注解方法参数:@ApiParm()

注解Controller类:@Api(tag = “xxx”)

注解实体类:@ApiModel(“xxx”)

注解实体类的字段:@ApiModelProperty(“xxxx”)

@ApiModel("用户实体类")
public class User {

    @ApiModelProperty("用户名")
    public String name;

    @ApiModelProperty("密码")
    public String password;
}

七、任务

7.1、异步任务

程序需要开多一个线程去执行别的方法时,可以先执行后面的方法

1、在方法上加注解**@Async**

@Service
public class AsyncService {

    /**
     * 告诉spring这是一个异步任务
     */
    @Async
    public void hello(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据正在处理。。。");
    }

}

2、在启动类上开启异步注解功能**@EnableAsync**

//开启异步注解功能
@EnableAsync
@SpringBootApplication
public class Springboot09TestApplication {

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

}

举例

@RestController
public class AsyncController {

    @Autowired
    private AsyncService asyncService;

    @GetMapping("/hello")
    public String hello(){
        asyncService.hello();
        return "ok";
    }

}

正常情况下:浏览器会转圈3秒然后再跳转视图

异步任务下:浏览器会直接跳转视图,同时3秒后输出”数据正在处理“

7.2、邮件发送

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

2、打开邮箱POP3/SMTP服务并且生成授权码

image-20220330180335403

3、配置文件

spring:
  mail:
    username: xxxxxxxxx@qq.com
    password: xxxxxxxxxxx
    host: smtp.qq.com

4、编写代码

@SpringBootTest
class Springboot09TestApplicationTests {

    @Autowired
    JavaMailSenderImpl mailSender;

    //发送一个剪简单的邮件
    @Test
    void contextLoads() {
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
        simpleMailMessage.setSubject("主题");
        simpleMailMessage.setText("文本");
        simpleMailMessage.setTo("xxxxxxxxxx@qq.com");
        simpleMailMessage.setFrom("xxxxxxxxx@qq.com");
        mailSender.send(simpleMailMessage);

    }

    //发送一个复杂的邮件
    @Test
    void contextLoads1() throws MessagingException {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        //组装
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);

        //正文
        helper.setSubject("主题");
        helper.setText("<p>文本</p>",true);

        //附件
        helper.addAttachment("1.jpg",new File("C:\\Users\\xxx的电脑\\Desktop\\1.jpg"));

        helper.setTo("xxxxxxxx@qq.com");
        helper.setFrom("xxxxxxxx@qq.com");
        mailSender.send(mimeMessage);
    }

}

7.3、定时任务

TaskScheduler		任务调度者
TaskExecutor		任务执行者

@EnableScheduling	开启定时功能的注解,加在启动类上
@Scheduled			什么时候执行

Cron表达式
/**
 * @author ywb
 */
@Service
public class ScheduledService {

    /**
     *  30 15 10 * * ?      每天10点15分30秒执行一次
     *  0/5 * * * * ?       每5秒执行一次
     *  30 0/5 10,18 * * ?  每天10点和18点,每隔5分钟执行一次
     *  cron表达式可以到网上直接生成
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void hello(){
        System.out.println("hello,你被执行了");
    }

}

八、SpringBoot整合Redis

springboot操作数据:Spring-Data jpa mongodb redis

SpringData也是和SpringBoot齐名的项目

8.1、整合测试

1、导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

说明:在springboot2.x之后,原来使用的jedis被替换为了lettuce

jedis:采用的是直连的方法,多个线程操作的话,是不安全的,如果想要避免不安全的,使用jedis pool 连接池,更像BIO模式

lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,可以减少线程数量,更像NIO模式

@Bean
@ConditionalOnMissingBean(
    name = {"redisTemplate"}
)  //我们需要自己定义一个redisTemplate来替换这个默认的
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    //默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的
    //默认的RedisTemplate,两个泛型都是Object,Object的类型,我们之后使用需要强制转换<String,Object>
    RedisTemplate<Object, Object> template = new RedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
//由于String是redis中最常使用的类型,所以单独提出来了一个Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    return new StringRedisTemplate(redisConnectionFactory);
}

2、配置连接

spring:
  redis:
    host: xxxxxxxx
    port: 6379
    database: 0
    password: xxxxxxx
#    lettuce:
#      pool:
#        max-active: 

注意:配置时一定要使用lettuce,不能用jedis

3、测试

@SpringBootTest
class Springboot10RedisApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //opsForValue   操作字符串   类似String
        //除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUD
        redisTemplate.opsForValue();
        //获取redis的连接对象
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        connection.flushAll();
        connection.flushDb();
    }

}

举例

@SpringBootTest
class Springboot10RedisApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //opsForValue   操作字符串   类似String
        //除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUD
        redisTemplate.opsForValue().set("key1","测试值");
        System.out.println(redisTemplate.opsForValue().get("key1"));
    }
}

image-20220330223341830

在redis客户端中出现乱码,redisTemplate需要序列化

image-20220330224013106

image-20220330224057032

关于对象的保存,所有的对象需要序列化,但是默认的序列化是djk序列化,我们需要自己更改

image-20220330225004523

8.2、自定义RedisTemplate模板

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    /**
     * 自己定义的RedisTemplate,固定模板
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        //我们为了自己开发方便,一般直接使用<String, Object>类型
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        //json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        //String序列化配置
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value序列化采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value序列化也采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

8.3、自定义Redis工具类

使用RedisTemplate需要频繁调用.opForxxx然后才能进行对应的操作,这样使用起来代码效率低下,工作中一般不会这样使用,而是将这些常用的公共API抽取出来封装成为一个工具类,然后直接使用工具类来间接操作Redis,不但效率高并且易用。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public final class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }


    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(String.valueOf(CollectionUtils.arrayToList(key)));
            }
        }
    }


    // ============================String=============================

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 普通缓存放入并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }


    /**
     * 递减
     * @param key   键
     * @param delta 要减少几(小于0)
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }


    // ================================Map=================================

    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashKey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * HashSet 并设置时间
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key   键
     * @param item  项
     * @param value 值
     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除hash表中的值
     *
     * @param key  键 不能为null
     * @param item 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }


    /**
     * 判断hash表中是否有该项的值
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param item 项
     * @param by   要增加几(大于0)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


    /**
     * hash递减
     *
     * @param key  键
     * @param item 项
     * @param by   要减少记(小于0)
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }


    // ============================set=============================

    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 根据value从一个set中查询,是否存在
     *
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将数据放入set缓存
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 将set数据放入缓存
     *
     * @param key    键
     * @param time   时间(秒)
     * @param values 值 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0)
                expire(key, time);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 获取set缓存的长度
     *
     * @param key 键
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 移除值为value的
     *
     * @param key    键
     * @param values 值 可以是多个
     * @return 移除的个数
     */

    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ===============================list=================================

    /**
     * 获取list缓存的内容
     *
     * @param key   键
     * @param start 开始
     * @param end   结束 0 到 -1代表所有值
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 获取list缓存的长度
     *
     * @param key 键
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }


    /**
     * 通过索引 获取list中的值
     *
     * @param key   键
     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 将list放入缓存
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }


    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0)
                expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 根据索引修改list中的某条数据
     *
     * @param key   键
     * @param index 索引
     * @param value 值
     * @return
     */

    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */

    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }

    }

}

九、分布式 Dubbo+Zookeeper+SpringBoot

9.1、什么是分布式系统?

在《分布式系统原理与范型》一书中有如下定义:”分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统“

分布式系统是一组通过网络进行通信,为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。

分布式系统(distributed system)是建立在网络之上的软件系统

首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存,加磁盘,使用更好的cpu)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题

单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键

适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。

image-20220330233552322

缺点:

1、性能扩展比较难

2、协同开发问题

3、不利于升级维护

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键

image-20220330233609747

通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。

缺点:公用模块无法重复利用,开发性的浪费

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速地响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键

image-20220330233837833

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[Service Oriented Architecture]是关键。

image-20220330234030202

9.2、RPC

RPC是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数。

RPC基本原理

image-20220330234830194

RPC两个核心模块:通讯、序列化(数据传输需要转换)

9.3、Dubbo

什么是dubbo

Apache Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现

image-20220330235614025

服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务

服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者

监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

9.4、Zookeeper

1、下载

Apache ZooKeeper

下载压缩包解压即可

2、运行/bin/zkServer.cmd,初次运行可能会报错

可能遇到的问题:闪退

解决方案:如下图,添加一个pause查看报错信息

image-20220331001136598

3、修改zoo.cfg配置文件

将conf文件夹下的zoo_sample.cfg一份,命名为zoo.cfg

注意几个重要位置:

dataDir=./ 临时数据存储的目录(可写相对路径)

clientPort=2181 zookeeper的端口号

修改完成后再次启动zookeeper

4、使用zkCli.cmd测试

ls /:列出zookeeper根下保存的所有节点

image-20220331001832514

[zk:127.0.0.1:2181(CONNECTED) 4] ls /
[zookeeper]

create -e /jiedian 123:创建一个jiedian节点,值为123

image-20220331001910781

[zk:localhost:2181(CONNECTED) 0] create -e /jiedian 123
Created /jiedian

get /jiedian:获取/jiedian节点的值

image-20220331001934105

[zk: localhost:2181(CONNECTED) 3] get /yang
123

9.5、安装dubbo

1、下载dubbo-admin

https://github.com/apache/dubbo-admin/tree/master

2、解压进入目录

修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址

server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest

dubbo.registry.address=zookeeper://127.0.0.1:2181

3、在项目目录下打包dubbo-admin

mvn clean package -Dmaven.test.skip=true

第一次打包过程有点慢

image-20220331003629148

4、执行 dubbo-admin\target下的dubbo-admin-0.0.1-SNAPSHOT.jar

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

同时需要打开zookeeper服务

执行完毕,访问localhost:7001,需要我们输入账号和密码,我们都是默认的root-root

登录成功后,查看界面

image-20220331004257533

dubbo-admin:是一个监控管理后台,可以查看我们注册了哪些服务,哪些服务被消费了(不是必须的)

zookeeper:注册中心

Dubbo:jar包

9.6、实战

<!--导入dubbo依赖-->
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.8</version>
</dependency>

<!--导入zkclient依赖-->
<!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
<dependency>
    <groupId>com.github.sgroschupf</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.1</version>
</dependency>

<!--日志会冲突-->
<!-- 引入zookeeper -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
    <!--排除这个slf4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

服务模块

1、编写服务模块和消费模块

image-20220331010206147

服务模块

image-20220331010235665

Service类:

提供服务的类上面一定要加一个**@DubboService**

@DubboService   //使用这个注解之后就可以被dubbo扫描到
@Component   //使用了Dubbo后不要用Service注解,容易导错包
public class TicketServiceImpl implements TicketService{
    @Override
    public String getTicket() {
        return "获取票";
    }
}

2、配置文件信息

server.port=8081

#服务应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#哪些服务要被注册
dubbo.scan.base-packages=com.ywb.service

3、启动zookeeper,即打开zkServer.cmd

4、启动服务模块项目

5、启动dubbo-admin,即jar包

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

6、可访问localhost:7001查看服务注册信息

image-20220331010526619

image-20220331010553494

消费模块

1、导入依赖

2、配置服务信息

server.port=8082

#消费者去哪里拿,同时需要暴露自己的名字
dubbo.application.name=consumer-server
#注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

3、编写服务

远程注入@DubboReference

import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    //想拿到provider-server提供的票,要去注册中心拿到服务
    //引用,但是这边没有TicketService,可以使用pom坐标,也可以定义路径相同的接口名
    @DubboReference
    TicketService ticketService;

    public void buyTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心拿到一张票==>"+ticket);
    }

}

image-20220331011720088

4、测试运行

@SpringBootTest
class ConsumerServerApplicationTests {

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        userService.buyTicket();
    }

}

/*
结果:在注册中心拿到一张票==>获取票
*/
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值