微服务学习笔记1-SpringBoot

SpringBoot

1、SpringBoot简介

SpringBoot是一个javaweb的开发框架,简化开发,约定大于配置!

SpringBoot的主要优点:

  • 让Spring开发者更快的入门
  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化Web项目
  • 没有冗余代码生成和XML配置的要求

2、微服务简介

  • 单体架构:打包成一个独立的单元(导入一个jar包或者是一个war包)部署完成应用之后,应用通过一个进程的方式来运行,例如,MVC三层架构
  • 微服务架构:一个大型的复杂软件应用,由一个或者多个微服务组成,系统中的各个微服务可以被独立部署,各个微服务之间是松耦合的,每个微服务仅仅关注于完成一件任务并很好的完成该任务

3、第一个SpringBoot程序

  • 使用idea创建工程

    • 1、创建一个新项目
    • 2、选择spring initalizr
    • 3、填写项目信息
    • 4、选择初始化的组件(勾选 Web:Spring Web )
    • 5、填写项目路径
    • 6、等待项目构建成功
  • 项目结构分析src

    • main/java底下程序主启动类:TestApplication.java
    • main/resources底下配置文件:application.properties,语法结构 :key=value,只能键值对,中文会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8;更推荐使用application.yaml
    • test/java底下测试类:TestApplicationTests.java
    • pom.xml依赖:
      • spring-boot-starter-parent核心依赖,导入依赖默认是不需要写版本,但是如果导入的包没有在依赖中管理着就需要手动配置版本
      • spring-boot-starter-xxx是spring-boot的场景启动器,要用什么功能就导入什么样的场景启动器即可,例如spring-boot-starter-web帮我们导入了web模块正常运行所依赖的组件
  • 编写一个http接口

    • 在主程序的同级目录新建controller包

    • 在controller包下新建一个HelloController类

      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;
      
      @RestController
      public class HelloController {
      
          @RequestMapping("/hello")
          public String hello(){
              return "hello world";
          }
      }
      
  • 浏览器访问http://localhost:8080/hello,页面出现hello world

  • 点击idea上Maven的Lifecycle/package打成jar包,在target目录下生成一个jar包,可以在任何地方运行

4、yaml语法

  • 空格不能省略
  • 以缩进来控制层级关系,左边对齐的一列数据都是同一个层级的
  • 属性和值的大小写敏感
#数字,布尔值,字符串
k1: v1

#对象、map
k2:
  v1:1
  v2:2
#对象、map-行内写法
k22: {v1:1,v2:2}

#数组
k3:
  - v1
  - v2
#数组-行内写法
k33: [v1,v2]

#使用占位符生成随机数
name: ${random.uuid}
age: ${random.int}

#松散绑定:last-name和lastName一样,-后面的字母默认是大写

5、属性赋值

5.1、@Value方式

  • 实体类 Dog
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

//注册bean
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Dog {
    @Value("三七")
    private String name;
    @Value("2")
    private Integer age;
}
  • 测试类中测试
@SpringBootTest
class TestApplicationTests {

    @Autowired
    private Dog dog;

    @Test
    void contextLoads() {
        System.out.println(dog);
    }
}

//控制台打印:Dog(name=三七, age=2)

5.2、yaml注入方式

  • 在springboot项目中的resources目录下新建一个文件 application.yaml
cat:
  name: 二十一
  age: 1
  • 实体类Cat
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

//注册bean
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
//将配置文件中配置的每一个属性的值,映射到这个组件中,prefix指定key
@ConfigurationProperties(prefix = "cat")
public class Cat {
    private String name;
    private Integer age;
}
  • pom.xml导入依赖
<!-- 导入配置文件处理器,需要重启idea -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
  • 测试类中测试
@SpringBootTest
class TestApplicationTests {

    @Autowired
    private Cat cat;

    @Test
    void contextLoads() {
        System.out.println(cat);
    }
}
//控制台打印:Cat(name=二十一, age=1)

5.3、指定配置文件方式

  • 在springboot项目中的resources目录下新建一个文件 pig.properties
name=小猪
age=1
  • 实体类Pig
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

//注册bean
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
//加载指定的配置文件
@PropertySource("classpath:pig.properties")
public class Pig {
    //指定属性值
    @Value("${name}")
    private String name;
    @Value("${age}")
    private Integer age;
}
  • 测试类中测试
@SpringBootTest
class TestApplicationTests {

    @Autowired
    private Pig pig;

    @Test
    void contextLoads() {
        System.out.println(pig);
    }
}
//控制台打印:Pig(name=小猪, age=1)

6、JSR303数据校验

  • pom.xml导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • people.yaml
people:
  name: 小雷
  age: 18
  email: 123456789@qq.com
  • 实体类People
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties(prefix = "people")
//@Validated数据校验
@Validated
public class People {
    @NotNull(message = "用户名不能为空")
    private String userName;
    @Max(value = 100,message = "年龄不能超过100岁")
    private int age;
    @Email(message = "邮箱格式错误")
    private String email;
}

/**
 * 空检查
 * @Null 验证对象是否为null
 * @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
 * @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0, 只对字符串, 且会去掉前后空格.
 * @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
 
 * Booelan检查
 * @AssertTrue 验证 Boolean 对象是否为 true  
 * @AssertFalse 验证 Boolean 对象是否为 false  
 
 * 长度检查
 * @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
 * @Length(min=, max=) string is between min and max included.
 
 * 日期检查
 * @Past 验证 Date 和 Calendar 对象是否在当前时间之前  
 * @Future 验证 Date 和 Calendar 对象是否在当前时间之后  
 * @Pattern 验证 String 对象是否符合正则表达式的规则

 * */

