1 Hello World

  • 学习网站:https://www.bilibili.com/video/BV1PE411i7CV?p=1

  • 笔记是B站狂神说相关视频的学习笔记

    1.1 Spring是怎么简化开发的?

  • 1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;

  • 2、通过IOC,依赖注入(DI)和面向接口实现松耦合;

  • 3、基于切面(AOP)和惯例进行声明式编程;

  • 4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate;

1 2 什么是SpringBoot?

  • 简化配置:Springboot是对Spring的进一步封装,基于注解开发,舍弃了笨重的xml配置,使用yml或者properties配置
  • 产品级独立运行:每一个工程都可以打包成一个jar包,内置了Tomcat和Servlet容器,可以独立运行
  • 强大的场景启动器:每一个特定场景下的需求都封装成了一个starter,只要导入了这个starter就有了这个场景所有的一切
  • 学习路线

在这里插入图片描述

1.3 Hello World

1 开发环境

  • JDK-1.8
  • Maven-3.6.1
  • Springboot-2.2.6

2 IDEA创建

  • 选择spring initalizr,初学勾选Web即可

3 编写一个HelloWordController

  • handler方法:
    • 注意:必须和主启动类在同一级目录下创建包和方法才能生效
      • 如果默认扫描包不一致,需要在主启动类上使用@ComponentScan指定扫描包
@RestController
public class HelloWordController {

    /*
        springBoot的contextPath默认是""空字符串,不用想SSM配置Tomcat时候配置工程名,简化了配置
        访问地址:http://localhost:8080/hello
     */
    @RequestMapping("/hello")
    public String hello() {
        return "hello world";
    }
}
// 将当前类标记成一个Springboot应用
@SpringBootApplication
public class SpringBoot_01_Application {

    public static void main(String[] args) {

        SpringApplication.run(SpringBoot_01_Application.class, args);
    }

}

4 打包jar发布

  • 测试用例不让打包,删除测试用例打包
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <!--
              在工作中,很多情况下我们打包是不想执行测试用例的
              可能是测试用例不完事,或是测试用例会影响数据库数据
              跳过测试用例执
              -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <!--跳过项目运行测试用例-->
                <skipTests>true</skipTests>
            </configuration>
        </plugin>
    </plugins>
</build>
  • <stron>
    • target中就会出现xxx.jar包,显示打包成功
    </stron>

2 pom.xml

2.1 父工程

  • 父工程师官网默认设置好了的资源和依赖,已有的,我们配置时就不用写版本号
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.6.RELEASE</version>
    <relativePath/>
</parent>
  • 点spring-boot-starter-parent进去看,就会看到官方配置好的依赖和资源版本号

2.2 starter启动器

  • SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来 , 我们要用什么功能就导入什么样的场景启动器即可 ;我们未来也可以自己自定义 starter;
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3 主启动类

@SpringBootApplication
public class DemoApplication {

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

}
  • SpringBootApplication注解:
// 主启动类的注解,一个tab表示点进去一次
@SpringBootApplication
    @SpringBootConfiguration// 表明是一个SpringBoot配置文件
        @Configuration// 再次说明这是一个Spring配置    

    @EnableAutoConfiguration // 自动配置
        @AutoConfigurationPackage
            @Import(AutoConfigurationPackages.Registrar.class) 
        @Import(AutoConfigurationImportSelector.class)

    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
            @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

3.1 @SpringBootApplication

@SpringBootConfiguration

  • 说明里面的 @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!
@SpringBootApplication
    @SpringBootConfiguration// 表明是一个SpringBoot配置文件
        @Configuration// 再次说明这是一个Spring配置

@EnableAutoConfiguration

  • @AutoConfigurationPackage
    • @AutoConfigurationPackages.Registrar.class:
      • Registrar.class作用就是将主启动所在的包及以下的所有子包都扫描进spring容器中
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
  • @Import(AutoConfigurationImportSelector.class) ,找到getCandidateConfigurations方法
    • getCandidateConfigurations方法:获得候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }
  • 找到SpringFactoriesLoader.loadFactoryNames,点进去,找到loadSpringFactories方法
    • SpringFactoriesLoader类中的预定义的自动装配路径 FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        String factoryTypeName = factoryType.getName();
        return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
    }

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    //获得classLoader
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    ...
}
  • 多次出现的spring.factories,就是预定好的加载配置文件
    在这里插入图片描述

@ComponentScan

  • 自动扫描并加载符合条件的组件bean,并将这个组件bean注入到IOC容器中
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

3.2 自动装配

  • 自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的org.springframework.boot.autoconfigure.包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。

  • 用户如何书写yml配置,需要去查看META-INF/spring.factories下的某自动配置,如HttpEncodingAutoConfiguration

    • EnableConfigrutionProperties(xxx.class):表明这是一个自动配置类,加载某些配置
    • XXXProperties.class:封装配置文件中的属性,yam中需要填入= 它指定的前缀+方法

在这里插入图片描述

在这里插入图片描述

3.3 工作原理总结

  1. 读取spring.properties文件
    1. SpringBoot在启动的时候从spring-boot-autoConfigure.jar包下的的META-INF/spring.factories中获取EnableAutoConfiguration属性的值加载自动配置类
    2. 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
  2. 加载XXXProperties
    1. 根据自动配置类中指定的xxxxProperties类设置自动配置的属性值,开发者可以根据该类在yml配置文件中修改自动配置
  3. 根据@ConditionalXXX注解决定加载哪些组件
    1. Springboot通过该注解指定组件加入IOC容器时锁需要具备的特定条件。这个组件会在满足条件时候加入到IOC容器内

在这里插入图片描述