7、多环境配置

7.1、使用properties

新建application-test.properties 代表测试环境配置

新建application-dev.properties 代表开发环境配置

springboot默认使用application.properties,需要在改该文件里指定使用的环境

spring.profiles.active=dev

7.2、使用yaml多文档块

不需要创建多个yaml,只需要在application.yaml中配置多个环境即可

#选择要激活那个环境块
spring:
  profiles:
    active: dev
---
server:
  port: 8082
spring:
  profiles: dev #配置环境的名称
---
server:
  port: 8083
spring:
  profiles: prod  #配置环境的名称

8、SpringBoot Web开发

8.1、静态资源映射规则

8.1.1、webjars

访问:localhost:8080/webjars/xxx

  • idea中按shift+shift,搜索WebMvcAutoConfigurationAdapter类(SpringBoot中,SpringMVC的web配置类),有一个方法addResourceHandlers是用来添加资源处理

  • 所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源

  • Webjars本质就是以jar包的方式引入静态资源 ,直接导入即可

  • 例如,pom.xml中引入jquery,访问http://localhost:8080/webjars/jquery/3.4.1/jquery.js即可

public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
    } else {
        this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
        this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
            registration.addResourceLocations(this.resourceProperties.getStaticLocations());
            if (this.servletContext != null) {
                ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
                registration.addResourceLocations(new Resource[]{resource});
            }
        });
    }
}

8.1.2、resources、static、public

访问:localhost:8080/xxx

  • ResourceProperties类可以设置和我们静态资源有关的参数,指向了它会去寻找资源的文件夹
以下四个目录存放的静态资源都可以被识别,优先级resources>static>public
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"

resources根目录下新建对应的文件夹,都可以存放我们的静态文件

8.2、首页、图标、404定制

  • 页面:

    • 在resources下的文件夹下,新建index.html,被 /** 映射
    • 访问http://localhost:8080/展示index页面
  • 图标:

    • 在resources下的文件夹下,放入图标文件favicon.ico

    • 关闭SpringBoot默认图标

      spring.mvc.favicon.enabled=false
      
    • 清除浏览器缓存,刷新网页显示图标

  • 404页面:

    • 在template文件夹下创建error文件夹,创建404.html页面,访问到未知url时,springboot会自动显示404页面

8.3、Thymeleaf模板引擎

8.3.1、Thymeleaf使用方法

  • pom.xml导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
  • 只需要把页面放在templates底下就可以被自动识别

    • 在templates下新建test.html

      <!DOCTYPE html>
      <!--导入thymeleaf命名空间的约束-->
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
      </head>
      <body>
          <!--th:text将div中的内容设置为它指定的值-->
          <div th:text="${msg}"></div>
      </body>
      </html>
      
    • controller

      import org.springframework.stereotype.Controller;
      import org.springframework.ui.Model;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      @Controller
      public class TestController {
      
          @RequestMapping("/test")
          public String test(Model model){
              //存入数据
              model.addAttribute("msg","thymeleaf模板引擎测试");
              //classpath:/templates/test.html
              return "test2";
          }
      }
      
    • 浏览器访问http://localhost:8080/test,展示test.html的内容,msg值为controller中传入的

8.3.2、Thymeleaf语法

参考网上即可…

  • th属性,常用th属性如下:
    • th:text:文本替换
    • th:utext:支持html的文本替换
    • th:value:属性赋值
    • th:each:遍历循环元素
    • th:if:判断条件,类似的还有th:unless,th:switch,th:case
    • th:insert:代码块引入,类似的还有th:replace,th:include,常用于公共代码块提取的场景
    • th:fragment:定义代码块,方便被th:insert引用
    • th:object:声明变量,一般和*{}一起配合使用,达到偷懒的效果
    • th:attr:设置标签属性,多个属性可以用逗号分隔

8.4、员工管理系统实战-无数据库

无数据库,使用模拟数据(首页、登录拦截、增删改查、404页面)

​ 链接:https://pan.baidu.com/s/1pL55cudzdVC-gPUpJM9_gw
​ 提取码:8qzo

9、SpringBoot整合数据库

9.1、整合JDBC

  • pom.xml导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
  • application.yaml配置
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/ums?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
  • dataSource连接测试
@Autowired
DataSource dataSource;

@Test
void contextLoads() throws SQLException {
    Connection connection = dataSource.getConnection();
    connection.close();
}
  • JdbcTemplate增删改查
@Autowired
JdbcTemplate jdbcTemplate;
/**
 * Spring Boot默认提供了数据源,默认提供了JdbcTemplate
 * JdbcTemplate自动注入数据源,自动连接和关闭,用于简化JDBC操作
 *
 * 查询query、queryForXXX
 * 新增、修改、删除:update、batchUpdate
 * 执行DDL语句:execute,例如create、alter、drop、truncate
 * 执行存储过程、函数相关语句:call
 */

@Test
void contextLoads2() throws SQLException {
    //查询
    List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from employee");
    System.out.println(maps);

    //新增
    jdbcTemplate.update("insert into employee(ename, email, gender, birthday, did) VALUES ('test', '1234567@qq.com', 1, '2022-03-11 10:39:42', 101)");

    //修改
    Object[] objects = new Object[2];
    objects[0]="姓名修改";
    objects[1]=1;
    jdbcTemplate.update("update employee set ename=? where id=?",objects);

    //删除
    Object[] objects1 = new Object[1];
    objects[0]=1;
    jdbcTemplate.update("delete employee where id=?",objects1);
}