3.4 SpringApplication.run(xxx.class)

  1. 推断应用的类型是普通的项目还是Web项目
  2. 查找并加载所有可用初始化器 , 设置到initializers属性中
  3. 找出所有的应用程序***,设置到listeners属性中
  4. 推断并设置main方法的定义类,找到运行的主类
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
    // ......
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.setInitializers(this.getSpringFactoriesInstances();
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

4 application.yml

4.1 传统的xml配置以标记语言为中心

<server>
    <port>8081<port>
</server>

4.2 yaml配置以数据为中心

  • yml: y are not markup langure . y 不是一个标记语言,而是以数据为中心
server:
  prot: 8080
  servlet:
    # 原先的Tomcat工程路径在这里修改
    context-path: /laosong      

4.3 yml语法

  1. 空格不能省略,键值对中间必须要有一个空格
  2. 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
  3. 属性和值的大小写都是十分敏感的。

4.4 yml基本类型写入

  1. 字面值:普通字符换、数值、布尔类型,直接写成k:v,字符串默认不用加‘’或“”

    name: zhangsan
    • “ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;

      比如 :name: "kuang \n shen" 输出 :kuang 换行 shen

    • '' 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出

      比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen

    • 注意:设置数据库密码如果是0开头,SpringBoot会默认按照八进制来解析,yml配置时加上引号,如'01111'

  2. 对象、Map:属性值必须和Bean中的对应一致

    Student:
            name: zhangsan
            age: nan
  3. 数组:使用 - 表示一个元素

    Countries: 
        - Chine
        - USA

4.5 yml配置bean属性,导入依赖

  1. 创建一个application.yml

    person:
      name: qinjiang
      age: 3
      happy: false
      birth: 2000/01/01
      maps: {k1: v1,k2: v2}
      lists:
        - code
        - girl
        - music
      dog:
        name: 旺财
        age: 1
  2. @ConfigurationProperties(prefix = "person")

    1. 默认springboot项目会报错,提示找不到注解配置,需要导包,然后Idea重启

    2. @ConfigurationProperties(prefix = "person")默认是从全局配置获取,文件名只能是application.yml

      <!-- 1 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-configuration-processor</artifactId>
          <optional>true</optional>
      </dependency>
      @Component
      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      // 2 绑定配置文件:指定yml配置中的配置名称装配
      @ConfigurationProperties(prefix = "person")
      public class Person {
          private String name;
          private Integer age;
          private Boolean happy;
          private Date birth;
          private Map<String,Object> maps;
          private List<Object> lists;
          private Dog dog;
      }
      
      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class Dog {
          private String name;
          private Integer age;
      }
  3. 测试

    // 3 编写测试类
    @SpringBootTest
    class DemoApplicationTests {
        @Autowired
        Person person;
        @Test
        void personTest() {
            System.out.println(person);
        }
    }
  4. 控制台显示

在这里插入图片描述

4.6 加载指定配置文件

  1. @ConfigurationProperties(prefix = "person"):默认是从全局配置获取,

    • 文件名只能是application.yml
    • 优点是只需要配置prefix=key会自定匹配相对应的属性
  2. @PropertySource :加载指定的配置文件;

    • 定义一个person.yml,yml中可以使用**{rondom.int}**来随机生成数
    person:
      name: qinjiang ${random.uuid}
      id: ${random.int}
    
    • 指定yml,缺点是需要手动注入属性${"XXX"}
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Component
    // 指定配置yml文件
    @PropertySource(value = "classpath:person.yml")
    public class Person1 {
        @Value("${name}")
        private String name;
        @Value("${id}")
        private String id;
    }

4.7 小结

  1. yml的好处?
     - **实现松散绑定?**
       - yml中的first-name = bean中的firstName 
     - @ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
     - **JSR303数据校验** 
     - 复杂类型封装

4.8 JSR303数据校验

  • 就是一种数据校验格式,在类上绑定@Validated,在属性上使用指定的参数如 @Email(message="邮箱格式错误")

  • 常见的数据校验参数,包是javax.validation.constraints

    // JSR303常用校验    
    @NotNull(message="名字不能为空")
    private String userName;
    @Max(value=120,message="年龄最大不能查过120")
    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  
    
    // 长度检查
    @Si***=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
    @Length(min=, max=) string is between min and max included.
    
    // 日期检查
    @Past       验证 Date 和 Calendar 对象是否在当前时间之前  
    @Future     验证 Date 和 Calendar 对象是否在当前时间之后  
    @Pattern    验证 String 对象是否符合正则表达式的规则
    

4.9 application.yml可以安装的位置

  • file:./config和file:./config
  • file:config/appplication.yml
  • file:application.yml
  • resources:/config/application.yml
  • resources:application.yml
  • 执行的优先级依次递减:

4.10 yml多配置

  • 来分割多个yml配置,并且用profiles来命名
    • 一个yml文件可以使用---来区分配置,比properties强大之一
  • 激活dev的配置
server:
  port: 8080
# 选择使用哪些配置
spring:
  profiles:
    active: dev # 激活dev配置
---
server:
  port: 8081
spring:
  profiles: test
---
server:
  port: 8082
spring:
  profiles: dev

5 Web开发

要解决的问题:

  • 导入静态资源

  • 指定首页

  • jsp。模版引擎Thymeleaf

  • 装配扩展SpringMVC

  • 增删改查

  • 拦截器和国际化

1 静态资源源码

  • idea按两下shift,搜索webautoConfiguration - WebMvcAutoConfigurationAdapter - addResourceHandlers
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    // 第一种方式 webjars
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    // 第二种方式
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

2 三个静态资源的位置

  • 第一个if:没有

  • 第二个if:webjars不推荐使用,官网导包,url:META-INF/resources/webjars/

  • 第三个if:优先级:resources > static(默认) > public

    • classPath :/**
    • classpath:/META-INF/resources/
    • classpath:/resources/
    • classpath:/static/(默认创建)
    • classpath:/public/
    private String staticPathPattern = "/**";
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/","classpath:/resources/","classpath:/static/","classpath:/public/" };
    //以上4个人位置加装静态资源

3 首页

WebMvcAutoConfiguration 下的 ,观察源码知:在静态资源目录下创建index.html即可

private Resource getIndexHtml(String location) {
   return this.resourceLoader.getResource(location + "index.html");
}

4 Thymeleaf

  • 注意:template/**下的任何页面都需要Controller来跳转才能访问,不能直接访问
  1. 导入依赖

    <!--thymeleaf模版引擎在,都是基于3.x版本开发-->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-java8time</artifactId>
    </dependency>
  2. 查看源码,在template下使用

    @ConfigurationProperties(prefix = "spring.thymeleaf")
    public class ThymeleafProperties {
    
        private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
        // 前缀
        public static final String DEFAULT_PREFIX = "classpath:/templates/";
        // 后缀
        public static final String DEFAULT_SUFFIX = ".html";
        ...
    }
  3. 第一次使用,传msg,接受:th:text="${msg}",(thymeleaf独有的语法)

    @Controller
    public class ThymeleafController {
    
        @RequestMapping("/thymeleaf")
        public String thy(Model model){
            model.addAttribute("msg","<p>hello Thymeleaf</p>");
            model.addAttribute("users", Arrays.asList("张三","李四","王五"));
            // 后缀默认是 .html
            return "thymeleafTest";
        }
    }
    <!DOCTYPE html>
    <!--thymeleaf约束-->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>thymeleaf</title>
    </head>
    <body>
    <!--所有的html元素都可以被Th接管:th:xx元素名-->
    <!--test=默认是转义文本-->
    <h1 th:text="${msg}"></h1>
    <!--utest=默认是不转义文本-->
    <h1 th:utext="${msg}"></h1>
    <h1>
        <!--遍历往前写item-->
        遍历一 推荐这么使用:
        <h2 th:each="user:${users}" th:text="com.wenyibi.futuremail.model.User@55e55bdb"></h2><br>
        遍历二:
        <h2 th:each="user:${users}" >[[com.wenyibi.futuremail.model.User@55e55bdb]]</h2>
    </h1>
    </body>
    </html>
  4. Thymeleaf常用语法

在这里插入图片描述

<h1>
    <h2 th:each="user:${users}" th:text="com.wenyibi.futuremail.model.User@55e55bdb"></h2>
</h1>

5 SpringMVC扩展

implements WebMvcConfigurer,重写一些方法

/**
 *  扩展SpringMVC:实现WebMvcConfigurer接口,重写某些方法让我们实现一些新的配置
 *      注意:不能再使用@EnableWebMvc,这个方***让扩展方法失效
 */
@Configuration
public class MyMVCConfig implements WebMvcConfigurer {
    /**
     * 视图跳转 访问/ssl,跳转到config.html
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController(
                "/ssl").setViewName("config");
    }
}

6 员工管理系统

  • 说明:这里未连接数据库数据,使用的是虚假的java

1 首页

  • 建议使用扩展MVC配置的首页访问方式
@Configuration
public class MyConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 访问首页,建议使用扩展MVC
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index").setViewName("index");
    }
}
  • template下的文件必须配置Controller跳转才能访问

  • thymeleaf语法:

    • 导入依赖
    • 导入约束: xmlns:th="http://www.thymeleaf.org
    • 前端url表达式语法: @{...} ,/表示默认默认从static下寻找
    <!--thymeleaf约束 -->
    <html lang="en" xmlns:th="http://www.thymeleaf.org">   
    <!-- / 从static开始 -->
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
    <link th:href="@{/css/signin.css}" rel="stylesheet">

2 国际化

  • Idea中setting中“file-encoding”修改项目编码、配置文件编码均为“utf-8”,否则配置文件中文会显示乱码
  • resources下配置i18n文件,index.html前端页面使用th:text="#{login.btn}"等接收配置文件里的参数

在这里插入图片描述

<!DOCTYPE html>
<html lang="zh" 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>
    <!-- / 从static开始 -->
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
    <link th:href="@{/css/signin.css}" rel="stylesheet">

</head>

<body class="text-center">
<form class="form-signin" action="/user/login">
    <img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
    <!--配置国际化 th:text="#{login.tip}-->
    <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}"></h1>
    <!--配置登录失败信息-->
    <P style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></P>
    <input name="username" value="admin" type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">

    <input name="password" value="123456" type="password" class="form-control" th:placeholder="#{login.password}" required="">

    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" value="remember-me" th:text="#{login.remember}">
        </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}"></button>
    <p class="mt-5 mb-3 text-muted">© 2020-2021</p>
    <!--国际化消息,配置MyLocalResolver,传递解析url
        thymeleaf用()表示?xx=xx  -->
    <a class="btn btn-sm" th:href="@{/index(language='zh_CN')}">中文</a>
    <a class="btn btn-sm" th:href="@{/index(language='en_US')}">English</a>
</form>

</body>
</html>
  • 自定义一个MyLocalResolver继承LocalResolver
    • 前端传递参数:th:href="@{/index(language='zh_CN')}",thymeleaf中的?key=value用(key=value)简化
public class MyLocalResolver implements LocaleResolver {
    //解析国际化请求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String language = request.getParameter("language");
        System.out.println("语言:" + language);
        Locale locale = Locale.getDefault();
        if (!Strings.isEmpty(language)) {
            String[] split = language.split("_");
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

    }
}
  • 将组件注册进IOC容器
    • 这里通过扩展的MVC来注册进组件,返回类型是LocaleResolver
@Configuration
public class MyConfig implements WebMvcConfigurer {

    // 国际化解析器注册进组件
    @Bean
    public LocaleResolver localeResolver() {
        return new MyLocalResolver();
    }


    // 首页视图解析器
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 访问首页,建议使用扩展MVC
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index").setViewName("index");
    }
}

3 拦截器

  • 继承处理拦截器implements HandlerInterceptor
    • 这里假设获取session中的username,如果存在就表示登录成功;不存在就表示登录失败,request中存失败msg
/**
 * 自定义拦截器
 */
public class LoginHandlerInterceptor implements HandlerInterceptor {
    /*
        登录拦截器
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取用户名称,登录成功之后,应该有用户的session
        Object username = request.getSession().getAttribute("username");
        // 登录失败返会登录页
        if (username == null) {
            request.setAttribute("msg", "没有权限,请重新登录");
            request.getRequestDispatcher("/index").forward(request, response);
            return false;
        } else {
            return true;
        }
    }
}
  • Myconfig中配置拦截的请求
    • 放心首页和表单提交地址,以及静态资源
 //登录拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 配置自定义拦截器
    registry.addInterceptor(new LoginHandlerInterceptor())
        .addPathPatterns("/**")
        .excludePathPatterns("/index", "/", "/user/login", "/css/**", "/img/**", "/js/**");
}

4 CRUD

1 提取公共页面

  • th:replace:是替换全部元素,原有模块已经不存在;th:insert:是加入元素,原有模块还是存在
<!--公共页面提供fragment-->
<nav th:fragment="navbar">
<!--需要公共页面的标签使用,commons包下的comms.html是提出的公共页面-->
<div th:replace="~{commons/comms::navbar}"></div>
<div th:insert="~{commons/comms::navbar}"></div>   
  • 字体高亮,普通页面传递参数(active='main.html')给公共页面
    • 公共页面使用"${active=='main.html'}?'nav-link active':'nav-link'"查看是否显示高亮
<div th:replace="~{commons/comms::sidebar(active='main.html')}"></div>
<a th:class="${active=='main.html'}?'nav-link active':'nav-link'" th:href="@{/index}">

2 展示员工列表

  • 不连接数据库,提供假数据,部门和员工
@Repository
public class DepartmentDao {
    // 模拟数据库中的数据,无需创建数据库
    private static Map<Integer, Department> departments = null;

    static {
        departments = new HashMap<>();
        departments.put(101, new Department(101, "教学部"));
        departments.put(102, new Department(102, "市场部"));
        departments.put(103, new Department(103, "教研部"));
        departments.put(104, new Department(104, "运营部"));
        departments.put(105, new Department(105, "后勤部"));
    }

    //获得所有部门的信息
    public Collection<Department> getDepartmens() {
        return departments.values();
    }

    // 通过id获取部门
    public Department getDepartmentById(Integer id) {
        return departments.get(id);
    }
}
@Repository
public class EmpDao {

    @Autowired
    private DepartmentDao departmentDao;
    // 模拟数据库中的数据,无需创建数据库
    private static Map<Integer, Employee> employees = null;

    static {
        employees = new HashMap<>();
        employees.put(1001, new Employee(1001, "AA", "123456@qq.com", 0, new Department(101, "教学部")));
        employees.put(1002, new Employee(1002, "BB", "123456@qq.com", 1, new Department(102, "市场部")));
        employees.put(1003, new Employee(1003, "CC", "123456@qq.com", 0, new Department(103, "教研部")));
        employees.put(1004, new Employee(1004, "DD", "123456@qq.com", 1, new Department(104, "运营部")));
        employees.put(1005, new Employee(1005, "EE", "123456@qq.com", 1, new Department(105, "后勤部")));
    }

    // 主键自增
    private static Integer initId = 1006;

    // 增加一个员工
    public void saveEmp(Employee employee) {
        if (employee.getId() == null) {
            employee.setId(initId++);
        }
        employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
        employees.put(employee.getId(), employee);
    }

    // 删除员工
    public void deleteEmppyeeById(Integer id) {
        employees.remove(id);
    }

    // 查询全部员工信息
    public Collection<Employee> getAllEmp() {
        return employees.values();
    }

    // 通过id查询员工
    public Employee getEmployeeById(Integer id) {
        return employees.get(id);
    }
}
  • Controller
@Controller
public class EmployeeController {
    @Autowired
    private EmpDao employeeDao;

    @RequestMapping("/emps")
    public String showEmployeeList(Model model) {
        Collection<Employee> employees = employeeDao.getAllEmp();
        model.addAttribute("emps", employees);
        return "emp/list";
    }
}
  • list.html
    • 遍历: th:each="emp:${emps}"
    • 日期格式化:th:text="'女':'男'"th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"
<table class="table table-striped table-sm">
    <thead>
        <tr>
            <th>id</th>
            <th>lastName</th>
            <th>email</th>
            <th>gender</th>
            <th>department</th>
            <th>birth</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="emp:${emps}">
            <td th:text="${emp.getId()}"></td>
            <td th:text="${emp.getLastName()}"></td>
            <td th:text="${emp.getEmail()}"></td>
            <!--性别需要前端判断-->
            <td th:text="'女':'男'"></td>
            <td th:text="${emp.getDepartment().getDepartName()}"></td>
            <!--日期需要改变格式-->
            <td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
            <td>
                <button class="btn btn-sm btn-primary">编辑</button>
                <button class="btn btn-sm btn-danger">删除</button>
            </td>
        </tr>
    </tbody>
</table>

3 添加员工

  • list.html,添加跳转
<!--添加员工-->
<h2>
    <!--默认get请求,第一次回显员工中的部门信息使用默认的get请求-->
    <a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a>
</h2>
  • add.html

    • 部门信息,需要先查找出所有部门信息,再回显表单,并且提交是department.id

    • 前端日期格式thymeleaf默认是yyyy/mm/dd,修改application.yml

      spring:
        mvc:
        # 修改日期格式,默认是yyyy/MM/dd
          date-format: yyyy-MM-dd
<form th:action="@{/emp}" method="post">
    <div class="form-group">
        <label>LastName</label>
        <input name="lastName" value="tom" type="text" class="form-control" id="exampleInputEmail1" placeholder="lastName">
    </div>
    <div class="form-group">
        <label>Email</label>
        <input name="email" value="123456@qq.com" type="email" class="form-control" id="exampleInputPassword1" placeholder="email">
    </div>
    <div class="form-group">
        <label>Gender</label>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" value="1" name="gender"/>
            <label class="form-check-label">男</label>
        </div>
        <div class="form-check form-check-inline">
            <input class="form-check-input" type="radio" value="0" name="gender"/>
            <label class="form-check-label">女</label>
        </div>
    </div>
    <div class="form-group">
        <label>Department</label>
        <!--提交的Department的是部门id-->
        <select name="department.id" class="form-control">
            <option th:each="dept:${departments}"
                    th:text="${dept.getDepartName()}"
                    th:value="${dept.getId()}"></option>
        </select>
    </div>
    <div class="form-group">
        <label>Birth</label>
        <input name="birth" value="2020-5-27" type="text"
               class="form-control"
               placeholder="yyyy-MM-dd">
    </div>
    <button type="submit" class="btn btn-default">添加</button>
</form>
  • Controller
@Controller
public class EmployeeController {
    @Autowired
    private EmpDao employeeDao;
    @Autowired
    private DepartmentDao departmentDao;

    @RequestMapping("/emps")
    public String showEmployeeList(Model model) {
        Collection<Employee> employees = employeeDao.getAllEmp();
        model.addAttribute("emps", employees);
        return "emp/list";
    }

    // 跳转到add.html
    @GetMapping("/emp")
    public String toAddEmpPage(Model model) {
        // 查出所有部门的信息
        Collection<Department> departments = departmentDao.getDepartments();
        model.addAttribute("departments", departments);
        return "emp/add";
    }

    // 提交添加员工
    @PostMapping("/emp")
    public String toAddEmp(Employee employee) {
        // 保存员工信息
        employeeDao.saveEmp(employee);
        return "redirect:/emps";
    }
}

4 修改员工

  • list.html点击“修改”
  <a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.getId()}">编辑</a>
  • update.html
    • 隐藏域存empId: < input type="hidden" th:value="${emp.getId()}">
    • option: th:selected="${dept.getId()==emp.getDepartment().getId()}"
    • 日期格式转换:th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/updateEmp}" method="post">
        <!--隐藏查出来的empId-->
        <input type="hidden" th:value="${emp.getId()}">
        <div class="form-group">
            <label>LastName</label>
            <input name="lastName" th:value="${emp.getLastName()}" type="text" class="form-control"
                   id="exampleInputEmail1" placeholder="lastName">
        </div>
        <div class="form-group">
            <label>Email</label>
            <input name="email" th:value="${emp.getEmail()}" type="email"
                   class="form-control" id="exampleInputPassword1" placeholder="email">
        </div>
        <div class="form-group">
            <label>Gender</label>
            <div class="form-check form-check-inline">
                <input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio"
                       value="1" name="gender"/>
                <label class="form-check-label">男</label>
            </div>
            <div class="form-check form-check-inline">
                <input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio"
                       value="0" name="gender"/>
                <label class="form-check-label">女</label>
            </div>
        </div>
        <div class="form-group">
            <label>Department</label>
            <!--提交的value是id-->
            <select name="department.id" class="form-control">
                <!--回显:部门id等于员工部门的id-->
                <option th:each="dept:${departments}"
                        th:selected="${dept.getId()==emp.getDepartment().getId()}"
                        th:text="${dept.getDepartName()}"
                        th:value="${dept.getId()}"></option>
            </select>
        </div>
        <div class="form-group">
            <label>Birth</label>
            <!--日期回显,需要更改默认格式-->
            <input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}" name="birth" type="text"
                   class="form-control" placeholder="yyyy-MM-dd">
        </div>
        <button type="submit" class="btn btn-default">修改</button>
    </form>
</main>
  • Controller
 // 修改员工,回显员工表
@GetMapping("/emp/{id}")
public String toUpdateEmpPage(@PathVariable("id") Integer id, Model model) {
    Employee employee = employeeDao.getEmployeeById(id);
    model.addAttribute("emp", employee);
    Collection<Department> departments = departmentDao.getDepartments();
    model.addAttribute("departments", departments);
    return "emp/update";
}

// 修改员工
@PostMapping("/updateEmp")
public String updateEmp(Employee employee) {
    employeeDao.saveEmp(employee);
    return "redirect:/emps";
}

5 删除员工

  • list.html
<a class="btn btn-sm btn-danger" th:href="@{/deleteEmp/}+${emp.getId()}">删除</a>
  • Controller
// 删除员工
@GetMapping("/deleteEmp/{id}")
public String deleteEmp(@PathVariable("id") Integer id) {
    employeeDao.deleteEmpById(id);
    return "redirect:/emps";
}

6 404错误

  • springBoot封装了,在template中创建error包,下面放404.html,会自动出错时跳转到该页面

7 注销功能

  • 注销标签
<a class="nav-link" th:href="@{/user/logout}">注销</a>
  • Controller:注销掉session
@RequestMapping("/user/logout")
public String logout(HttpSession session) {
    // 注销session
    session.invalidate();
    return "redirect:/index";
}

8 如何写一个网站

  1. 前端

    1. 模版:自己网站搜
    2. 框架:组件,需要自己手动拼接:BootStrap,Layui,semantic-ui
  2. 设计数据库(真正的难点)

  3. 前端让他能够自动运行,独立化工程

  4. 数据接口如何对接:json,对象

  5. 前后端联调

    1. 前端:自己能够通过“’框架”网站组合出一个网页
    2. 后端:必须要有自己熟悉的一个后台模版,99%公司会让你自己写:推荐X-admin网站模版

7 整合JDBC

  • 对于数据访问层,无论是SQL还是NOSQL,SpringBoot都是采用Spring Data方式统一处理

  • 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
  • application.yml
spring:
  datasource:
    username: root
    password: admin
    url: jdbc:mysql://localhost:3306/mybatis?userUnicode=true&chacacterEncoding=utf-8&serverTimezone=UTC
    # Springboot使用 com.mysql.cj.jdbc.Driver 针对Mysql8以上,5可能会有bug
    driver-class-name: com.mysql.cj.jdbc.Driver
  • 测试查看默认数据源:hikari,据说是目前最快的数据源连接
@SpringBootTest
class JdbcApplicationTests {
    /**
     * SpringBoot只要配置了数据源,就自动将数据源封装进IOC容器,用户无需配置数据源组件,直接取出
     */
    @Autowired
    DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        // 查看使用的数据源:目前最快的数据源:hikari.HikariDataSource
        //  System.out.println(dataSource.getClass());
        // 获取数据库连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        // 关闭数据源
        connection.close();
    }
}
  • controller
@Controller
public class JdbcController {
    @Autowired
    JdbcTemplate jdbcTemplate;

    // 查询出user表中的所有信息,没有实体类,可以使用万能的map来存
    @GetMapping("/userList")
    @ResponseBody
    public List<Map<String, Object>> userList() {
        String sql = "select * from user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }
}

8 整合Druid

  • Druid是阿里开源的数据源,自动整合了日志监控
  • 依赖
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.22</version>
</dependency>

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

在这里插入图片描述

@Configuration
public class DruidConfig {

    //将自定义的Druid配置进IOC容器
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource getDataSource() {
        return new DruidDataSource();
    }


    //配置 Druid 监控管理后台的Servlet;
    //内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
    @Bean
    public ServletRegistrationBean statViewServlet() {
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");

        // 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
        // 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
        Map<String, String> initParams = new HashMap<>();
        initParams.put("loginUsername", "admin"); //后台管理界面的登录账号
        initParams.put("loginPassword", "123456"); //后台管理界面的登录密码

        //后台允许谁可以访问
        //initParams.put("allow", "localhost"):表示只有本机可以访问
        //initParams.put("allow", ""):为空或者为null时,表示允许所有访问
        initParams.put("allow", "");
        //deny:Druid 后台拒绝谁访问
        //initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问

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


    //配置 Druid 监控 之  web 监控的 filter
    //WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
    @Bean
    public FilterRegistrationBean webStatFilter() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new WebStatFilter());
        //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
        Map<String, String> initParams = new HashMap<>();
        initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
        bean.setInitParameters(initParams);
        //"/*" 表示过滤所有请求
        bean.setUrlPatterns(Arrays.asList("/*"));
        return bean;
    }
}

9 整合Mybatis

  • 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
  • application.yml
    • 注意:非常重要,解决绑定异常:mapper-locations中mapper.xml最好和接口的包名路径一致,避免出现问题
# 整合数据源
spring:
  datasource:
    username: root
    password: admin
    url: jdbc:mysql://localhost:3306/mybatis?userUnicode=true&chacacterEncoding=utf-8&serverTimezone=UTC
    # Springboot使用 com.mysql.cj.jdbc.Driver 针对Mysql8以上,5可能会有bug
    driver-class-name: com.mysql.cj.jdbc.Driver
# 整合mybatis
mybatis:
  type-aliases-package: com.ssl.bean
  # 解决绑定异常:mapper.xml最好和接口的包名路径一致
  mapper-locations: classpath:com.ssl.mapper/*.xml
  • mapper接口和xml,bean
// 这个注解表示了这是一个mapper的注解类
@Mapper
@Repository
public interface UserMapper {

    List<User> getAllUser();

    User getUserById(@Param("id") int id);

    void addUser(User user);

    void deleteUser(@Param("id")int id);

    User updateUser(User user);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String pwd;
}
<?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.ssl.mapper.UserMapper">

    <select id="getAllUser" resultType="User">
      SELECT * FROM USER;
    </select>

    <select id="getUserById" parameterType="int" resultType="User">
      select * from user where id=#{id};
    </select>

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

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

    <delete id="deleteUser" parameterType="int">
        delete from user where id = #{id}
    </delete>
</mapper>
  • controller
@Controller
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/users")
    @ResponseBody
    public List<User> getUsers() {
        return userMapper.getAllUser();
    }
}

10 整合SpringSecurity

  • 功能权限、访问权限、菜单权限...,我们使用过滤器,拦截器需要写大量的原生代码,这样很不方便
  • 所以在网址设计之初,就应该考虑到权限验证的安全问题,其中Shiro、SpringSecurity使用很多
  • 前端资源:后期百度云补充

1 简介

  • SpringSecurity是Springboot底层安全模块默认的技术选型,它可以实现强大的Web安全机制,只需要少数的spring-boot--spring-security依赖,进行少量的配置,就可以实现
  • SpringBoot中的SpringSecurity依赖:
<!--security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 记住几个类 :

    • WebSecurityConfigurerAdapter:自定义Security策略
    • AuthenticationManagerBuilder:自定义认证策略
    • @EnableWebSecurity:开启WebSecurity模式
  • 两个单词:en是认证,or是权限

    • 认证方式:Authentication
    • 权限:Authorization

2 认证和授权

  • controller
@Controller
public class RouterController {

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

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

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

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


    @RequestMapping("/level3/{id}")
    public String level3(@PathVariable("id") int id) {
        return "views/level3/" + id;
    }
}
  • 写一个类继承WebSecurityConfigurerAdapter,使用@EnableWebSecurity开启web安全服务
    • 地址授权:使用HttpSecurity security
    • 账户认证和给予权限:使用AuthenticationManagerBuilder builder
      • SpringSecurity5 以后默认需要密码加密方式,推荐使用passwordEncoder(new BCryptPasswordEncoder())
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    // url授权:  HttpSecurity
    @Override
    protected void configure(HttpSecurity security) throws Exception {
        // 首页所有人可以访问,但是功能也只有对有权限的人可以访问
        security
                .authorizeRequests() // 认证请求
                .antMatchers("/").permitAll()
                .antMatchers("/level1/**").hasRole("vip1")
                .antMatchers("/level2/**").hasRole("vip2")
                .antMatchers("/level3/**").hasRole("vip3")
        ;
        // 自带登录页面,http://localhost:8080/login
            // 定制登录页,loginPage("/toLogin")
            // 指定表单提交url:loginProcessingUrl("/user/login")
        security.formLogin().loginPage("/toLogin")
                .usernameParameter("username").passwordParameter("password")
                .loginProcessingUrl("/user/login");
        // 开启注销功能,源码http://localhost:8080/logout,并且注销成功后跳转到/的Controller
        security.logout().logoutSuccessUrl("/");
            // 版本不同问题,可能会出现注销失败,关闭csrf
            // security.csrf().disable();
        // 开启记住我功能:本质就是记住一个cookies,默认保存2周
        security.rememberMe().rememberMeParameter("remember");
    }

    // 用户认证:AuthenticationManagerBuilder
        // SpringSecurity5 以后默认需要新郑密码密码加密方式
    @Override
    public void configure(AuthenticationManagerBuilder builder) throws Exception {
        // 内存中测试数据
        builder.inMemoryAuthentication()   // SpringSecurity5 以后默认需要新郑密码密码加密方式
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1")
                .and()
                .withUser("admin1").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3")
        ;
    }
}

3 登录注销和记住我

  • 定制登录
    • 开启登录功能:formLogin(),Springboot默认自带一个登录页/login定制看下面
    • 定制登录页:loginPage("/toLogin")
    • 修改表单提交的name:.usernameParameter("username").passwordParameter("password")
    • 表单提交的action:loginProcessingUrl("/user/login")
// 自带登录页面,http://localhost:8080/login
    // 定制登录页,loginPage("/toLogin")
    // 指定表单提交url:loginProcessingUrl("/user/login")
security.formLogin().loginPage("/toLogin")
        .usernameParameter("username").passwordParameter("password")
        .loginProcessingUrl("/user/login");
  • 注销
    • springboot自带注销页:/logout
    • 注销成功后跳转到/控制器
// 开启注销功能,源码http://localhost:8080/logout,并且注销成功后跳转到/的Controller
security.logout().logoutSuccessUrl("/");
  • 记住我
    • 本质就是存一个cookies,默认保存2周
    • 自定前端记住我的name:rememberMeParameter("remember")
// 开启记住我功能:本质就是记住一个cookies,默认保存2周
security.rememberMe().rememberMeParameter("remember");
<div class="field">
     <input type="checkbox" name="remember"> 记住我
</div>

4 补充:前端权限验证

  • 前端根据用户权限选择性展示元素,可以使用SpringSecurity和thymeleaf的整合包,更方便授权
  • 依赖:thymeleaf-extras-springsecurity5
    • Springboot2.1.X以上需要springSecurity5的版本
<!--security整合thymeleaf,便于前端整合
            Springboot2.1.X以上需要springSecurity5的版本
            但是xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"后缀还是4依然可以使用
-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>
  • 前端
    • 登录授权检查: 没有登录就消失登录按钮sec:authorize="!isAuthenticated()",反之亦然
    • 登录后,展示用户名:<span sec:authentication="name"
<!--登录注销-->
<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" th:href="@{/logout}">
            <!--从授权那里获取name-->
            用户名:<span sec:authentication="name"></span>
            <!-- 有bug不能使用,角色:<span sec:authentication="principal.getAuthorities()"></span>-->
        </a>
        <a class="item" th:href="@{/logout}">
            <i class="sign-out card icon"></i> 注销
        </a>
    </div>
</div>

11 整合Shiro

  • shiro官网:http://shiro.apache.org/,用Idea观看官方Quickstart源码,配置运行一遍
  • 核心三大对象:用户Subject, 管理用户SecurityManager, 连接数据Realms
  • Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
  • SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
  • Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
  • 前端资源:后期百度云整理

1 依赖

<!--前端交互整合-->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>
<!--整合Mybatis-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.22</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<!-- shiro-spring -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.3</version>
</dependency>
<!--thymeleaf-->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

2 Controller

  • token:登录表单提交的信息,封装成UsernamePasswordToken,这是shiro提供的,便于xxxRelam使用
@Controller
public class MyController {

    @RequestMapping({"/", "/index"})
    public String toIndex(Model model) {
        model.addAttribute("msg", "首页");
        return "index";
    }

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

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

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

    @RequestMapping("/login")
    public String login(String username, String password, Model model) {
        // 获取用户
        Subject user = SecurityUtils.getSubject();
        // 封装参数,获取token
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 验证登录
        try {
            // 执行登录操作,跨类调用
            user.login(token);
            model.addAttribute("msg", "成功登录");
            return "index";
        } catch (UnknownAccountException uae) {
            model.addAttribute("msg", "用户名错误");
            return "login";
        } catch (IncorrectCredentialsException ice) {
            model.addAttribute("msg", "用户密码错误");
            return "login";
        }
    }

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

2 ShiroConfig

  • 1 创建XXXrealm对象,注册进组件
  • 2 获取安全管理器DefaultWebSecurityManager,设置xxxReaml
  • 3 获取ShiroFilterFactoryBean
    • 创建ShiroFilterFactoryBean,设置安全管理器setSecurityManager
    • LinkedMap存储url和权限对应,setFilterChainDefinitionMap(map);
    • 指定登录映射:setLoginUrl
@Configuration
public class ShiroConfig {

    // 3 获取ShiroBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(
            @Qualifier("getDefaultWebSecurityManager") WebSecurityManager securityManager
    ) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        bean.setSecurityManager(securityManager);
        // 添加shiro的内置过滤器
        /*
            anon: 无需认证就可以登录
            authc:必须认证才能登录
            user: 必须拥有“记住我”这个功能
            perms:拥有对某个资源的权限才能访问
            role:拥有某个角色才能访问
         */
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        // 权限授权,访问url需要权限,支持通配符
        map.put("/user/add", "perms[user:add]");
        map.put("/user/update", "perms[user:update]");
        bean.setFilterChainDefinitionMap(map);
        // 设置登录url映射
        bean.setLoginUrl("/toLogin");
        // 设置未授权的请求
        bean.setUnauthorizedUrl("/noAuth");
        return bean;
    }

    // 2 获取安全管理器
    @Bean(name = "getDefaultWebSecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(
            @Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 直接userRealm()传参也可以,这里演示Spring指定自动注入
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    // 1 创建realm对象,需要自定义另一个类
    @Bean(name = "userRealm")
    public UserRealm userRealm() {
        return new UserRealm();
    }

    // 结合Mybatis,整合ShiroDialect进组件
    @Bean
    public ShiroDialect getShiroDialect(){
        return new ShiroDialect();
    }

}