9.2、整合Druid

Druid是监控 DB 池连接和 SQL 的执行情况连接池

  • pom.xml导入依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>

<!--监控用到-->
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
  • application.yaml配置
spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/ums?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.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注入
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  • Druid配置类:数据源属性注入、数据源监控、监控过滤器
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;

@Configuration
public class DruidConfig {

    /**
     * 将自定义的 Druid数据源添加到容器中,不再让SpringBoot自动创建
     * 将全局配置文件中前缀为spring.datasource的属性值注入到com.alibaba.druid.pool.DruidDataSource的同名参数中
    */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    /**
     * Druid 数据源具有监控的功能,并提供了一个 web界面方便用户查看
     * 配置Druid数据源监控
     * 启动项目后,访问http://localhost:8080/druid/login.html
     */
    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        HashMap<String, String> initParams = new HashMap<>();
        //后台登录账号密码
        initParams.put("loginUsername", "admin");
        initParams.put("loginPassword", "123456");
        //后台允许谁可以访问:localhost只允许本机访问,为空或者为null允许所有访问
        initParams.put("allow", "");
        //后台拒绝谁访问
        //initParams.put("username", "192.168.1.20");

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

    /**
     * 配置 Druid web监控 filter过滤器
     */
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new WebStatFilter());
        //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
        HashMap<String, String> initParams = new HashMap<>();
        initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
        bean.setInitParameters(initParams);
        //"/*" 表示过滤所有请求
        bean.setUrlPatterns(Arrays.asList("/*"));
        return bean;
    }
}
  • dataSource连接测试,看到配置参数已经生效
@Autowired
DataSource dataSource;

@Test
void contextLoads() throws SQLException {
    Connection connection = dataSource.getConnection();
    DruidDataSource druidDataSource = (DruidDataSource) dataSource;
    //打印druidDataSource 数据源最大连接数:20
    System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
    connection.close();
}

9.3、整合mybatis(重点)

  • 数据库准备
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department`  (
  `id` int(11) NOT NULL,
  `departmentName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `department` VALUES (101, '教学部');
INSERT INTO `department` VALUES (102, '市场部');
INSERT INTO `department` VALUES (103, '教研部');
INSERT INTO `department` VALUES (104, '运营部');
INSERT INTO `department` VALUES (105, '后勤部');
  • pom.xml导入依赖
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
  • application.yaml中datasource配置不变(同章节10),新增mybatis配置
mybatis:
  type-aliases-package: com.leijiao.system.pojo # 实体类路径
  mapper-locations: classpath:mybatis/mapper/*.xml # maper.xml路径(resource底下)
  • pojo
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
//部门表
public class Department {
    private Integer id;
    private String dname;

}
  • mapper
import com.leijiao.system.pojo.Department;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface DepartmentMapper {
    // 获取所有部门信息
    List<Department> getAllDepartments();

    // 通过id获得部门
    Department getDepartmentById(@Param("id") int id);
}
  • 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.leijiao.system.mapper.DepartmentMapper">

    <select id="getAllDepartments" resultType="Department">
        select * from department
    </select>

    <select id="getDepartmentById" resultType="Department" parameterType="int">
        select * from department where id=#{id}
    </select>

</mapper>
  • 测试
@Autowired
DepartmentMapper departmentMapper;
@Test
void contextLoads3(){
    System.out.println(departmentMapper.getAllDepartments());
}

12、安全框架

  • 用户认证(Authentication):凭据,如用户名/密码
  • 用户授权(Authorization):用户认证后,授予访问资源的完全权限
  • Spring Security功能比shiro强大,shiro更加简化。大项目用Spring Security,小项目用shiro

12.1、Spring Security

  • pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • Spring Security配置类,继承WebSecurityConfigurerAdapter
    • 重写configure(HttpSecurity http)方法,定制授权规则,常用方法如下
      • formLogin():开启自动配置的登录功能,表单身份验证
      • logout():开启自动配置的注销的功能
      • rememberMe():记住我
      • authorizeHttpRequests():访问放行/限制
      • csrf():CSRF支持,跨站请求伪造
    • 重写configure(AuthenticationManagerBuilder auth)方法, 定义认证规则
      • inMemoryAuthentication():内存验证
      • jdbcAuthentication():基于JDBC的验证
      • userDetailsService:添加UserDetailsService
      • authenticationProvider(AuthenticationProvider authenticationProvider):添加AuthenticationProvider
      • ldapAuthentication():LADP验证
    • 重写configure(WebSecurity web)方法:用于配置静态资源的处理方式,可使用 Ant 匹配规则
package com.leijiao.system.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


@Configuration
//开启WebSecurity模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 定制授权规则
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        /**
         * antMatchers("/").permitAll():放行,首页所有人可以访问
         *.anyRequest().authenticated():所有请求都要认证
         */
        http.authorizeHttpRequests().antMatchers("/","/index.html").permitAll()
            	.antMatchers("/user/login").permitAll()
                .antMatchers(
                        "/**/*.css",
                        "/**/*.js",
                        "/**/*.svg"
                ).permitAll()
                .anyRequest().authenticated()
        /**
         * 关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
         */
        http.csrf().disable();

        /**
         * 开启自动配置的登录功能:如果没有权限,就会跳转到登录页面
         * loginPage("/") 指定登录页
         * loginProcessingUrl("/user/login") 指定登录提交页
         * /login?error 重定向到这里表示登录失败
         */
        http.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginPage("/") 
                .loginProcessingUrl("/user/login");

        /**
         * 开启自动配置的注销的功能
         * /logout 注销请求
         * .logoutSuccessUrl("/"); 注销成功来到首页
         */

        http.logout().logoutSuccessUrl("/");

        /**
         * 记住我
         */
        http.rememberMe().rememberMeParameter("remember");

    }

    /**
     * 定义认证规则
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         * 可以在内存中定义,也可以在jdbc中取
         * bcrypt密码加密
         */
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("admin");
    }
}