3 userRelam

  • 类继承extends AuthorizingRealm
  • 先认证AuthenticationInfo
    • ShiroConfig中的配置的登录映射走这里,提交表单封装成一个token,通过token中的信息走数据库查询
    • 数据库查出用户名是否存在,如果存在就进行密码验证
      • new SimpleAuthenticationInfo(currentUser, currentUser.getPwd(), "");:第一个形参传递查询出用户作为subject,这个subject等待AuthorizationInfo使用;第二个参数是用户密码;第三个参数realmName
  • 后授权AuthorizationInfo
    • ShiroConfig的Map配置的Url被拦截走这里,创建简单的授权信息 new SimpleAuthorizationInfo()
    • AuthenticationInfo最后第一参数传递过来subject获取当前用户,再获取权限,封装进简单授权信息,完成简单权限设置
// 自定义realms,继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
    @Autowired
    private UserServiceImpl userService;

    //  授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 进入被拦截的url,就会进这个info
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 授权应该从数据库查出权限字段
        // info.addStringPermission("user:add");
        // 从 new SimpleAuthenticationInfo(queryUser, queryUser.getPwd(), "");传递过来第一个参数user最为subject
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User) subject.getPrincipal();
        // 从数据库中获取验证权限
        info.addStringPermission(currentUser.getPerms());
        return info;
    }

    // 用户认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 模拟数据库中查出用户名、密码
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        User currentUser = userService.queryUserByName(userToken.getUsername());
        // 验证用户名
        if (currentUser == null) {
            // 用户名不正确,就抛出UnknownAccountException
            return null;
        }
        // 密码验证,shiro完成,不需要用户判断.直接返回
        return new SimpleAuthenticationInfo(currentUser, currentUser.getPwd(), "");
    }
}