12.2、shiro

12.2.1、简介

  • GitHub:https://github.com/apache/shiro.git

  • Shiro三大功能模块

    • Subject:主体,一般指用户(把操作交给SecurityManager)
    • SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件(关联Realm)
    • Realms:用于进行权限信息的验证,shiro链接数据的桥梁
  • 细分功能

    • Authentication:身份认证/登录,验证用户是否拥有相应的身份(账号密码验证)
    • Authorization:授权,验证某个已认证的用户是否拥有某权限
    • Session Manager:会话管理,用户登录后,用户信息保存在session会话中
    • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
    • Web Support:Web支持,集成Web环境
    • Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中
    • Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去
    • Testing:测试支持
    • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
    • Remember Me:记住我

12.2.2、quickstart分析

  • 分析GitHub下载\shiro-main\samples\quickstart.java
//获取当前用户
Subject currentUser = SecurityUtils.getSubject();
//获取当前用户session
Session session = currentUser.getSession();
//判断当前用户是否被认证
currentUser.isAuthenticated();
//token令牌
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
//记住我
token.setRememberMe(true);
//登录
currentUser.login(token);
//获取当前用户的认证
currentUser.getPrincipal();
//判断当前用户是否拥有xx角色
currentUser.hasRole("xx");
//判断当前用户是否拥有xx权限
currentUser.isPermitted("xx");
//注销
currentUser.logout()   

12.2.3、shiro内置过滤器

  • 认证相关
    • anon:/admins/**=anon # 表示该 uri 可以匿名访问
    • authc:/admins/**=authc # 表示该 uri 需要认证才能访问
    • authcBasic:/admins/**=authcBasic # 表示该 uri 需要 httpBasic 认证
    • logout:/logout=logout # 表示注销
    • user:/admins/**=user # 表示该 uri 需要认证或记住我认证才能访问
  • 授权相关
    • roles:/admins/**=roles[admin] # 表示该 uri 需要认证用户拥有 admin 角色才能访问
    • perms:/admins/**=perms[user:add:*] # 表示该 uri 需要认证用户拥有 user:add:* 权限才能访问
    • port:/admins/**=port[8081] # 表示该 uri 需要使用 8081 端口
    • rest:/admins/**=rest[user] # 相当于 /admins/**=perms[user:method],其中,method 表示 get、post、delete等
    • ssl:/admins/**=ssl # 表示该 uri 需要使用 https 协议
    • noSessionCreation:需要指定权限才能访问

12.2.4、springboot整合shiro

主要就是CustonRealm类、shiroConfig类

  • pom.xml依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.8.0</version>
</dependency>
  • 编写CustonRealm类
package com.leijiao.shiro02.config;

import com.leijiao.shiro02.pojo.User;
import com.leijiao.shiro02.service.impl.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    UserServiceImpl userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //获取当前用户的权限(数据库中存的)
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User)subject.getPrincipal();
        //设置当前用户的权限
        info.addStringPermission(currentUser.getPermission());
        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
        //用户名和密码从数据库取
        User user = userService.getUserByUsername(token.getUsername());
        //校验用户名
        if (user==null){
            return null;//抛出异常UnknownAccountException
        }
        //校验密码
        return new SimpleAuthenticationInfo(user,user.getPassword(),"");
    }
}
  • 编写shiroConfig类
package com.leijiao.shiro02.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;

@Configuration
public class ShiroConfig {
    /** shiro三大对象
     * Subject 用户
     * SecurityManager 安全管理器:管理用户(关联Realm)
     * Realms 用户数据和Shiro数据交互的桥梁
     * @return
     */

    //3、获得过滤器,设置安全管理器
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        //设置登录请求
        shiroFilterFactoryBean.setLoginUrl("/login");
        //设置未授权的请求
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        //添加shiro内置过滤器
        HashMap<String, String> filterChainDefinitionMap = new HashMap<>();
        //认证
        filterChainDefinitionMap.put("/user/**","authc");
        //授权
        filterChainDefinitionMap.put("/user/add","perms[test:add]");
        filterChainDefinitionMap.put("/user/edit","perms[test:edit]");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    //2、获得安全管理器,关联Realm
    @Bean(name = "securityManager")
    public DefaultSecurityManager defaultWebSecurityManager(@Qualifier("customRealm") CustomRealm customRealm){
        DefaultSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //关联Realm
        defaultWebSecurityManager.setRealm(customRealm);
        return defaultWebSecurityManager;
    }

    //1、创建Realm对象
    @Bean
    public CustomRealm customRealm(){
        return new CustomRealm();
    }

}
  • controller