12 整合Swagger

1 第一个Swagger

  • 依赖:提供Swagger2、SwaggerUI
 <!-- springfox-swagger2 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<!-- springfox-swagger-ui -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>
  • helloworld
@RestController
public class HelloController {

    @RequestMapping(value = "/hello")
    public String hello() {
        return "hello";
    }
}
  • 配置Swagger,编写一个空白的MySwagger
@Configuration
@EnableSwagger2 // 开启Swagger2
public class MySwagger {

}

在这里插入图片描述

2 修改默认info信息

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

    // 配置SwaggerBean实例
    @Bean
    public Docket getDocker() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(changeInfo());
    }

    // 更改默认info
    private ApiInfo changeInfo() {
        Contact author_contact = new Contact("laoSong", "http://123", "123@qq.com");
        return new ApiInfo(
                        "swagger文档学习",
                        "学习swagger",
                        "v1.0",
                        "http://123",
                        author_contact,
                        "Apache 2.0",
                        "http://www.apache.org/licenses/LICENSE-2.0",
                        new ArrayList());

    }

}

在这里插入图片描述

3 配置扫描接口和路径

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

    // 配置SwaggerBean实例
    @Bean
    public Docket getDocker() {
        return new Docket(DocumentationType.SWAGGER_2)
                // 是否启用swagger,则浏览器不能访问
                // .enable(false)
                .apiInfo(changeInfo())
                .select()
                        // 扫描接口:,点进去看源码有很多种方式指定扫描方式
                        .apis(RequestHandlerSelectors.basePackage("com.ssl.controller"))
                        // 扫描路劲:过滤路径
                        .paths(PathSelectors.ant("/hello"))
                        .build();
    }

    // 更改默认info
    private ApiInfo changeInfo() {
        Contact author_contact = new Contact("laoSong", "http://123", "123@qq.com");
        return new ApiInfo(
                        "swagger文档学习",
                        "学习swagger",
                        "v1.0",
                        "http://123",
                        author_contact,
                        "Apache 2.0",
                        "http://www.apache.org/licenses/LICENSE-2.0",
                        new ArrayList());

    }

}