package com.leijiao.shiro02.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyController {

    @RequestMapping(value = {"/","/login"}, method = RequestMethod.GET)
    public String login(){
        return "login";
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(String username, String password,Model model){
        //获取当前用户
        Subject subject = SecurityUtils.getSubject();
        //封装用户登录数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
        } catch (UnknownAccountException uae) {//用户名不存在
            model.addAttribute("msg","用户名不存在");
            return "login";
        }catch (IncorrectCredentialsException ice){//密码错误
            model.addAttribute("msg","密码错误");
            return "login";
        }
        return "home";
    }

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

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

    @RequestMapping("/user/edit")
    public String editUser(){
        return "user/edit";
    }
}
  • pojo、mapper、service、template省略

12.2.5、shiro整合thymeleaf

  • pom.xml依赖
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.1.0</version>
</dependency>
  • shiroConfig类中加入方法:shiro标签与thymeleaf标签结合
//整合thymeleaf
@Bean
public ShiroDialect shiroDialect(){
    return new ShiroDialect();
}
  • 页面需要引入命名空间 xmlns:shiro=“http://www.pollix.at/thymeleaf/shiro”,使用标签shiro:xxx
<!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>
    首页
    <br>
    <!--  有权限时显示a标签,无权限不显示  -->
    <div shiro:hasPermission="test:add">
        <a th:href="@{/user/add}">新增用户</a>
    </div>
    <br>
    <div shiro:hasPermission="test:edit">
        <a th:href="@{/user/edit}">修改用户</a>
    </div>

</body>
</html>

13、SpringBoot集成Swagger

13.1、简介

  • 官网:https://swagger.io/

  • Swagger是一款让你更好的书写API文档规范且完整的框架

  • Restful Api 文档在线自动生成器:API 文档 与API 定义同步更新

  • 直接运行,在线测试API

13.2、使用方法

  • pom.xml导入依赖
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger2</artifactId>
   <version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger-ui</artifactId>
   <version>2.9.2</version>
</dependency>
  • SwaggerConfig配置类
import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2 //开启Swagger2的自动配置
public class SwaggerConfig {
}
  • 随便写一个controller,启动项目测试,访问http://localhost:8080/swagger-ui.html
    • 若是出现报错:Failed to start bean ‘documentationPluginsBootstrapper’,可能是版本问题。解决方法:降低springboot版本到2.6.0以下(pom.xml中修改)

在这里插入图片描述

13.3、配置Swagger信息

在SwaggerConfig类中配置

/**
 * 配置swagger信息.apiInfo()
 * 定义apiInfo,配置swagger的Docket实例,关联apiInfo
 */
//配置swagger信息,主要是标题、描述
private ApiInfo apiInfo() {
    Contact contact = new Contact("leijiao", "http://xxx.xxx.com", "12345678@qq.com");
    return new ApiInfo(
            "Swagger标题", // 标题
            "Swagger描述", // 描述
            "v1.0", // 版本
            "http://terms.service.url", // 组织链接
            contact, // 联系人信息
            "Apach 2.0", // 许可
            "https://www.apache.org/licenses/LICENSE-2.0", // 许可连接
            new ArrayList<>()// 扩展
    );
}
//配置swagger的Docket实例
@Bean
public Docket docket(){
    //关联apiInfo()
    return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}

13.4、配置扫描接口

在SwaggerConfig类中配置

/**
 * 扫描接口 .select().apis().build()
 *    RequestHandlerSelectors:扫描接口的方式
 *       - basePackage():指定要扫描的包
 *       - any():扫描所有
 *       - none():不扫描
 *       - withClassAnnotation(Controller.class):扫描类上的注解
 *       - withMethodAnnotation(GetMapping.class):扫描方法上的注解
 *
 * 扫描接口 .select().paths().build()
 *   PathSelectors:扫描接口的方式
 *      - any():扫描所有
 *      - none():不扫描
 *      - regex():通过正则表达式控制
 *      - ant():路径匹配
 */

//配置swagger的Docket实例
@Bean
public Docket docket(){
    //关联apiInfo
    return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
        //扫描接口:只扫描com.leijiao.swaggertest.controller包下的接口
      .select().apis(RequestHandlerSelectors.basePackage("com.leijiao.swaggertest.controller")).build();
}

13.4、配置Swagger开关

在SwaggerConfig类中配置:动态配置当项目处于test、dev环境时显示swagger,处于rd时不显示

//配置Swagger开关 .enable(b)

//配置swagger的Docket实例
@Bean
public Docket docket(Environment environment){
    // 设置要显示swagger的环境
    Profiles of = Profiles.of("dev", "test");
    // 判断当前是否处于该环境
    boolean b = environment.acceptsProfiles(of);

    //关联apiInfo
    return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
            //配置是否启用Swagger,如果是false,在浏览器将无法访问
            .enable(b)
            //扫描接口:只扫描com.leijiao.swaggertest.controller包下的接口
            .select().apis(RequestHandlerSelectors.basePackage("com.leijiao.swaggertest.controller")).build();
}

13.5、配置API分组

//配置API分组 .groupName("hello")