4 如何指定swagger在生产环境中使用

方法:

  1. 判断是不是生产环境的yml
  2. 注入enable(flag)
  • application.yml,指定环境配置时dev
server:
  port: 8080
# 选择使用哪些配置
spring:
  profiles:
    active: dev # 激活dev配置
---
server:
  port: 8081
spring:
  profiles: test
---
server:
  port: 8082
spring:
  profiles: dev
  • SwaggerConfig

    @Configuration
    @EnableSwagger2 // 开启Swagger2
    public class MySwagger {
    
        // 配置SwaggerBean实例
        @Bean
        public Docket getDocker(Environment environment) {
            // 判断是否是生产环境的配置文件
            Profiles isDevPro = Profiles.of("dev");
            boolean flag = environment.acceptsProfiles(isDevPro);
    
            return new Docket(DocumentationType.SWAGGER_2)
                    // 是否启用swagger,则浏览器不能访问
                    // .enable(false)
                    // 这里判断是dev生产环境才开启Swagger
                    .enable(flag)
                    .apiInfo(changeInfo())
                    .select()
                    // 扫描接口:,点进去看源码有很多种方式指定扫描方式
                    .apis(RequestHandlerSelectors.basePackage("com.ssl.controller"))
                    // 扫描路劲:过滤路径
                    .paths(PathSelectors.ant("/hello"))
                    .build();
        }
    
        // 更改默认info
        private ApiInfo changeInfo() {
            Contact author_contact = new Contact("laoSong", "http://123", "123@qq.com");
            return new ApiInfo(
                    "swagger文档学习",
                    "学习swagger",
                    "v1.0",
                    "http://123",
                    author_contact,
                    "Apache 2.0",
                    "http://www.apache.org/licenses/LICENSE-2.0",
                    new ArrayList());
    
        }
    
    }