//若需配置多个分组只需要配置多个docket即可    
@Bean
public Docket docket1(){
   return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
@Bean
public Docket docket2(){
   return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}

13.6、常用注解

  • @Api(tags = “xxx模块说明”):作用在controller模块类上
  • @ApiOperation(“xxx接口说明”):作用在controlle接口方法上
  • @ApiParam(“xxx参数说明”):作用在参数、方法和字段上
  • @ApiModel(“xxxPOJO说明”):作用在pojo实体类上
  • @ApiModelProperty(value = “xxx属性说明”,hidden = true):作用在pojo实体类的属性上

14、任务相关

14.1、异步任务

先响应,后台继续执行

//在service中的方法上添加该注解,表示异步方法
@Async

//在主程序类上添加该注解,开启异步功能
@EnableAsync

14.2、邮件任务

  • pom.xml导入依赖
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
  • application.properties配置
spring.mail.username=XXXXXXXXX@qq.com
spring.mail.password=你的qq授权码
spring.mail.host=smtp.qq.com
# qq需要配置ssl
spring.mail.properties.mail.smtp.ssl.enable=true
  • 测试
@Autowired
JavaMailSenderImpl mailSender;

@Test
public void contextLoads() {
   //一个简单的邮件
   SimpleMailMessage message = new SimpleMailMessage();
   message.setSubject("主题");
   message.setText("内容");	
   message.setTo("xxx@qq.com");//接收方
   message.setFrom("xxx3@qq.com");//发送方
   mailSender.send(message);
}

@Test
public void contextLoads2() throws MessagingException {
   //一个复杂的邮件
   MimeMessage mimeMessage = mailSender.createMimeMessage();
   MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
   helper.setSubject("主题");
   helper.setText("<b style='color:red'>内容</b>",true);
   helper.addAttachment("1.jpg",new File(""));//发送附件
   helper.setTo("xxx@qq.com");
   helper.setFrom("24736743@qq.com");
   mailSender.send(mimeMessage);
}

14.3、定时任务

//在service中的方法上添加该注解,表示定时方法
@Scheduled(cron = "* * * * * *") 

//在主程序类上添加该注解,开启定时功能
@EnableScheduling
    /**
     * crom表达式:秒、分、时、日、月、周几
     * 常用:
     *      0/2 * * * * ?   表示每2秒 执行任务
     *      0 0/2 * * * ?   表示每2分钟 执行任务
     *      0 0 2 1 * ?   表示在每月的1日的凌晨2点调整任务
     *      0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业
     *      0 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行作
     *      0 0 10,14,16 * * ?   每天上午10点,下午2点,4点
     *      0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时
     *      0 0 12 ? * WED   表示每个星期三中午12点
     *      0 0 12 * * ?   每天中午12点触发
     *      0 15 10 ? * *   每天上午10:15触发
     *      0 15 10 * * ?     每天上午10:15触发
     *      0 15 10 * * ?   每天上午10:15触发
     *      0 15 10 * * ? 2005   2005年的每天上午10:15触发
     *      0 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触发
     *      0 0/5 14 * * ?   在每天下午2点到下午2:55期间的每5分钟触发
     *      0 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
     *      0 0-5 14 * * ?   在每天下午2点到下午2:05期间的每1分钟触发
     *      0 10,44 14 ? 3 WED   每年三月的星期三的下午2:10和2:44触发
     *      0 15 10 ? * MON-FRI   周一至周五的上午10:15触发
     *      0 15 10 15 * ?   每月15日上午10:15触发
     *      0 15 10 L * ?   每月最后一日的上午10:15触发
     *      0 15 10 ? * 6L   每月的最后一个星期五上午10:15触发
     *      0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发
     *      0 15 10 ? * 6#3   每月的第三个星期五上午10:15触发
     */

15、SpringBoot集成redis

  • pom.xml导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • application.properties配置
# Redis数据库索引(默认为0)  
spring.redis.database=0  
# Redis服务器地址  
spring.redis.host=127.0.0.1 
# Redis服务器连接端口  
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)  
spring.redis.password=123456  
# 连接池最大连接数(使用负值表示没有限制)  
spring.redis.pool.max-active=200  
# 连接池最大阻塞等待时间(使用负值表示没有限制)  
spring.redis.pool.max-wait=-1  
# 连接池中的最大空闲连接  
spring.redis.pool.max-idle=10 
# 连接池中的最小空闲连接  
spring.redis.pool.min-idle=0  
# 连接超时时间(毫秒)  
spring.redis.timeout=1000
  • 配置类RedisConfig
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Qualifier;
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 {

    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        //json序列化配置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        //String序列化配置
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key的序列化采用StringRedisSerializer
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        //value值的序列化采用jackson2JsonRedisSerializer
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        return template;
    }
}
  • 工具类RedisUtils
package com.leijiao.redistest.utils;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Component
@SuppressWarnings({"unchecked", "all"})
public class RedisUtils {
    private static final Logger log = LoggerFactory.getLogger(RedisUtils.class);
    private RedisTemplate<Object, Object> redisTemplate;


    public RedisUtils(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

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

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

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

    /**
     * 查找匹配key
     *
     * @param pattern key
     * @return /
     */
    public List<String> scan(String pattern) {
        ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection rc = Objects.requireNonNull(factory).getConnection();
        Cursor<byte[]> cursor = rc.scan(options);
        List<String> result = new ArrayList<>();
        while (cursor.hasNext()) {
            result.add(new String(cursor.next()));
        }
        try {
            RedisConnectionUtils.releaseConnection(rc, factory);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return result;
    }

    /**
     * 分页查询 key
     *
     * @param patternKey key
     * @param page       页码
     * @param size       每页数目
     * @return /
     */
    public List<String> findKeysForPage(String patternKey, int page, int size) {
        ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection rc = Objects.requireNonNull(factory).getConnection();
        Cursor<byte[]> cursor = rc.scan(options);
        List<String> result = new ArrayList<>(size);
        int tmpIndex = 0;
        int fromIndex = page * size;
        int toIndex = page * size + size;
        while (cursor.hasNext()) {
            if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
                result.add(new String(cursor.next()));
                tmpIndex++;
                continue;
            }
            // 获取到满足条件的数据后,就可以退出了
            if (tmpIndex >= toIndex) {
                break;
            }
            tmpIndex++;
            cursor.next();
        }
        try {
            RedisConnectionUtils.releaseConnection(rc, factory);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return result;
    }

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

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    public void del(String... keys) {
        if (keys != null && keys.length > 0) {
            if (keys.length == 1) {
                boolean result = redisTemplate.delete(keys[0]);
                log.debug("--------------------------------------------");
                log.debug(new StringBuilder("删除缓存:").append(keys[0]).append(",结果:").append(result).toString());
                log.debug("--------------------------------------------");
            } else {
                Set<Object> keySet = new HashSet<>();
                for (String key : keys) {
                    keySet.addAll(redisTemplate.keys(key));
                }
                long count = redisTemplate.delete(keySet);
                log.debug("--------------------------------------------");
                log.debug("成功删除缓存:" + keySet.toString());
                log.debug("缓存删除数量:" + count + "个");
                log.debug("--------------------------------------------");
            }
        }
    }

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

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

    /**
     * 批量获取
     *
     * @param keys
     * @return
     */
    public List<Object> multiGet(List<String> keys) {
        List list = redisTemplate.opsForValue().multiGet(Sets.newHashSet(keys));
        List resultList = Lists.newArrayList();
        Optional.ofNullable(list).ifPresent(e-> list.forEach(ele-> Optional.ofNullable(ele).ifPresent(resultList::add)));
        return resultList;
    }

    /**
     * 普通缓存放入
     *
     * @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) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key      键
     * @param value    值
     * @param time     时间
     * @param timeUnit 类型
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, timeUnit);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return false;
        }
    }

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

    /**
     * HashGet
     *
     * @param key  键 不能为null
     * @param item 项 不能为null
     * @return 值
     */
    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 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            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)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }

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

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

    /**
     * 根据key获取Set中的所有值
     *
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     *
     * @param key 键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            return 0;
        }
    }

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

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

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

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

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

    /**
     * 将list放入缓存
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒)
     * @return
     */
    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) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            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) {
            log.error(e.getMessage(), e);
            return false;
        }
    }

    /**
     * 移除N个值为value
     *
     * @param key   键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            return redisTemplate.opsForList().remove(key, count, value);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return 0;
        }
    }

    /**
     * @param prefix 前缀
     * @param ids    id
     */
    public void delByKeys(String prefix, Set<Long> ids) {
        Set<Object> keys = new HashSet<>();
        for (Long id : ids) {
            keys.addAll(redisTemplate.keys(new StringBuffer(prefix).append(id).toString()));
        }
        long count = redisTemplate.delete(keys);
        // 此处提示可自行删除
        log.debug("--------------------------------------------");
        log.debug("成功删除缓存:" + keys.toString());
        log.debug("缓存删除数量:" + count + "个");
        log.debug("--------------------------------------------");
    }
}

16、分布式

16.1、分布式简介

  • 分布式系统

    • 建立在网络之上的软件系统
    • 是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统
    • 利用更多的机器,处理更多的数据
  • 分布式服务架构(RPC:Remote Procedure Cal)

    • 指远程过程调用,是一种进程间通信方式

    • 允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节

    • RPC就是要像调用本地的函数一样去调远程函数

    • RPC两个核心模块:通讯,序列化

    • 基本原理图如下

在这里插入图片描述

16.2、Dubbo

16.2.1、Dubbo简介

官网:https://dubbo.apache.org/zh/

Apache Dubbo是一款分布式服务框架,高性能和透明化的RPC远程服务调用方案,SOA服务治理方案

架构流程图如下:

在这里插入图片描述

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

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

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

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

服务运行容器(Container):服务运行容器

16.2.2、Dubbo环境搭建

16.2.2.1、zookeeper

官网:https://zookeeper.apache.org/

  • 到官网下载压缩包后解压,更名为zookeeper

  • /bin/zkServer.cmd的末尾添加pause,避免闪退无法排查出错原因

    setlocal
    call "%~dp0zkEnv.cmd"
    
    set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain
    echo on
    call %JAVA% "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%" -cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*
    pause
    endlocal
    
  • conf/zoo_sample.cfg复制一份改名为zoo.cfg,可修改以下配置

    • dataDir:临时数据存储的目录(可写相对路径)
    • dataLogDir:存储log的目录
    • clientPort:zookeeper的端口号
  • 以管理员身份运行/bin/zkServer.cmd,即可启动zookeeper

16.2.2.2、dubbo-admin

一个可视化的监控程序,让用户更好的管理监控众多的dubbo服务。可以不使用

  • 下载地址:https://github.com/apache/dubbo-admin/tree/master(README_ZH.md里有教程)

  • 下载解压后可修改注册中心地址(和zookeeper配置的一致):dubbo-admin-master\dubbo-admin-server\src\main\resources\application.properties

  • 构建:进入dubbo-admin-master目录执行cmd打包:mvn clean package -Dmaven.test.skip=true

  • 启动:进入dubbo-admin-distribution\target目录下执行cmd:java -jar dubbo-admin-0.3.0.jar

  • 访问地址http://localhost:8080,默认用户名root、密码root(zookeeper需启动)