5 API分组

  • 设置docket分组,配置多个不同名注入bean,供多人开发使用
@Bean
public Docket docket2() {
    return new Docket(DocumentationType.SWAGGER_2)
        .groupName("AA");
}

6 常用api注释

@ApiModel("User实体类")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @ApiModelProperty("用户名")
    private String username;
    @ApiModelProperty("用户密码")
    private String password;
}
@RestController
public class HelloController {

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

    //返回实体类,就会扫描进SwaggerUI中的modal
    @GetMapping(value = "/user")
    @ApiOperation("返回一个user")
    public User user() {
        return new User("张三", "123456");
    }
}

在这里插入图片描述

13 异步任务

方法上使用: @Async

启动类上使用:@EnableAsync

@Service
public class AsyncService {

    //告诉Spring这是一个异步的方法
    @Async
    public void hello() {
        try{
           Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("正在处理异步请求");
    }
}
@Controller
public class AsyncController {

    @Autowired
    private AsyncService asyncService;

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

    // 开启异步后,先返回结果,在等待后台处理请求
    @RequestMapping("/sleep")
    @ResponseBody
    public String sleep() {
        // hello开启异步后,会创建另一个线程进行该操作
        asyncService.hello();
        return "hello";
    }
}
// 启动类开启异步注解功能
@EnableAsync
@SpringBootApplication
public class TestApplication {

    public static void main(String[] args) {

        SpringApplication.run(TestApplication.class, args);
    }

}

14 邮件任务

  • 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
  • 待补充

15 定时任务

  • 待补充

16 整合Redis

17 整合Dubbo+Zookeeper

  • 学习分布式理论:待补充
  • 什么是RPC
    • Http:是无状态的,基于网络通信的协议
    • RPC:远程过程调用,是允许程序调用另一个地址空间的接口,两个核心:通讯和序列化

Dubbo概念

原理图

在这里插入图片描述