16.2.3、SpringBoot + Dubbo + zookeeper

  • 创建module:provider-server,表示服务提供者

    • pom.xml导入依赖
    <!-- Dubbo Spring Boot Starter -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
        <version>2.7.3</version>
    </dependency>
    <!-- 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>
    
    • application.properties配置
    #服务端口
    server.port=8081
    #当前应用名字
    dubbo.application.name=provider-server
    #注册中心地址
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    #扫描指定包下服务
    dubbo.scan.base-packages=com.leijiao.service
    #如果出现端口被占用,可以修改一下
    dubbo.protocol.port=12345
    
    • server接口
    public interface TicketService {
        public String getTicket();
    }
    
    • server接口实现类
    import com.leijiao.service.TicketService;
    import org.springframework.stereotype.Component;
    //使用了dubbo后应该导入apache的
    //import org.springframework.stereotype.Service;
    import org.apache.dubbo.config.annotation.Service;
    
    @Service //发布服务
    @Component //放在容器:应用启动,dubbo就会扫描指定的包下带有@Component注解的服务,将它发布在指定的注册中心中
    public class TicketServiceImpl implements TicketService {
    
        @Override
        public String getTicket() {
            return "xxx";
        }
    }
    
    • 启动zookeeper和dubbo-admin,启动provider-server,在http://localhost:8080看到服务列表中有provider-server服务
  • 创建module:consumer-server,表示服务消费者

    • pom.xml导入依赖,和provider-server的一样
    • application.properties配置
    #服务端口
    server.port=8082
    #当前应用名字
    dubbo.application.name=consumer-server
    #注册中心地址
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    
    • 复制服务提供者项目中service的全路径,,路径名要和消费者项目的路径名一样,。只需要接口类,不需要实现类
    public interface TicketService {
        public String getTicket();
    }
    
    • 消费者服务类
    import com.leijiao.service.TicketService;
    import org.apache.dubbo.config.annotation.Reference;
    //使用spring下的Service
    import org.springframework.stereotype.Service;
    
    @Service //注入到容器
    public class UserService {
        //需要去拿注册中心的服务
        @Reference //远程引用指定的服务,按照全类名匹配
        TicketService ticketService;
    
        public void bugTicket(){
            String ticket = ticketService.getTicket();
            System.out.println("在注册中心买到"+ticket);
        }
    
    }
    
  • 测试一下(启动zookeeper、启动dubbo-admin(可以不用)、启动服务提供者、启动服务消费者测试)

import com.leijiao.service.TicketService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;

@Service //注入到容器
public class UserService {
    //需要去拿注册中心的服务
    @Reference //远程引用指定的服务,按照全类名匹配
    TicketService ticketService;

    public void bugTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心买到"+ticket);
    }
}

//控制台打印:在注册中心买到xxx

  - application.properties配置

  ```properties
  #服务端口
  server.port=8081
  #当前应用名字
  dubbo.application.name=provider-server
  #注册中心地址
  dubbo.registry.address=zookeeper://127.0.0.1:2181
  #扫描指定包下服务
  dubbo.scan.base-packages=com.leijiao.service
  #如果出现端口被占用,可以修改一下
  dubbo.protocol.port=12345
  • server接口
public interface TicketService {
    public String getTicket();
}
  • server接口实现类
import com.leijiao.service.TicketService;
import org.springframework.stereotype.Component;
//使用了dubbo后应该导入apache的
//import org.springframework.stereotype.Service;
import org.apache.dubbo.config.annotation.Service;

@Service //发布服务
@Component //放在容器:应用启动,dubbo就会扫描指定的包下带有@Component注解的服务,将它发布在指定的注册中心中
public class TicketServiceImpl implements TicketService {

    @Override
    public String getTicket() {
        return "xxx";
    }
}
  • 启动zookeeper和dubbo-admin,启动provider-server,在http://localhost:8080看到服务列表中有provider-server服务

  • 创建module:consumer-server,表示服务消费者

    • pom.xml导入依赖,和provider-server的一样
    • application.properties配置
    #服务端口
    server.port=8082
    #当前应用名字
    dubbo.application.name=consumer-server
    #注册中心地址
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    
    • 复制服务提供者项目中service的全路径,,路径名要和消费者项目的路径名一样,。只需要接口类,不需要实现类
    public interface TicketService {
        public String getTicket();
    }
    
    • 消费者服务类
    import com.leijiao.service.TicketService;
    import org.apache.dubbo.config.annotation.Reference;
    //使用spring下的Service
    import org.springframework.stereotype.Service;
    
    @Service //注入到容器
    public class UserService {
        //需要去拿注册中心的服务
        @Reference //远程引用指定的服务,按照全类名匹配
        TicketService ticketService;
    
        public void bugTicket(){
            String ticket = ticketService.getTicket();
            System.out.println("在注册中心买到"+ticket);
        }
    
    }
    
  • 测试一下(启动zookeeper、启动dubbo-admin(可以不用)、启动服务提供者、启动服务消费者测试)

import com.leijiao.service.TicketService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;

@Service //注入到容器
public class UserService {
    //需要去拿注册中心的服务
    @Reference //远程引用指定的服务,按照全类名匹配
    TicketService ticketService;

    public void bugTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心买到"+ticket);
    }
}

//控制台打印:在注册中心买到xxx

版权声明:本文为aoliaoliaoo原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。