尝试在月底前更完, 之前看的那个教程虽然是2020年最新的, 但是感觉讲师有点半吊子, 慕名看了雷丰阳的SpringBoot教程https://www.bilibili.com/video/BV1Et411Y7tQ, 实在是精细而专业, 受益匪浅
认真要学还是亲自去看一遍网课, 本文主要是记录雷神授课时的笔记以及一些额外的细节, 相对较为详细, 可作参考
20201001更新: 都怪进巨太好看, 月底是更不玩了… 国庆再说吧…
最终还是做了个人, 赶在国庆假期最后一天把课程看完了. 后八章是进阶部分, 没有详细实践, 有些东西等要用再说吧
截至20201008, 更新完成16章111节的摘要

目录

1. Spring Boot入门

  1. Spring Boot来简化Spring应用开发, 约定大于配置, 去繁从简, just run就能创建一个独立的, 产品级别的应用

  2. 背景: J2EE笨重的开发, 繁多的配置, 低下的开发效率, 复杂的部署流程, 第三方技术集成难度大

  3. Spring Boot问题解决: Spring全家桶时代

  • Spring Boot: J2EE一站式解决方案
  • Spring Cloud: 分布式整体解决方案
  1. Spring Boot优点:
  • 快速创建独立运行的Spring项目以及主流框架的集成
  • 使用嵌入式的Servlet容器, 应用无需打包成WAR
  • starters自动依赖与版本控制
  • 大量的自动配置, 简化开发, 也可以修改默认值
  • 无需配置XML, 无代码生成, 开箱即用
  • 准生产环境的运行时应用监控
  • 与云计算天然继集成
  1. Spring Boot与微服务: 区别于单体应用架构风格
  1. Spring Boot前置环境约束:
  • jdk 1.7版本及以上版本
  • maven 3.x版本
  • spring tools suite 4.x.x版本, 截至本文发布时spring-boot发行的最新版本是4.8.0
  1. 关于如何安装springboot及启动一个Helloworld项目详见https://caoyang.blog.csdn.net/article/details/108700844
  • 因为笔者使用的是sts4.8.0, 而雷风阳使用的是sts1.5.9, 因此上手时参考的是另一个视频教程, 后来感觉还是雷风阳的教程比较规整, 因此切换回来后跳过了入门部分的一些章节, 比如sts4从零开始创建Helloworld项目的流程以及注解器和日志等细节, 该链接中笔者较为详细地记录了这些跳过的内容, 两个版本区别可能不会太大; 下文中很多笔记都是基于链接中创建的Helloworld项目继续完善的

2. Spring Boot配置

补充: .properties文件的注释是#, .yml文件不建议写注释

  1. properties与yml配置文件详见https://caoyang.blog.csdn.net/article/details/108700844第二章内容

  2. 配置文件数据注入到javabean详见https://caoyang.blog.csdn.net/article/details/108700844第二章内容

  • 除了使用@ConfigurationProperties(prefix="student")来实现配置文件数据注入外, 也可以使用@Value注解来实现(以Student类)👇

    public class Student {
    
        @Value("${student.id}")
        private Integer id;
    	
    	// 在Value注解中使用SpEL表达式
    	@Value("#{11*2}")
    	private Integer age;
    }
    
  • @ConfigurationProperties@Value注解在注入配置文件数据的区别

    • 前者写一个注解可以批量注入, 后者必须一个个写注解的注入
    • 前者支持松散语法绑定, 后者不支持(如变量名为驼峰命名的studentId, 可以绑定到)
    • 前者不支持SpEL表达式, 后者支持#{表达式}的写法, 关于SpEL表达式详见https://www.jianshu.com/p/e0b50053b5d3
    • 前者支持数值校验, 后者不支持
      • 如强者可以在private Integer id上方添加@Email的注解来限定是邮箱格式的字段, 后者添加@Email则无效(@Value应该比@Email更接近private Integer id)
    • 前者可以用于复杂类型, 后者只能注入简单类型的数据(如数组, 字典无法注入)
    • 两者在.yml.properties配置文件都可以获取到值
      • 若只是在某个业务逻辑中需要获取配置文件中的某项值, 一般使用@Value
      • javabean中一般使用@ConfigurationProperties
  • @PropertySource@ImportResource注解

    • @PropertySource: 加载指定的配置文件
      • 不加该注解是默认从全局配置文件application.propertiesapplication.yml中读取
      • 如果在src/main/resources/目录下新建了其他配置文件则需要在Student类上方添加注解@PropertySource(value={"classpath:xxx.properties","classpath:xxx.yml"}), 即可以一次性引入多个配置文件
    • @ImportSource: 导入Spring的xml配置文件, 让配置文件里面的内容生效
      • src/main/resources/目录下创建beans.xml文件并写入
      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context">
          <bean id="helloService" class="com.example.service.HelloService"></bean>
      </beans>
      
      • com.example.demo.service包中创建HelloService.java
      • 在用于junit test的文件里写入
      @Autowired
      ApplicationContext ioc;
      
      boolean flag = ioc.containsBean("helloService"); // flag为false
      
      • 这表明Spring Boot里面没有Spring的配置文件, 我们自己编写的配置文件, 也不能自动识别; 如果想让Spring的配置文件生效, 加载进来, 需要在主启动类上添加@ImportResource(locations={"classpath:beans.xml"})就会发现flag输出为true
      • Spring Boot并不推荐使用@ImportSource引入xml配置文件给容器中添加组件:
        • 配置类======Spring配置文件
        • 推荐使用全注解的方式来给容器添加组件, 而无需在主启动类上添加@ImportResource注解也可以得到junit test测试结果返回true👇
        // 在com.example.demo.config下新建的HelloworldConfig.java类
        package com.example.demo.config;
        
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import com.example.service.HelloService;
        /*
         * @Configuration: 指明当前类是一个配置类, 就是来替代之前的Spring的xml配置文件 
         * 在配置文件中是<bean></bean>标签添加组件
         * 
         */
        
        @Configuration
        public class HelloworldConfig {
        	
        	//将方法的返回值添加到容器中, 容i中这个组件默认的id就是方法名
        	@Bean
        	public HelloService helloService() {
        		return new HelloService();
        	}
        }
        
  1. 配置文件占位符
  • RandomValuePropertySource配置文件中可以使用随机数
    • ${random.value}
    • ${random.int}
    • ${random.long}
    • ${random.int(10)}
    • ${random.int[1024,65536]}
  • 属性配置占位符
    • 可以在配置文件中引用前面配置过的属性(优先级前面配置过的这里都能用)
    • ${app.name:默认值}来指定找不到属性时的默认值
    # application.properties文件
    app.name=MyApp
    app.description=${app.name} is a Spring Boot application
    app.owner=${app.user:caoyang}
    
  1. Profile多环境支持
  • Profile是Spring对不同环境提供不同配置功能的支持, 可以通过激活, 指定参数等方式快速切换环境
    • properties多profile文件形式:
      • 配置文件名可以是格式: application-{profile}.properties
        • 如新建application-dev.properties, application-prod.properties配置文件
        • 默认依然是使用application.properties配置文件
        • 如果想要默认使用其他配置文件, 需要在application.properties中写入spring.profiles.active=dev即可以使用application-dev.properties
    • yml配置文件多profile文档块模式:
      • 如果是application-{profile}.yml则更为容易, 在第一个文档块指定spring.profile.active值来决定要激活prod抑或dev, 如下所示👇
      server: 
        port: 8081
      spring:
        profiles:
          active: dev
      ----
      server: 
        port: 8082
      spring:
        profile: dev
      ----
      server: 
        port: 8083
      spring:
        profile: prod
      ----
      
      
    • 激活dev的方式:
      • 命令行带参数: java -jar spring-boot-xx-config-x.x.x-SNAPSHOT.jar --spring.profiles.active=dev, 可以在run的时候填写参数
      • 配置文件中指定: spring.profiles.active=dev
      • jvm虚拟机带参数: -Dspring.profiles.active=dev
  1. 配置文件加载位置
  • Spring Boot启动会扫描以下位置的application.properties或者application.yml文件作为Spring Boot的默认配置文件
    • file: ./config/
    • file: ./
    • classpath: ./config/
    • classpath: ./
    • 以上是按照优先级从高到低的顺序, 所有位置的文件都会被加载, 相同的内容高优先级配置内容将覆盖低优先级配置内容
    • classpath指代/src/main/resources目录
    • file指代项目工程目录, 即pom.xml配置文件所在目录
  • 我们也可以通过配置spring.config.location参数来改变默认位置
    • 命令行启动jar包指令: java -jar spring-boot-xx-config-x.x.x-SNAPSHOT.jar --spring.config.location=<绝对路径>
      • 即可以在打包完成后再给个新的配置文件路径
  1. 外部配置加载顺序: 官方文档中有17个, 这里选取其中常用的11个, 优先级从高到低
  • 命令行参数: java -jar spring-boot-xx-config-x.x.x-SNAPSHOT.jar --<参数名>=<参数值>
  • 来自java:com[/envJNDI属性
  • Java系统属性(System.getProperties())
  • 操作系统环境变量
  • RandomValuePropertySource配置的random.*属性值
  • jar包外部的application-{profile}.propertiesapplication.yml(带spring.profile)配置文件
  • jar包内部的application-{profile}.propertiesapplication.yml(带spring.profile)配置文件
  • jar包外部的application.propertiesapplication.yml(不带spring.profile)配置文件
  • jar包内部的application.propertiesapplication.yml(不带spring.profile)配置文件
  • @Configuration注解类上的@PropertySource
  • 通过SpringApplication.setDefaultProperties指定的默认属性
  1. 自动配置原理:
  • 配置文件中可以写什么东西?配置文件能配置的属性参照文档
  • 自动配置原理:
    • SpringBoot启动时加载主配置类, 开启了自动配置功能@EnableAutoConfiguration
    • @EnableAutoConfiguration的作用
      • 利用@EnableAutoConfigurationImportSelector给容器中导入一些组件
      • 可以参考selectImports()方法的内容
      • List<String> configuration = getCandidateConfigurations(annotationMetadata, attributes);获取候选的配置
      • 将类路径下META-INF/spring.factories里面配置的所有EnableAutoConfiguration的值添加进来到容器中
      • 每一个这样的xxxAutoConfiguration类都是容器中的一个组件, 加入到容器中, 用它们来做自动配置
    • 配一个自动配置类进行自动配置功能
    • HttpEncodingAutoConfiguration为例解释自动配置原理
    @Configuraion // 表明这是一个配置类
    @EnableConfigurationProperties(HttpEncodingProperties.class) // 启动指定类ConfigurationProperties功能, 将配置文件中对应值和HttpEncodingProperties绑定起来
    @ConditionalOnWebApplication // Spring底层@Conditional注解, 根据不同的条件, 如果满足指定的条件, 整个配置类里面的配置就会生效, 判断当前应用是否是web应用, 如果是则当前配置类生效
    @ConditionalOnClass(CharacterEncodingFilter.class) // 判断当前项目有没有这个类, 用于乱码解决的过滤器	@ConditionalOnProperty(prefix="spring.http.encoding",value="enabled",matchIfMissing=true) // 判断配置文件中是否存在某个配置spring.http.encoding.enabled; 如果不存在, 判断也是成立, 即便我们配置文件中不配置spring.http.encoding.enabled, 也会默认生效(matchIfMissing=true)
    public class HttpEncodingAutoConfiguration {
    
    	private final HttpEncodingProperties properties; // 它已经和SpringBoot的配置文件映射了
    	
    	//只有一个有参构造器的情况下, 参数的值就会从容器中拿
    	public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
    		this.properties = properties;
    	}
    	
    	
    	@Bean // 给容器中添加一个组件, 这个组件的某些值需要从properties中获取
    	@ConditionalOnMissing
    }
    
    • 比如可以配置spring.http.encoding.enabled=true, spring.http.encoding.charset=utf-8, spring.http.encoding.force=true, 都是在预先设定好的
    • 所有在配置文件中能配置的属性都是在xxxProperties类中封装着, 配置文件能配置什么就可以参照某个功能对应的属性类
  • SpringBoot的精髓
    • SpringBoot启动会加载大量的自动配置类
    • 需要的功能有没有SpringBoot默认写好的自动配置类? 没有就需要自己写
    • 我们再来看这个自动配置类中到底配置了哪些组件, 只要我们要用的组件有, 就不需要再来配置了
    • 给容器中自动配置类添加组件的时候, 会从properties类中获取某些属性, 我们就可以在配置文件中指定这些属性的值
    • xxxAutoConfiguration自动配置类给容器添加组件 --> xxxProperties类封装配置文件中相关属性
  • 一些细节:
    • @Conditional派生注解
      • 作用: 必须是@Conditional指定的条件成立, 才给容器中添加组件, 配置文件中的所有内容才会生效
      • @ConditionalOnJava: 系统的Java版本是否符合要求
      • @ConditionalOnBean: 容器中存在指定Bean
      • @ConditionalOnMissingBean: 容器中不存在指定Bean
      • @ConditionalOnExpression: 满足SpEL表达式指定
      • @ConditionalOnClass: 系统中有指定的类
      • @ConditionalOnMissingClass: 系统中没有指定的类
      • @ConditionalOnSingleCandidate: 容器中只有一个指定的Bean, 或者这个Bean是首选的Bean
      • @ConditionalOnProperty: 系统中指定的属性是否有指定的值
      • @ConditionalOnResource: 类路径下是否存在指定资源文件
    • application.properties配置文件中首行写入debug=true
      • 开启Spring调试模式: 控制台里打印自动配置报告, 哪些配置类使用, 哪些没有使用

3. Spring Boot日志

3.1 日志框架分类和选择

  1. 市面上的日志框架:
  • 日志门面(日志的抽象层):
    • JCL: Jakarta Commons Logging, 这个最后更新是2014年, 基本已经弃用了
    • slf4j: Simple Logging Facade for java
    • jboss-logging
  • 日志实现:
    • JUL: java.util.logging
    • logback
    • log4j
    • log4j2
  • 选择日志门面中的一个作为框架(抽象层), 再选择日志实现中的一个来实现

SpringBoot底层是Spring框架, 默认使用的是JCL, 但SpringBoot选用的是slf4j呵呵logback

3.2 slf4j使用原理

  1. 如何在系统中使用slf4j
  • 开发的时候, 日志记录方法的调用, 不应该直接调用体制的实现类, 而是调用日志抽象层里面的方法
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(HelloWorld.class);
  	  logger.info("Hello World");
    }
}

每一个日志的实现框架都有自己的配置文件, 使用slf4j以后, 配置文件还是做成日志实现框架自己本身的配置文件

3.3 其他日志框架统一转换为slf4j

  1. 遗留问题
  • 开发A系统(slf4j+logback): Spring(commons-logging), Hibernate(jboss-logging), MyBatis等
  • 统一日志记录, 即便是别的框架也一起使用slf4j来输出?

3.4 SpringBoot日志关系

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

SpringBoot使用spring-boot-starter-logging依赖来实现日志功能

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
  1. SpringBoot底层也是使用slf4j+logback的方式进行日志记录
  2. SpringBoot也把其他的日志都替换成了slf4j
  3. SpringBoot使用了中间替换包
  4. 如果我们要引入其他框架, 一定要把这个框架的默认日志依赖移除
  • Spring框架用的是commons-logging
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-core</artifactId>
    	<exclusion>
    		<groupId>commons-logging</groupId>
    		<artifactId>commons-logging</artifactId>
    	</exclusion>
    </dependency>
    
  • SpringBoot能自动适配所有的日志, 而且底层使用slf4j+logback的方式记录日志, 引入其他框架的时候, 只需要把这个框架依赖的日志框架排除掉

3.5 SpringBoot默认配置

  1. SpringBoot默认输出日志级别是info
  • logger.level.com.example.demo = info: 指定com.example.demo包的输出级别为info及以上
  • logging.file = F:/spring-boot-helloworld.log: 指定日志输出位置(精确到文件)
  • logging.path = /spring/log: 在当前磁盘的根目录下创建spring文件夹和里面的log文件夹, 使用spring.log作为默认的文件名
  • logging.pattern.console = %d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n: 在控制台输出的日志格式
  • logging.pattern.file = %d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} === %msg%n: 指定文件中输出的格式
  • 日志输出格式:
    • %d表示日期时间
    • %thread表示线程名
    • %-5level级别从左显示5个字符宽度
    • %logger{50}表示logger名字最长50个字符, 否则按照句点分割
    • %msg表示日志消息
    • %n表示换行符
  1. LoggerFactory的logger对象方法(级别从低到高)
  • logger.trace()
  • logger.debug()
  • logger.info()
  • logger.warn()
  • logger.error()

3.6 指定日志文件和日志Profile功能

  1. 指定配置
  • 给类路径下放上每个日志框架自己的配置文件即可, SpiringBoot就不适用默认配置了
  • 不同日志系统的自定义配置文件
    • logback: logback-spring.xml, logback-spring.groovy, logback.xml, logback.groovy
    • log4j2: log4j2-spring.xml, log4j2.xml
    • JDK(java util logging): logging.properties
      • 如果文件名为logging.xml: 直接就被日志框架识别了
      • 如果文件名为logback-spring.xml: 日志框架就不直接加载日志的配置项, 由SpringBoot解析日志配置, 可以使用SpringBoot的高级Profile功能
        <springProfile name="staging">
            <!-- configuration to be enabled when the "staging" profile is active -->
            可以指定某段配置只在某个环境下生效
            比如可以把staging改成dev即在开发环境生效, !dev则非开发环境生效
        </springProfile>
        
  • 举个logback-spring.xml配置文件的例子:
    • 集成到springboot的yml格式配置文件的示例:
      logging:
      config: classpath:logback-spring.xml
      level:
        dao: debug
        org:
          mybatis: debug
      
    • 具体logback配置:
      <?xml version="1.0" encoding="UTF-8"?>
      <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
      <!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
      <!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。
                         当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
      <!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
      <configuration  scan="true" scanPeriod="10 seconds">
      <contextName>logback</contextName>
      
      <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
      <property name="log.path" value="G:/logs/pmp" />
      
      <!--0. 日志格式和颜色渲染 -->
      <!-- 彩色日志依赖的渲染类 -->
      <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
      <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
      <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
      <!-- 彩色日志格式 -->
      <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
      
      <!--1. 输出到控制台-->
      <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                  <level>debug</level>
            </filter>
            <encoder>
                  <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
                  <!-- 设置字符集 -->
                  <charset>UTF-8</charset>
            </encoder>
      </appender>
      
      <!--2. 输出到文档-->
      <!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
      <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 正在记录的日志文档的路径及文档名 -->
            <file>${log.path}/web_debug.log</file>
            <!--日志文档输出格式-->
            <encoder>
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
                  <charset>UTF-8</charset> <!-- 设置字符集 -->
            </encoder>
            <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                  <!-- 日志归档 -->
                  <fileNamePattern>${log.path}/web-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                  <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                        <maxFileSize>100MB</maxFileSize>
                  </timeBasedFileNamingAndTriggeringPolicy>
                  <!--日志文档保留天数-->
                  <maxHistory>15</maxHistory>
            </rollingPolicy>
            <!-- 此日志文档只记录debug级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                  <level>debug</level>
                  <onMatch>ACCEPT</onMatch>
                  <onMismatch>DENY</onMismatch>
            </filter>
      </appender>
      
      <!-- 2.2 level为 INFO 日志,时间滚动输出  -->
      <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 正在记录的日志文档的路径及文档名 -->
            <file>${log.path}/web_info.log</file>
            <!--日志文档输出格式-->
            <encoder>
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
                  <charset>UTF-8</charset>
            </encoder>
            <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                  <!-- 每天日志归档路径以及格式 -->
                  <fileNamePattern>${log.path}/web-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                  <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                        <maxFileSize>100MB</maxFileSize>
                  </timeBasedFileNamingAndTriggeringPolicy>
                  <!--日志文档保留天数-->
                  <maxHistory>15</maxHistory>
            </rollingPolicy>
            <!-- 此日志文档只记录info级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                  <level>info</level>
                  <onMatch>ACCEPT</onMatch>
                  <onMismatch>DENY</onMismatch>
            </filter>
      </appender>
      
      <!-- 2.3 level为 WARN 日志,时间滚动输出  -->
      <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 正在记录的日志文档的路径及文档名 -->
            <file>${log.path}/web_warn.log</file>
            <!--日志文档输出格式-->
            <encoder>
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
                  <charset>UTF-8</charset> <!-- 此处设置字符集 -->
            </encoder>
            <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                  <fileNamePattern>${log.path}/web-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                  <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                        <maxFileSize>100MB</maxFileSize>
                  </timeBasedFileNamingAndTriggeringPolicy>
                  <!--日志文档保留天数-->
                  <maxHistory>15</maxHistory>
            </rollingPolicy>
            <!-- 此日志文档只记录warn级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                  <level>warn</level>
                  <onMatch>ACCEPT</onMatch>
                  <onMismatch>DENY</onMismatch>
            </filter>
      </appender>
      
      <!-- 2.4 level为 ERROR 日志,时间滚动输出  -->
      <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 正在记录的日志文档的路径及文档名 -->
            <file>${log.path}/web_error.log</file>
            <!--日志文档输出格式-->
            <encoder>
                  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
                  <charset>UTF-8</charset> <!-- 此处设置字符集 -->
            </encoder>
            <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                  <fileNamePattern>${log.path}/web-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                  <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                        <maxFileSize>100MB</maxFileSize>
                  </timeBasedFileNamingAndTriggeringPolicy>
                  <!--日志文档保留天数-->
                  <maxHistory>15</maxHistory>
            </rollingPolicy>
            <!-- 此日志文档只记录ERROR级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                  <level>ERROR</level>
                  <onMatch>ACCEPT</onMatch>
                  <onMismatch>DENY</onMismatch>
            </filter>
      </appender>
      
      <!--
            <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
            以及指定<appender>。<logger>仅有一个name属性,
            一个可选的level和一个可选的addtivity属性。
            name:用来指定受此logger约束的某一个包或者具体的某一个类。
            level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
                    还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
                    如果未设置此属性,那么当前logger将会继承上级的级别。
            addtivity:是否向上级logger传递打印信息。默认是true。
            <logger name="org.springframework.web" level="info"/>
            <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
      -->
      
      <!--
            使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
            第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
            第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
            【logging.level.org.mybatis=debug logging.level.dao=debug】
       -->
      
      <!--
            root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
            level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
            不能设置为INHERITED或者同义词NULL。默认是DEBUG
            可以包含零个或多个元素,标识这个appender将会添加到这个logger。
      -->
      
      <!-- 4. 最终的策略 -->
      <!-- 4.1 开发环境:打印控制台-->
      <springProfile name="dev">
            <logger name="com.sdcm.pmp" level="debug"/>
      </springProfile>
      
      <root level="info">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
      </root>
      
      <!-- 4.2 生产环境:输出到文档
      <springProfile name="pro">
            <root level="info">
                  <appender-ref ref="CONSOLE" />
                  <appender-ref ref="DEBUG_FILE" />
                  <appender-ref ref="INFO_FILE" />
                  <appender-ref ref="ERROR_FILE" />
                  <appender-ref ref="WARN_FILE" />
            </root>
      </springProfile> -->
      
      </configuration>
      

3.7 切换日志框架

  1. 可以按照slf4j的日志适配图, 进行相关的切换
  2. slf4j切换为log4j的方式:
<dependency>  
	<groupId>org.spring.framework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<exclusions>
		<exclusion>
			<artifactId>logback-classic</artifactId>
			<groupId>ch.qos.logback</groupId>
		</exclusion>
		<exclusion>
			<artifactId>log4j-over-slf4j</artifactId>
			<groupId>org.slf4j</groupId>
		</exclusion>
	</exclusions>
</dependency>  
<dependency>  
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-log4j12</artifactId>	
</dependency>  
  1. slf4j切换为log4j2:
<dependency>  
	<groupId>org.spring.framework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<exclusions>
		<exclusion>
			<artifactId>spring-boot-starter-logging</artifactId>
			<groupId>org.spring.framework.boot</groupId>
		</exclusion>
	</exclusions>
</dependency>  
<dependency>  
	<groupId>org.spring.framework.boot</groupId>
	<artifactId>spring-boot-starter-log4j2</artifactId>	
</dependency>  

4. Spring Boot与Web开发

4.1 简介

  1. 使用SpringBoot进行Web开发的步骤
  • 创建SpringBoot应用, 选中我们需要的模块
    • 普通小项目只需要勾选Web下的Spring Web即可
    • 需要连接MyBatis, Redis, MySQL都是有对应的选项可以勾选
  • Spring已经默认将这些场景配置好了, 只需要在配置文件中指定少量配置就可以运行起来
    • 关于如何修改配置, 可以修改哪些配置详见第二章
    • 可以自己在STS4编辑器左栏Package Explorer中Maven Denpendencies–>spring-boot-autoconfigure-x.x.x.RELEASE.jar中查看相关的源码配置
      xxxxAutoConfiguration: 帮我们给容器中自动配置组件
      xxxxProperties: 配置类来封装配置文件的内容
      
  • 自己编写业务代码
  1. 目前sts4直接新建一个项目后在主启动类下写个简单的controller类就可以直接运行个helloworld出来了, 别的什么都不需要额外配置
//简单的controller类
package com.example.demo;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Controller {
	/*
	 * 在这里使用@RequestMapping建立请求映射
	 */
	@RequestMapping(value="/helloworld",method=RequestMethod.GET)
	public String helloworld() {
		return "<h1>Helloworld caoyang</h1>";
	}
}

4.2 webjars与静态资源映射资源

  1. SpringBoot对静态资源的配置规则
@ConfigurationProperties(prefix="spring.resources",ignoreUnknownFields=false)
public class ResourceProperties implements ResourceLoaderAware {
    // 可以设置和静态资源有关的参数, 缓存时间等
}
  • 所有/webjars/**的资源, 都去classpath:META-INF/resources/webjars找资源
    • webjars: 以jar包的方式引入静态资源
    • 如何引入webjars依赖可参考https://www.webjars.org/
    • 引入jquery的webjars:
      <!-- 引入jquery的webjars, 3.5.1是当时最新版本 -->
      <dependency>
          <groupId>org.webjars</groupId>
          <artifactId>jquery</artifactId>
          <version>3.5.1</version>
      </dependency>
      
      • 访问localhost:8080/webjars/jquery/3.5.1/jquery.js即可看到已经引入的jquery依赖
  • /**访问当前项目的任何资源, 静态资源的文件夹(classpath指代/src/main/resources目录)
    • classpath:/META-INF/resources/
    • classpath:/resources/
    • classpath:/static/
    • classpath:/public/
    • 当前项目根路径
    • 如访问localhost:8080/xxx就可以访问到自己的静态资源, 一般就是放在src/main/resources目录下里面
  • 欢迎页: 静态资源文件夹下的所有index.html页面, 被/**映射
    • localhost:8080/就是欢迎页
  • 标签页的图标: 所有的**/favicon.ico都是在静态资源文件夹下查找
    • 如在/src/main/resources/static里放一张图标文件favicon.ico

4.3 引入thymeleaf

  1. 模板引擎:
  • JSP
  • Velocity
  • Freemarket
  • Thymeleaf: SpringBoot推荐, 语法简单, 功能强大
  1. 引入thymeleaf
<!-- 引入thymeleaf模板引擎 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 默认使用2.1.6版本, 这太低了, 需要自定义版本, 在properties节点下添加, 特别注意3.x.x版本的thymeleaf需要2.x.x版本的layout来支持, 后续可能会更新, 需要参考github上的版本
    <thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
    	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
    
  • 这里插个题外话, properties节点下可以添加
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    

4.4 thymeleaf语法

  1. 检查ThymeleafProperties源码可知
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8"); 
    private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
  • 可以在application.yml配置文件中做如下配置
spring:
  thymeleaf: 
    cache: false
    prefix: classpath:/templates/
    suffix: .html
    encoding: UTF-8
    mode: HTML
  • 只要我们把HTML页面放在classpath:/templates下, 以.html结尾就可以自动被thymeleaf渲染
  • 具体语法可以在官网下载官方文档PDF查看
  • 编写controller类: 这里有个坑是Controller类的类注解不能是@RestController, 只能是@Controller, 否则不能实现页面的绑定
    package com.example.demo;
    
    import java.util.Map;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.servlet.ModelAndView;
    
    @Controller // 此处不能是@RestController, 否则无法实现跳转页面
    public class Web1Controller {
    	/*
    	 * 在这里使用@RequestMapping建立请求映射
    	 */
    	@RequestMapping("/success")
    	public String success(Map<String,Object> map) {
    		// classpath:/templates/thymeleaftest.html
    		map.put("hello","你好");
    		return "success";
    	}
    }
    
  • /src/main/resources/templates/目录下编写success.html文件
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8">
    <title>thymeleaf test</title>
    </head>
    <body>
    	<h1>成功!</h1>
    	<div th:text="${hello}"></div> <!-- 如果div中有信息, 则会被替换成hello变量的值, 正常访问则显示div中的信息 -->
    </body>
    </html>
    
    • 注意点: 导入thymeleaf的名称空间: <html lang="en" xmlns:th="http://www.thymeleaf.org">, 以获取语法提示
    • 访问localhost:8080/success即可看到"成功!你好"的字样
  1. thymeleaf语法规则
  • th标签: 官方文档PDF的第十章
    • th:textth:utext: 文本(前者转义后者不转义)
    • th.each: 循环
    • th.if: 条件
    • th:attr任意属性修改, attr可以是id, class这些原生的HTML属性, 都可以修改
  • 表达式: 官方文档PDF第四章
    • ${...}: 取变量值
      • 去自定义值
        • ${person.name}
        • ${persons[0]}
      • 使用内置的基本对象
        • ${ctx}: 上下文对象
        • ${var}: 上下文变量
        • ${locale}: 上下文位置
        • ${request}: 请求对象
        • ${session}: 会话对象
        • ${servletContext}: servletcontext对象
      • 使用基本的工具对象: number, boolean, float等
    • *{...}: 选择表达式, 和${...}功能基本相同, 在取字典型的对象值时在对象控制域内可以简写(下面的firstname等价于写session.user.firstname), 主要是配合th:object使用
      <div th:object="${session.user}">
          <p>Name: <span th:text="*{firstname}">Sebastian</span></p>
      </div>
      
    • #{...}: 取国际化内容
    • @{...}: 定义URL链接, 一般用于th:href标签
      • @{localhost:8080/helloworld}@{/helloworld}
    • ~{...}: 片段引用表达式
      • ...
  • 字面量:
    • 字符串
    • 数字
    • 布尔型
    • null
    • 字面量标记: one, sometext, main
  • 文本操作:
    • 字符串拼接: “+”
    • 字面量替换: |The name is ${name}|
  • 算术运算符: …
  • 逻辑运算符: …
  • 比较运算符: 注意大于号和小于号因为在HTML里是特殊字符可以用gt, lt, ge, le替代, 等于和不等于是eq和ne
  • 条件运算符:
    • if-then: (if) ? (then)
    • if-then-else: (if) ? (then) : (else)
    • default: (value) ?: (defaultvalue)
  • 特殊标记:
    • No-operation:
  • 简单的一个样例
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8">
    <title>thymeleaf test</title>
    </head>
    <body>
    	<h1>成功!</h1>
    	<div th:text="${hello}">这是转义的信息</div>
    	<div th:utext="${hello}">这是不转义的信息</div>
    	<br/>
    	<h4 th:text="${user}" th:each="user:${users}"></h4>
    	<span th:each="user:${users}">[[${user}]]</span><!-- 行内写法: [[...]]不转义 -->
    	<br/>
    	<span th:each="user:${users}">[(${user})]</span><!-- 行内写法: [(...)]将转义 -->
    	
    </body>
    </html>
    

4.5 SpringMVC自动配置原理

  1. SpringBoot自动配置好了SpringMVC
  2. 以下是SpringBoot对SpringMVC的默认配置:
  • 自动配置了```ViewResolver````: 视图解析器, 根据方法的返回值得到视图对象(View), 视图对象决定如何渲染(转发,重定向等)
    • ContentNegotiatingViewResolver: 组合所有的视图解析器的
    • 如何定制: 我们可以自己给容器中添加一个视图解析器, 自动将其组合进来
  • 静态资源文件夹路径和webjars
  • 静态首页访问: index.html
  • 标签页图标favicon.icon
  • 自动注册几个工具类:
    • Converter: 转换器
    • Fomatter: 格式化器
    • GenericConverter: 总体转换器
  • 支持HttpMessageConverter:
    • HttpMessageConverter是SpringMVC用来转换Http请求和响应的
    • HttpMessageConverter是从容器中确定, 获取所有的HttpMessageConverter
    • 自己给容器中添加HttpMessageConverter, 只需要将自己的组件放在容器中
  • 定义错误代码生成规则: MeaageCodesResolver
  • 我们可以配置一个ConfigurableWebBindingInitializer来替换默认的配置, 添加到容器
    初始化WebDataBinder
    请求数据=====JavaBean
    
    • org.springframework.boot.autoconfigure.web: web的所有自动场景
  1. 如何修改SpringMVC的默认配置
  • 模式:
    • SpringBoot在自动配置很多组件的时候, 先看容器中有没有用户自己配置的(@Bean,@Component), 如果有就用用户配置的, 如果没有才会自动配置, 如果有些组件可以有多个(如ViewResolver), 则会将用户配置的和自己默认的组合起来
    • 在SpringBoot中会有非常多的xxxWebMvcConfigurer, 需要多留心

4.6 扩展与全面接管SpringMVC

  1. 扩展SpringMVC:
<!-- springmvc.xml的一个示例, 一个控制器与一个拦截器 -->
<mvc:view-controller path="/hello" view-name="success">
<mvc:interceptors>
	<mvc:interceptors>
		<mvc:mapping path="/hello" />
		<bean></bean>
	</mvc:interceptors>
<mvc:interceptors>
  • 编写一个配置类(@Configuration), 是WebMvcConfigurerAdapter类型, 不能标注@EnableWebMvc
    • 注意教程中提到的继承WebMvcConfigurerAdapter已经被弃用, 改成继承WebMvcConfigurationSupport
    • 既保留了所有的自动配置也能用我们扩展的配置
    package com.example.demo;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
    
    
    @Configuration
    public class Web1Config extends WebMvcConfigurationSupport {
    	@Override
    	public void addViewControllers(ViewControllerRegistry registry) {
    		registry.addViewController("/caoyang").setViewName("success");
    	}
    }
    
    • 此时直接访问localhost:8080/caoyang即可看到success页面的信息
  • 原理:
    • WebMvcConfigurerAdapter类是SpringMVC的自动配置类
    • 在做其他自动配置时会导入, @Import(EnableWebMvcConfiguration.class)
    • 容器中所有的WebMvcConfigurerAdapter都会一起起作用
    • 我们的配置类也会被调用
      • 效果: SpringMVC的自动配置和我们的扩展配置都会起作用
  1. 全面接管SpringMVC配置
  • SpringBoot对SpringMVC的自动配置不需要了, 所有都是我们自己配
  • 只需要在配置类上添加@EnableWebMvc注解, 注意第1点里的配置文件是只能添加一个@Configuration注解
  • 一般不建议全面接管, 否则就连index.html也无法访问了
  • 原理: 为什么@EnableWebMvc会使SpringMVC的默认配置都失效
    • @EnableWebMvc注解将WebMvcConfigurationSupport组件导入进来
    • 导入的WebMvcConfigurationSupport组件只是SpringMVC的基本功能配置

4.7 【实验】引入资源

  1. 编写配置类实现路由映射

package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.context.annotation.Bean;

@Configuration
public class Web1Config extends WebMvcConfigurationSupport {
	
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/caoyang").setViewName("success"); // 可以通过访问/caoyang从而起到访问success的作用
		registry.addViewController("/").setViewName("success");
		registry.addViewController("/index.html").setViewName("success");
	}
}

4.8 【实验】国际化

  1. 编写国际化配置文件
  • classpath下新建i18n文件夹, 其中新建三个配置文件login.properties, login_en_US.properties, login_zh_CN.properties
    • 在里面分别填写一些默认配置数据(这是中文的模板, 还有英文的)
      login.btn=登录
      login.password=密码
      login.remember=记住我
      login.tip=请登录
      
  1. SpringBoot自动配置好了管理国际化资源的文件
  • application.properties中进行国际化配置文件的路径配置: spring.messages.basename=i18n.login
  1. 去页面获取国际化的值:
  • 官方文档第四章: #{...}获取国际化信息: <div th:text="#{login.tip}"></div>即可
  • 此时根据浏览器的语言或地区信息就可以实现中英文的切换
  1. 原理:
  • 根据请求头中的Accept-language字段的语言排序来决定显示中文还是英文
  • 当然可以重写LocaleResolver👇
    package com.example.demo;
    
    import java.util.Locale;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.util.StringUtils;
    import org.springframework.web.servlet.LocaleResolver;
    
    public class MyLocaleResolver implements LocaleResolver{
    	
    	@Override
    	public Locale resolveLocale(HttpServletRequest request) {
    		String l = request.getParameter("l");
    		Locale locale = Locale.getDefault(); // 获取操作系统的默认值
    		if(!StringUtils.isEmpty(l)) {
    			String[] split = l.split("_");
    			locale = new Locale(split[0],split[1]);
    		}
    		return locale;
    	}
    
    	@Override
    	public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
    		// TODO Auto-generated method stub
    		
    	}
    }
    
    • 这样可以实现在访问的URL带请求字符串l=en_US或者l=zh_CN来切换语言, 而不是根据浏览器的默认语言配置得到的请求头来配置语言信息的

4.9 【实验】登录和拦截器

  1. Controller里写一个简单的登录视图创建(需要在templates下新建login.html与dashboard.html, 简单编写即可)
@RequestMapping(value="/login",method=RequestMethod.GET)
public String login() {
	return "login";
	
}
	
@RequestMapping(value="/user/login",method=RequestMethod.POST)
public String login(
	@RequestParam("username") 
	String username,
	@RequestParam("password") 
	String password,
	Map<String,Object> map
) {
	if(StringUtils.isEmpty(username) && "123456".equals(password)) {
		//登录成功
		return "dashboard";
	} else {
		//登陆失败
		map.put("msg","用户名密码错误");
		return "login";
	}
	
}
  • login.html文件
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Login</title>
    </head>
    <body>
    
    <form th:action="@{/user/login}" method="post">
    	<p th:text=${msg} th:if="${not #strings.isEmpty(msg)}"></p>
    	<input type="text" name="username" placeholder="用户名">
    	<br>
    	<input type="password" name="password" placeholder="密码">
    	<br>
    	<button type="submit">登录</button>
    </form>
    
    </body>
    </html>
    
  • dashboard文件
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Dashboard</title>
    </head>
    <body>
    
    这里是dashboard
    
    </body>
    </html>
    
  1. 注意点:
  • 开发时要设置spring.thymeleaf.cache=false来禁用缓存, 确保修改模板后更新生效
  • 为了防止登录成功后可以重复提交表单, 可以设计重定向
  • 可以在登录成功后在session中添加loginUser便于后续的登录状态跟踪
    @RequestMapping(value="/user/login",method=RequestMethod.POST)
    public String login(
    	@RequestParam("username") 
    	String username,
    	@RequestParam("password") 
    	String password,
    	Map<String,Object> map,
    	HttpSession session
    ) {
    	System.out.println("这里是username: "+username);
    	System.out.println("这里是password: "+password);
    	if(!StringUtils.isEmpty(username) && "123456".equals(password)) {
    		//登录成功
    		System.out.println("登录成功");
    		session.setAttribute("loginUser",username);
    		return "redirect:/main.html"; // 添加重定向, 为了防止重复提交表单, 然后main.html在Config里被映射到了dashboard.html
    	} else {
    		//登陆失败
    		map.put("msg","用户名密码错误");
    		System.out.println("登录失败");
    		return "login";
    	}
    	
    }
    
    • 需要再去AddViewControllers类里写个视图映射的
    package com.example.demo;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.LocaleResolver;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
    import org.springframework.context.annotation.Bean;
    
    @Configuration
    public class Web1Config extends WebMvcConfigurationSupport {
    	
    	@Override
    	public void addViewControllers(ViewControllerRegistry registry) {
    		registry.addViewController("/caoyang").setViewName("success"); // 可以通过访问/caoyang从而起到访问success的作用
    		registry.addViewController("/").setViewName("success");
    		registry.addViewController("/index.html").setViewName("success");
    		registry.addViewController("/main.html").setViewName("dashboard");
    	}
    	
    	@Bean
    	public LocaleResolver localeResolver() {
    		return new MyLocaleResolver();
    	}
    }
    
  1. 拦截器:
  • 首先需要创建拦截器类
    package com.example.demo;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    /*
     * 拦截器: 登陆检查
     */
    
    public class LoginHandlerInterceptor implements HandlerInterceptor{
    
    	@Override
    	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    			throws Exception {
    		Object user = request.getSession().getAttribute("loginUser");
    		//System.out.println(user.toString());
    		if(user==null) {
    			//未登录, 返回index.html
    			request.setAttribute("msg", "未登录, 请登录!");
    			request.getRequestDispatcher("/user/login").forward(request,response);
    			return false;
    		} else {
    			//已登录
    			return true;
    		}
    	}
    
    	@Override
    	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
    			ModelAndView modelAndView) throws Exception {
    		// TODO Auto-generated method stub
    		//HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    	}
    
    	@Override
    	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
    			throws Exception {
    		// TODO Auto-generated method stub
    		//HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    	}
    }
    
  • 然后需要去configuration类里注册拦截器
    package com.example.demo;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.LocaleResolver;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
    import org.springframework.context.annotation.Bean;
    
    @Configuration
    public class Web1Config extends WebMvcConfigurationSupport {
    	
    	@Override
    	public void addViewControllers(ViewControllerRegistry registry) {
    		//registry.addViewController("/caoyang").setViewName("success"); // 可以通过访问/caoyang从而起到访问success的作用
    		//registry.addViewController("/").setViewName("success");
    		//registry.addViewController("/index.html").setViewName("success");
    		registry.addViewController("/main.html").setViewName("dashboard");
    	}
    	
    	// 注册拦截器
    	@Override
    	public void addInterceptors(InterceptorRegistry registry) {
    		//super.addInterceptors(registry);
    		// Springboot已经做好了静态资源映射, 拦截器不需要考虑
    		registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
    				.excludePathPatterns("/index.html","/","/user/login"); // 这些页面不需要登录
    	}
    	
    	
    	// 处理地区信息的Bean组件
    	@Bean
    	public LocaleResolver localeResolver() {
    		return new MyLocaleResolver();
    	}
    }
    
  • 拦截器通过检查session中是否有user来确定用户是否登录

4.10 【实验】Restful实验要求

  1. 实验要求:
  • RestfulCRUD: CRUD满足Rest风格
    • URI: /资源名称/资源表示
    • HTTP请求方式区分对资源CRUD操作
    • 普通CRUD(uri来区分操作)与RestfulCRUD的比较
      • 查询: getEmpv.s. emp---GET
      • 添加: addEmp?xxxv.s. emp---POST
      • 修改: updateEmp?id=xxx&xxx=xxv.s. emp/{id}---PUT
      • 删除: deleteEmp?id=xxxv.s. emp/{id}---DELETE
  1. 实验的请求架构:
  • 查询所有员工: 请求URI为emps, 请求方式为GET
  • 查询某个员工: 请求URI为emp/1, 请求方式为GET
  • 来到添加页面: 请求URI为emp, 请求方式为GET
  • 添加员工: 请求URL为emp/1, 请求方式为POST
  • 来到修改页面(查出员工信息进行信息回显): 请求URI为emp/{id}, 请求方式为GET
  • 修改员工: 请求URI为emps, 请求方式为PUT
  • 删除员工: 请求URI为emp/1, 请求方式为DELETE

4.11 【实验】员工列表: 公共页面抽取

从这边往下因为没有数据库资源和先置的几个数据模板类, 就不能跟着写代码了, 主要记一些要点

  1. thymeleaf公共页面元素抽取:
1. 抽取公共片段: th:fragment
<div th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</div>

2. 引入公共片段: th:insert
<div th:insert="~{footer :: copy}"></div>
~{templatename::selector} : 模板名::选择器
~{templatename::fragmentname} : 模板名::片段名, 这里用的是这个, 把copy片段包含进来

3. 默认效果:
insert的功能片段在div标签中
如果使用th:insert等属性进行引入, 可以不用写~{}
但是行内写法([[]]与[()])一定要写~{}
  • 注意模板名是要按照thymeleaf的命名规则, 比如要把dashboard.html里的某个块拿出来引入, 则模板名应当写成<div th:insert="dashboard::copy"> </div>
  1. 三种引入功能片段的th属性:
<footer th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>

<body>
<div th:insert>"footer :: copy"</div>    # 将整个公共片段插入到声明引入的元素中
<div th:replace>"footer :: copy"</div>   # 将整个公共片段插入到声明引入的元素中并删除原来的元素标签
<div th:include>"footer :: copy"</div>   # 将公共片段的内部HTML插入到声明引入的元素中
</body>

效果:
<div>
    <footer th:fragment="copy">
    &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
</div>

<footer th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>

<div>
&copy; 2011 The Good Thymes Virtual Grocery
</div>
  1. 用途: 将不同页面上的公共元素或者HTML块进行共用, 方便修改也编写

4.12 【实验】员工列表: 链接高亮与列表完成

  1. 参数化的fragment签名:
  • 即引入片段的时候是可以传入参数的
  • 可以考虑把很多公共元素写在一个bar.html文件中以备调用
  • 带参数的引入
    <div th:replace="commons/bar::#sidebar(activeUri='main.html')"></div> <!-- 侧边栏引入 -->
    
  • 用activeUri变量的取值来决定是否高亮链接: <a th:class="${activeUri=='main.html'?'nav-link active':'nav-link'}">
  1. 列表循环th:each:
<tbody>
    <tr th:each="emp:${emps}">
        <td th:text="${emp.id}"></td>
        <td>[[${emp.lastName}]]</td>
        <td>[[${#date.format(emp.birth,'yyyy-MM-dd HH:mm:ss')}]]</td> <!-- 工具化变量#date -->
        <td>[[${emp.gender}==0?'女':'男']]</td>
    </tr>
</tbody>

4.13 【实验】员工添加: 来到添加页面

  1. <a th:href="@{/emp}"></a>: uri是/emp
  2. 控制类中编写:
//来到员工添加页面
@GetMapping("/emp")
public String toAddPage(Model model) {
	//来到添加页面, 查出所有部门, 在页面显示
	Collection<Department> departments = departmentDao.getDepartment();
	model.addAttribute("depts",departments); // 传到thymeleaf模板中的变量depts
	return "emp/add";
}  

4.14 【实验】员工添加: 添加完成

  1. 控制类中编写:
// 员工添加
// SpringMVC自动将请求参数和入参对象的属性进行一一绑定, 要求了请求参数的名字和javabean入参的对象里面的属性名是一样的
@PostMapping("/emp")
public String addEmp(Employee employee) {
	//来到员工列表页面
	
	System.out.println("保存的员工信息: "+employee);
	employeeDao.save(employee);
	// redirect是重定向到一个地址, forward是返回
	//return "redirect: /emps";
	return "forward: /emps";
}
  1. 提交的数据格式不对: 生日(日期)
  • 日期格式化: SpringMVC将页面提交的值需要转换为指定的类型:
  • 默认日期是按照yyyy/MM/dd
  • 通过修改spring.mvc.date-format=yyyy-MM-dd HH:mm:ss来自定义日期格式

4.15 【实验】员工修改: 重用页面与修改完成

  1. <a th:href="@{/emp/}+${emp.id}">

  2. 控制器类中编写:

//来到修改页面, 查出当前员工, 在页面回显
@GetMapping(/emp/{id}")
public String toEditPage(@PathVariable("id") Integer id) {
	Employee employee = employeeDao.get(id);
	model.addAttribute("emp",employee);
	
	//回到修改页面(add是一个修改添加二合一的页面, 但是修改时页面上的表单应该有回显值, 即员工原来的信息)
	return "emp/add";
}
  1. th:checked="${emp.gender==1}": th:checked属性
  2. 下拉式列表中使用th:selected属性
<select>
	<option th:selected="${dept.id==emp.department.id}" th:values="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">
</select>
  1. 在修改时显示原先的员工信息, 添加则不显示: <input type="text" th:values="${emp!=null}?${emp.lastName}">

  2. 在控制器中编写:

//员工修改
@PutMapping("/emp")
public String updateEmployee(Employee employee) {
	System.out.println("保存的员工信息: "+employee);
	employeeDao.save(employee);
	return "redirect: /emps";
}

4.16 【实验】员工删除: 删除完成

  1. th:attr属性:
  • 设置一个标签属性
<form action="subscribe.html" th:attr="action@{/subscribe}">
	<fieldset>
		<input type="text" name="email" />
		<input type="submit" th:attr="value=#{subscribe.submit}" />
	</fieldset>
</form>
  • 设置多个标签属性: <img src="../1.png" th:attr="src=@{/1.png},title=#{logo},alt=#{logo}">

4.17 错误处理原理与定制错误页面

  1. Spring默认的错误处理机制
  • 默认效果:
    • 返回一个默认的错误页面: Whitelabel Error Page xxx
    • 如果是其他客户端访问, 默认响应一个json数据(可以使用客户端模拟器来测试)
  • 原理:
    • 可以参照ErrorMvcAutoConfiguration: 错误处理的自动配置类
    • 给容器中添加了以下组件
      • DefaultErrorAttributes: 在页面共享信息
      • BasicErrorController: 处理默认的/error请求
        • 处理一: 返回HTML响应数据produce="text/html"
        • 处理二: 返回JSON响应数据
      • ErrorPageCustomizer:
        @Value("${error.path/error}")
        private String path = "/error"; //系统出现错误后来到error请求进行处理(类似web.xml注册的错误处理配置)
        
      • DefaultErrorViewResolver
    • 一旦系统出现4xx或者5xx之类的错误时, ErrorPageCustomizer就会生效(定制错误的响应规则), 就会来到"/error"请求, 就会被BasicErrorController处理
    • 响应页面: 去哪个页面由DefaultErrorResolver解析得到的
  1. 如何定制错误响应
  • 如何定制错误的页面
    • 有模板引擎的情况下: 编辑error/状态码, 即在tempplates文件夹里新建error文件夹, 在里面编辑404.html 403.html 503.html等等, 也可以直接编辑4xx.html 5xx.html, 逻辑是有精确匹配error/404.html就去精确匹配的页面, 没有就去4xx.html 5xx.html错误页面
      • 页面能获取的信息DefaultErrorAttributes:
        • ${timestamp}: 时间戳
        • ${status}: 状态码
        • ${error}: 错误提示
        • ${exception}: 异常对象
        • ${message}: 异常消息
        • ${errors}: JSR303数据校验的错误
    • 没有模板引擎(没有/templates文件夹), 就在静态资源文件夹/static下找
    • 以上都没有错误页面, 就用SpringBoot默认的错误提示页面

4.18 定制错误数据

  1. 如何定制错误的json数据
  • 编写UserNotExistException类
    package com.example.demo;
    
    public class UserNotExistException extends RuntimeException{
    	public UserNotExistException() {
    		super("用户不存在!");
    	}
    }
    
  • 在控制器类中添加一个路由
    @RequestMapping(value="/hello",method=RequestMethod.GET)
    public String hello(@RequestParam("user") String user) {
    	if(user.equals("aaa")) {
    		throw new UserNotExistException();
    	}
    	return "hello";
    }
    
    • 此时访问localhost:8080/hello?user=bbb就可以看到用户不存在的异常
  • 如果想要显示json数据的异常响应, 则编写MyExceptionHandler类, 类上加注解@ControllerAdvice, 类中的方法加上注解@ExceptionHandler(UserNotExistException.class)来捕获异常
    package com.example.demo;
    
    import java.util.HashMap;
    import java.util.Map;
    import javax.servlet.http.HttpServletRequest;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.servlet.ModelAndView;  // 模板引擎
    
    /*
     * 1. 新建一个Class, 这里取名为MyExceptionHandler
     * 2. 在class上添加注解, @ControllerAdvice
     * 3. 在class中添加一个方法
     * 4. 在方法上添加@ExceptionHandler(Exception.class)拦截相应的异常信息
     * 5. 如果返回的是View, 方法的返回值是ModelAndView
     * 6. 如果返回的是String或是Json数据, 那么需要在方法上添加@ResponseBody注解
     */
    
    @ControllerAdvice
    public class MyExceptionHandler {
    	
    	@ResponseBody
    	@ExceptionHandler(UserNotExistException.class) // 这里相当于只处理UserNotExistException异常, 也可以写成Exception就可以处理所有异常
    	public Map<String,Object> handleException(Exception e) {
    		Map<String,Object> map = new HashMap<>();
    		map.put("code", "user.notexist");
    		map.put("message",e.getMessage());
    		return map;
    	}
    }
    
    • 这种写法的缺点是浏览器和客户端返回的都将是json数据, 解决方案是转发到/error即可("/error"是SpringBoot)
      // 如果希望区分浏览器和客户端可以考虑将请求转发到/error页面让SpringBoot自动处理
      // 这样的话一定要传入错误状态码才能使用自己在templates/error里写的模板	@ExceptionHandler(UserNotExistException.class) // 这里相当于只处理UserNotExistException异常, 也可以写成Exception就可以处理所有异常
      public String handleException(Exception e, HttpServletRequest request) {
          request.setAttribute("javax.servlet.error.status_code", 400);
          Map<String,Object> map = new HashMap<>();
          map.put("code", "user.notexist");
          map.put("message",e.getMessage());
          return "forward:/error"; // 转发到/error
      }
      
  • 将我们的定制数据携带出去
    • 出现错误以后, 会来到/error请求会被BasicErrorController处理, 相应出去可以获取的数据是由getErrorAttributes得到的
    • 完全来编写一个ErrorController的实现类, 或者是编写AbstractErrorController的子类, 放在容器中
    • 页面中上能用的数据, 或者是json返回能用的数据都使用过errorAttributes.getErrorAttributes得到, 容器中DefaultErrorAttributes.getErrorAttributes()默认进行数据处理的
    • 自定义ErrorAttributes:
      package com.example.demo;
      
      import java.util.Map;
      import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
      import org.springframework.web.context.request.WebRequest;
      
      @Component
      public class MyErrorAttributes extends DefaultErrorAttributes{
      
          @Override
          public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        	  Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        	  map.put("company","SUFE");
        	  return map;
          }
      }
      

4.19 嵌入式Servlet容器配置修改

  1. SpringBoot默认用的是Tomcat作为嵌入式的Servlet容器
  2. 问题:
  • 如何定制和修改Servlet容器的相关配置(如端口等)?
    • 修改和server有关的配置即可, 如:server.port server.context-path server.tomcat.xxx
    • 也可以编写一个EmbeddedServletContainer: 嵌入式的Servlet容器的定制器, 来修改Servlet容器的配置

4.20 注册servlet三大组件

由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器, 不能在web.xml配置文件里注册三大组件

  1. 注册servlet: ServletRegistrationBean
  2. 注册Filter: FilterRegistrationBean
  3. 注册Listener: ServletListenerRegistrationBean

4.21 切换其他嵌入式Servlet容器

  1. SpringBoot支持Jetty和Undertow两种Servlet容器
  • Jetty一般用于HTTP长连接
  • Undertow不支持JSP
  • Tomcat是SpringBoot默认支持的容器
  1. 切换方法: 修改pom.xml配置文件
  • 排除掉tomcat:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusion>
        <artifactId>spring-boot-starter-tomcat</artifactId>
  	  <groupId>org.springframework.boot</groupId>
    </exclusion>
</dependency>
  • 引入其他容器:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

4.22 嵌入式Servlet容器自动配置原理

  1. 详见EmbeddedServletContainerAutoConfiguration类: 嵌入式的Servlet容器自动配置类
  • EmbeddedServletContainerFactory: 嵌入式的Servlet容器工厂
  • EmbeddedServletContainer: 嵌入式的Servlet容器
  • 我们对嵌入式容器的配置修改是怎么生效:
    • ServerProperties, EmbeddedServletContainerCustomer
    • EmbeddedServletContainerCustomizer: 定制器帮我们修改了Servlet容器的配置
    • 容器中导入了: EmbeddedServletContainerCustomizerBeanPostProcessor
  1. 步骤总结:
  • SpringBoot根据导入的以来情况, 给容器中添加响应的EmbeddedServletContainerFactory
    • 以tomcat为例就是TomcatEmbeddedServletContainerFactory
  • 容器中某个组件要创建时对象就会惊动后置处理器EmbeddedServletContainerCustomizerBeanPostProcessor
    • 只要是嵌入式的Servlet容器工厂, 后置处理器就工作
  • 后置处理器, 从容器中获取所有的EmbeddedServletContainerCustomer, 调用定制器的定制方法

4.23 嵌入式Servlet容器自动启动原理

  1. 问题:
  • 什么时候创建嵌入式Servlet容器工厂?
  • 什么时候获取嵌入式的Servlet容器并启动Tomcat?
  1. 获取嵌入式的Servlet容器工厂:
  • SpringBoot应用启动运行run方法
  • refreshContext(context), SpringBoot刷新IOC容器: 创建IOC容器并初始化容器, 创建容器中的每一个组件; 如果是web应用创建AnnotationConfigEmbeddedWebApplicationContext, 否则创建AnnotationConfigApplicationContext
  • refresh(context)刷新刚才创建好的IOC容器
  • onRefresh(): web的IOC容器重写了onRefresh方法
  • webioc容器会创建嵌入式的Servlet容器: createEmbeddedServletContainer()
  • 获取嵌入式的Servlet容器工厂
  • 使用容器工厂获取嵌入式Servlet容器
  • 嵌入式的Servlet容器创建对象并启动Servlet容器
    • 先启动嵌入式的Servlet容器, 再将IOC容器中剩下没有创建出的对象获取出来
    • IOC容器启动创建嵌入式的Servlet容器

4.24 使用外部Servlet容器与JSP支持

  1. 嵌入式的Servlet容器: 应用打包成可执行的jar包
  • 优点: 简单便携
  • 缺点: 默认不支持JSP, 优化定制比较复杂
  1. 外置的Servlet容器: 外面安装Tomcat, 应用war包的方式打包
  • 步骤:
    • 必须创建一个war项目
    • 将嵌入式的Tomcat指定为provided
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-tomcat</artifactId>
          <scope>provided</scope>
      </dependency>
      
    • 必须编写一个SpringBootServletInitializer的实现类(子类), 并调用configure方法
    public class ServletInitializer extends SpringBootServletInitializer {
    	@Override
    	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    		return application.sources(Web1Application.class);
    	}
    }
    
    • 启动服务器就可以使用

4.25 外部Servlet容器启动SpringBoot应用原理

  1. SpringBootServletInitializer: 重写configure
  2. SpringApplicationBuilder: builder.source(@SpringBootApplication)
  3. 启动原理:
  • Servlet3.0标准ServletContainerInitializer赛秒所有jar包中META-INF/services/javax.servlet.ServletContainerInitializer文件指定的类并加载
  • 加载spring web包下的SpringServletContainerInitializer
  • 扫描@HandleType(WebApplicationInitializer)
  • 加载SpringServletContainerInitializer并运行onStartup方法
  • 加载@SpringBootApplication主类, 启动容器

5. Spring Boot与Docker

简介

  1. Docker是一个开源的应用容器引擎, 基于Go原因呢并村从Apache2.0协议开源
  2. Docker可以让开发者打包他们的应用以及依赖包到一个轻量级, 可移植的容器中, 然后发布到任何流行的Linux机器上, 也可以实现虚拟化
  3. 容器是完全使用沙箱机制, 相互之间不会有任何接口, 更重要的是容器性能开销极低
  4. 将已经安装配置好的软件打包成一个镜像, 只要在Docker运行镜像就可以直接配置好

核心概念

  1. docker主机(Host): 安装了docker程序的机器, 一个物理或虚拟的机器用于执行Docker守护进程和容器, API
  2. docker镜像(Images): docker镜像是用于创建docker容器的模板
  3. docker容器(Container): 容器是独立运行的一个或一组应用
  4. docker客户端(Client): 客户端通过命令行或者其他工具使用docker
  5. docker仓库(Repository): docker仓库用来保存镜像, 可以理解为代码控制中的代码仓库, docker提供了庞大的镜像集合供使用
  6. 使用docker的步骤:
  • 安装docker
  • 去docker仓库找到这个软件对应的镜像
  • 使用docker运行这个镜像, 这个镜像就会生成一个docker容器
  • 对容器的启动停止就是对软件的启动停止

linux环境准备

  1. 安装linux虚拟机
  • 可以安装VMWare(重量级), Oracle VM VirtualBox(轻量级)
  • 导入虚拟机文件
  • 双击启动linux虚拟机, 使用账号密码登录
  • 使用客户端连接linux服务器进行命令操作
  • 设置虚拟机网络
    • 桥接网络
    • 选择网卡
    • 接入网线
  • 使用命令重启虚拟机的网络: service network restart
  • 查看linux的ip地址: ip addr

docker安装启动和停止

  1. 在linux虚拟机上安装docker
  • 查看centos版本: docker要求centos系统内核版本高于3.10
    • uname -r
  • 升级软件包及内核(选做): yum update
  • 安装docker: yum install docker
  • 启动docker: start docker
  • 将docker服务设为开机启动: systemctl enable docker

docker镜像操作常用命令

  1. 镜像操作
  • 检索: docker search <关键字>
    • docker search mysql
    • 本质是去dockerhub里搜索
  • 拉取: docker pull <镜像名>
    • docker pull mysql也可以写全名, 默认下载最新的mysql
    • docker pull mysql:5.5下载5.5版本的mysql
  • 列表: docker images查看本地所有docker镜像
  • 删除: docker rmi <镜像id>删除指定的本地镜像

docker容器操作常用命令

  1. 软件镜像(如QQ安装程序)----运行镜像----产生一个容器(正在运行的QQ)
  2. 步骤:
  • 搜索镜像: docker search tomcat
  • 拉取镜像: docker pull tomcat
  • 运行: docker run --name <container-name> -d <image-name>
    • e.g. docker run --name myredis -d redis
    • --name: 自定义容器名
    • -d: 后台运行
  • 列表: docker ps查看运行中的容器
    • -a: 可以查看所有容器
  • 停止: docker stop <container-name>或<container-id>
  • 启动: docker start <container-name>或<container-id>
  • 删除: docker rm <container-id>
  • 端口映射: -p 6379:6379
    • e.g. docker run -d -p 6379:6379 --name myredis docker.io/redis
    • -p: 主机端口号(映射到)容器内部的端口
  • 容器日志: dockers logs <container-name>或<container-id>
  • 更多命令

docker安装MySQL

  1. 后续课程需要安装的软件:
  • 安装mysql: 数据访问
  • 安装redis: 缓存
  • 安装rabbitmq: 消息中间件
  • 安装elasticsearch: 全文检索
  1. 安装MySQL示例:
  • 拉取: docker pull mysql
  • 运行:
    • 错误的启动: docker run --name mysql01 -d mysql
    • 正确的启动: docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -d mysql
    • 做了端口映射的启动: docker run -p 3306:3306 --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -d mysql
  • 其他的高级操作:
    • docker run --name mysql03 -v /my/custom:/erc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 -d mysql
      • 把主机的my/custom文件夹挂在到mysqldocker容器的/etc/mysql/conf.d文件夹里面
      • 改mysql的配置文件就只需要把mysql配置文件放在里面
    • docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql --character-set-server=utf8nb4 --collation-server=utf8mb4_unicode_ci

6. Spring Boot数据访问

简介

  1. 前置技术知识: JBDC, MyBatis, Spring Data JPA
  2. 对于数据访问层, 无论是SQL还是NOSQL, SpringBoot默认采用整合SpringData的方式进行统一处理, 添加大量自动配置, 屏蔽了很多设置, 引入各种xxxTemplate, xxxRepository来简化我们对数据访问层的操作, 对我们来说只需要进行简单的设置即可

JDBC与自动配置原理

  1. 新建一个springboot项目, 选择spring web + mysql + jdbc
  • pom配置文件中的依赖项应该包含以下几个:
    <!-- JDBC依赖 -->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <!-- WEB依赖 -->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- MySQL依赖 -->
    <dependency>
    	<groupId>mysql</groupId>
    	<artifactId>mysql-connector-java</artifactId>
    	<scope>runtime</scope>
    </dependency>
    
  1. 编写application.yml配置文件
spring:
  datasource: 
    username: root
	password: 123456
	url: jdbc:mysql://192.168.15.22:3306/jdbc
	driver-class-name: com.mysql.jdbc.Driver
  • com.mysql.jdbc.Driver已经被弃用了, 现在是com.mysql.cj.jdbc.Driver
  • 效果: 默认使用tomcat.jdbc.pool.DataSource作为数据源
  • 数据源的相关配置都在DataSourceProperties类里
  • 我实际配置的是: 注意url加了个参数serverTimeZone, 我不加会报时区错误
  spring.datasource.username=root
  spring.datasource.password=
  spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC
  spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  ```
- 单元测试文件编写
  ```
  package com.sufe;

  import java.sql.Connection;
  import java.sql.SQLException;

  import javax.sql.DataSource;

  import org.junit.jupiter.api.Test;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.boot.test.context.SpringBootTest;

  @SpringBootTest
  class EcologyApplicationTests {
  	
  	@Autowired
  	DataSource dataSource;
  	
  	@Test
  	void contextLoads() throws SQLException {
  		System.out.println("这里是dataSource的类"+dataSource.getClass());
  		Connection connection = dataSource.getConnection();
  		System.out.println("这里是connection"+connection);
  		connection.close();
  	}
  }
  ```
  + 输出
    * 这里是dataSource的类```class com.zaxxer.hikari.HikariDataSource```
    * 这里是```connectionHikariProxyConnection@1473128600 wrapping com.mysql.cj.jdbc.ConnectionImpl@1ea930eb```

3. 数据源自动配置原理```org.springframework.boot.autoconfigure.jdbc:
+ 参考DataSourceConfiguration, 根据配置创建数据源, 默认使用Tomcat连接池
+ SpringBoot默认可以支持
  * org.apache.tomcat.jdbc.pool.DataSource
  * HikariSource
  * BasicDataSource
  * 也可以是自定义数据源

4. DataSourceInitializer: ApplicationListener
- 作用: 
  * 运行建表语句: ```runSchemaScriptes()```
  * 运行插入数据的sql语句: ```runDataScripts()```
- 默认只需要将文件命名为: ```schema-*.sql```和```data-*.sql```就可以被```DataSourceInitializer```读取
  + 默认规则是```schema.sql```或```schema-all.sql```
  + 文件位置放到classpath即resources文件夹下
  + 可以修改配置使得读取非默认命名的sql文件
    ```
    spring:
      datasource: 
  	  username: root
  	  password: 123456
  	  url: jdbc:mysql://192.168.15.22:3306/jdbc
  	  driver-class-name: com.mysql.jdbc.Driver
  	  schema:
  	    - classpath:department.sql
    ```
  + 这些文件在项目启动时都会执行一次, 所以建表建一次就要把哪些sql文件给删了, 否则以后每次启动都会建表

## 整合Druid与配置数据源监控
1. 去maven repository中搜索druid的依赖:
com.alibaba druid 1.1.24 ``` 2. 配置文件```application.yml```中写入: ``` spring: datasource: username: root password: 123456 url: jdbc:mysql://192.168.15.22:3306/jdbc driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource ```
  1. 此时再去单元测试文件中那个输出dataSource.getClass(), 结果为com.alibaba.druid.pool.DruidDataSource

  2. 数据源配置大全

spring:
  datasource: 
#   数据源基本配置
    username: root
    password: 123456
    url: jdbc:mysql://192.168.15.22:3306/jdbc
    driver-class-name: com.mysql.jdbc.Driver
	type: com.alibaba.druid.pool.DruidDataSource
#   数据源其他配置
    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, 去掉后监控界面sql无法统计, 'wall'用于防火墙
    filters: stat,wall,log4j
	maxPoolPreparedStatementPerConnectionSize: 20
	useGlobalDataSourceStat: true
	connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  1. 自定义数据源配置类:
@Configuration
public class DruidConfig {
	@ConfigurationProperties(prefix="spring.datasource")
	@Bean
	public DataSource druid() {
		return new DruidDataSource();
	}
	
	// 配置Druid的监控
	// 1. 配置一个管理后台的Servlet
	public ServletRegistrationBean statViewServlet() {
		ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*")
		Map<String,String> initParams = new HashMap<>();
		initParams.put("loginUsername","admin");
		initParams.put("loginPassword","123456");
		initParams.put("allow",""); // 默认允许所有登录, 第二个参数可以写成localhost
		initParams.put("deny","192.168.15.21");
		bean.setInitParameters(initParams);
		return bean;
	}
	
	// 2. 配置一个监控的filters
	public FilterRegistrationBean() webStatFilter() {
		FilterRegistrationBean bean = new FilterRegistrationBean();
		bean.setFilter(new WebStatFilter());
		Map<String,String> initParams = new HashMap<>();
		initParams.put("exclusion","*.js,*.css,/druid/*");
		bean.setInitParameters(initParams);
		bean.setUrlPatterns(Arrays.asList("/*"));
		return bean;
	}
}

整合MyBatis: 基础环境搭建

  1. 新建项目的时候将SQL中的mybatis framework添加进来, 相当于引入以下的starter依赖
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
  1. 配置druid数据源的依赖, 见上一节
  2. 编写druid数据源的配置文件application.yml, 见上一节
  3. 编写DruidConfig配置类, 见上一节
  4. 建表sql文件编写, 并将sql文件添加到application.yml配置文件中
spring:
  datasource: 
#   数据源基本配置
    username: root
    password: 123456
    url: jdbc:mysql://192.168.15.22:3306/mybatis
    driver-class-name: com.mysql.jdbc.Driver
	type: com.alibaba.druid.pool.DruidDataSource
#   数据源其他配置
    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, 去掉后监控界面sql无法统计, 'wall'用于防火墙
    filters: stat,wall,log4j
	maxPoolPreparedStatementPerConnectionSize: 20
	useGlobalDataSourceStat: true
	connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
	schema: 
	  - classpath: sql/department.sql
	  - classpath: sql/employee.sql
  1. 运行项目执行建表sql, 则在mysql的mybatis数据库中会新建department与employee表
  2. 创建department与employee的数据模型javabean类

整合MyBatis: 注解版MyBatis

  1. 架构步骤:
  • 创建User的javabean:
    package com.sufe.model;
    
    public class User {
    	private int userId;
    	private String username;
    	private String password;
    	private String email;
    	private boolean isProfessional;
    	private boolean isAdministrator;
    	
    	public int getUserId() {
    		return userId;
    	}
    	public void setUserId(int userId) {
    		this.userId = userId;
    	}
    	public String getUsername() {
    		return username;
    	}
    	public void setUsername(String username) {
    		this.username = username;
    	}
    	public String getPassword() {
    		return password;
    	}
    	public void setPassword(String password) {
    		this.password = password;
    	}
    	public String getEmail() {
    		return email;
    	}
    	public void setEmail(String email) {
    		this.email = email;
    	}
    	public boolean isProfessional() {
    		return isProfessional;
    	}
    	public void setProfessional(boolean isProfessional) {
    		this.isProfessional = isProfessional;
    	}
    	public boolean isAdministrator() {
    		return isAdministrator;
    	}
    	public void setAdministrator(boolean isAdministrator) {
    		this.isAdministrator = isAdministrator;
    	}
    }
    
    
  • 创建一个mapper的interface:
    package com.sufe.mapper;
    
    import org.apache.ibatis.annotations.Delete;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Options;
    import org.apache.ibatis.annotations.Select;
    import org.apache.ibatis.annotations.Update;
    
    import com.sufe.model.User;
    
    @Mapper
    public interface UserMapper {
    	
    	@Select("select * from user where userId=#{userId};")
    	public User getUserById(int userId);
    	
    	@Delete("delete from user where userId=#{userId}")
    	public int deleteUserById(int userId);
    	
    	//@Insert("insert into user values(#{userId},#{username},#{password},#{email},#{isProfessional},#{isAdministrator})")
    	@Options(useGeneratedKeys=true,keyProperty="userId") // 是否使用自动生成的主键
    	@Insert("insert into user(username) values(#{username})")
    	public int insertUser(User user);
    	
    	@Update("update user set userId=#{userId} username=#{username} password=#{password} email=#{email} isProfressional=#{isProfessional} isAdministrator=#{isAdministrator} where userId=#{userId}")
    	public int updateDept(User user);
    }
    
  • 创建对应的控制器controller
    package com.sufe.controller;
    
    import org.apache.ibatis.annotations.Options;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import com.sufe.model.User;
    import com.sufe.mapper.UserMapper;
    
    @Controller
    public class UserController {
    	
    	@Autowired
    	UserMapper usermapper;
    	
    	@ResponseBody // 不加会报错, 加上才能返回json数据
    	@GetMapping("/userquery/{userId}")
    	public User getUser(@PathVariable("userId") int userId) {
    		return usermapper.getUserById(userId);
    	}
    	
    	@ResponseBody // 不加会报错, 加上才能返回json数据
    	@GetMapping("/userinsert")
    	public User insertUser(User user) {
    		usermapper.insertUser(user);
    		return user;
    	}
    }
    
  • 访问localhost:8080/userquerylocalhost:8080/userinsert?username=caoyang实现查询与插入, 删除与更新同理
  1. 问题点记录:
  • 建表时一定注意把主键字段设为自增, 否则会插入user会报错duplicated primary key
    create table user (
    	userId int(64) not null AUTO_INCREMENT,
    	username varchar(32),
    	password varchar(32),
    	email varchar(256),
    	isProfessional bit,
    	isAdministrator bit,
    	primary key(userId)
    );
    
  • 方法体上一定添加@ResponseBody注解, 否则可以成功插入, 但无法正常返回页面, 查询也无法正常查询
  • 数据库里的字段名与javabean尽量相同, 比如都用驼峰命名法, 如果数据库中用的是下划线命名的话, 可以写个配置类来解决:
    package com.sufe.config;
    
    import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class MyBatisConfig {
    	
    	@Bean
    	public ConfigurationCustomizer configurationCustomizer() {
    		return new ConfigurationCustomizer() {
    			@Override
    			public void customize(org.apache.ibatis.session.Configuration configuration) {
    				configuration.setMapUnderscoreToCamelCase(true);
    			}
    		};
    	}
    }
    
  • 可以在主启动类上使用@Mapperscan(value="com.sufe.mapper")注解来批量扫描所有的Mapper

7. Spring Boot启动配置原理

为了更深入的了解SpringBoot, 学习SpringBoot的启动原理

第一步: 创建SpringApplication对象

第二步: 启动应用

  1. 运行run方法

事件监听机制相关测试

  1. 几个重要的事件回调机制:
  • 配置在META-INF/spring.factories
    • ApplicationContextInitializer
    package com.sufe.listener;
    
    import org.springframework.context.ApplicationContextInitializer;
    import org.springframework.context.ConfigurableApplicationContext;
    
    public class EcologyContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>{
    	@Override
    	public void initialize(ConfigurableApplicationContext applicationContext) {
    		System.out.println("ApplicationContextInitializer...initializer..."+applicationContext);
    	}
    
    
    • SpringApplicationRunListener
    package com.sufe.listener;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.SpringApplicationRunListener;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.core.env.ConfigurableEnvironment;
    
    public class EcologySpringApplicationRunListener implements SpringApplicationRunListener{
    	
    	public EcologySpringApplicationRunListener(SpringApplication application, String[] args) {
    		
    	}
    	
    	@Override
    	public void starting() {
    		// TODO Auto-generated method stub
    		SpringApplicationRunListener.super.starting();
    		System.out.println("SpringApplicationRunListener...starting...");
    	}
    
    	@Override
    	public void environmentPrepared(ConfigurableEnvironment environment) {
    		// TODO Auto-generated method stub
    		SpringApplicationRunListener.super.environmentPrepared(environment);
    		Object o = environment.getSystemEnvironment().get("os.name");
    		System.out.println("SpringApplicationRunListener...environmentPrepared..."+o);
    	}
    
    	@Override
    	public void contextPrepared(ConfigurableApplicationContext context) {
    		// TODO Auto-generated method stub
    		SpringApplicationRunListener.super.contextPrepared(context);
    		System.out.println("SpringApplicationRunListener...contextPrepared...");
    	}
    
    	@Override
    	public void contextLoaded(ConfigurableApplicationContext context) {
    		// TODO Auto-generated method stub
    		SpringApplicationRunListener.super.contextLoaded(context);
    		System.out.println("SpringApplicationRunListener...loaded...");
    	}
    
    	@Override
    	public void started(ConfigurableApplicationContext context) {
    		// TODO Auto-generated method stub
    		SpringApplicationRunListener.super.started(context);
    		System.out.println("SpringApplicationRunListener...started...");
    	}
    
    	@Override
    	public void running(ConfigurableApplicationContext context) {
    		// TODO Auto-generated method stub
    		SpringApplicationRunListener.super.running(context);
    		System.out.println("SpringApplicationRunListener...running...");
    	}
    
    	@Override
    	public void failed(ConfigurableApplicationContext context, Throwable exception) {
    		// TODO Auto-generated method stub
    		SpringApplicationRunListener.super.failed(context, exception);
    		System.out.println("SpringApplicationRunListener...failed...");
    	}
    
    }
    	
    
  • 只需要放在IOC容器中
    • ApplicationRunner
    package com.sufe.listener;
    
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.stereotype.Component;
    
    @Component
    public class EcologyApplicationRunner implements ApplicationRunner{
    
    	@Override
    	public void run(ApplicationArguments args) throws Exception {
    		// TODO Auto-generated method stub
    		System.out.println("ApplicationRunner...run...");
    	}
    
    }
    
    
    • CommandLineRunner
    package com.sufe.listener;
    
    import java.util.Arrays;
    
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;
    
    @Component
    public class EcologyCommandLineRunner implements CommandLineRunner{
    
    	@Override
    	public void run(String... args) throws Exception {
    		// TODO Auto-generated method stub
    		System.out.println("CommandLineRunner...run..."+Arrays.asList(args));
    	}
    
    }
    
  1. 最后在src/main/resources/META-INF/spring.factories文件里写入配置
org.springframework.context.ApplicationContextInitializer=com.sufe.listener.EcologyContextInitializer
org.springframework.context.SpringApplicationRunListener=com.sufe.listener.EcologySpringApplicationRunListener

8. Spring Boot自定义starters

  1. starter编写思路
  • 这个场景需要使用到的依赖是什么
  • 如何编写自动配置
    @Configuration
    @ContionalOnXXX
    @AutoConfigureAfter // 指定配置类的顺序
    @Bean // 给容器中添加组件
    
    @ConfigurationProperties结合相关的XXXProperties类来绑定相关的配置
    @EnableConfigurationProperties // 将XXXProperties生效加入到容器中
    
    自动配类要能加载, 将需要启动加载的自动配置类, 配置在META-INF/spring.factories
    
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=...
    
  • 模式:
    • 启动器(starter)
      • 启动器模块是一个空JAR文件, 仅是提供辅助性依赖管理, 这些依赖可能用于自动装配或者其他类库
      • 命名规约:
        • 推荐使用以下命名规约
          • 官方命名空间: spring-boot-starter-模块名
          • 自定义命名空间: 模块名-spring-boot-starter
  1. 实际操作:
  • 新建一个maven工程, 定义groupId和artifactId
  • 事实上maven工程里只需要配置pom.xml, 以便于给其他springboot项目的pom.xml来引入依赖(以groupId,artifactId)

SpringBoot基础阶段小结

至此前八章为基础内容, 后八章为高级应用, 本质仍是基于前八章
https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples有包括amap, aop, cache等样例代码, 在后面的章节中会有所提及


9. Spring Boot缓存

JSR-107简介

  1. JSR-107缓存规范
  • Java Caching定义了5个核心接口, 分别是CachingProvider, CacheManager, Cache, Entry, Expiry
    • CachingProvider定义了创建, 配置, 获取, 管理和控制多个CacheManager, 一个应用可以在运行期访问多个CachingProvider
    • CacheManager定义了创建, 配置, 获取, 管理和控制多个唯一命名的Cache, 这些Cache存在于CacheManager的上下文中, 一个CacheManager仅被一个CachingProvider所拥有
    • Cache是一个类似Map数据结构并临时存储以Key为索引的值, 一个Cache仅被一个CacheManager所拥有
    • Entry指一个存储在缓存中的键值对
    • Expiry每一个存储在缓存中的条目都有一个定义的有效期, 一旦超过这个时间, 条目为过期状态, 将不可访问更新和删除, 缓存有效期可以通过ExpiryPolicy设置

Spring缓存抽象简介

Spring从3.1开始定义了org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口来统一不同的缓存技术, 并支持使用JCache(JSR-107)注解来简化开发
Cache接口为缓存的组件规范定义, 包含缓存的各种操作集合
Cache接口下Spring提供了各种XXXCache的实现, 如RedisCache, EhCacheCache, ConcurrentMapCache
每次调用需要缓存功能的方法时, Spring会检查指定参数的指定目标方法是否已经被调用过, 如果有就直接从缓存中获取方法调用后的结果, 如果没有就调用方法并缓存结果后返回给用户, 下次调用直接从缓存中读取
使用Spring缓存抽象时注意两点: 确定方法需要被缓存以及它们的缓存策略, 如何从缓存中读取之前缓存存储的数据

  1. 几个重要概念和缓存注解
  • Cache: 缓存接口, 定义缓存操作
  • CacheManager: 缓存管理器, 管理各种缓存Cache组件
  • @Cacheable: 主要针对方法配置, 能够根据方法的请求参数对其结果进行缓存
  • @CacheEvict: 清空缓存
  • @CachePut: 保证方法被调用, 又希望结果被缓存, 用于缓存更新
  • @EnableCaching: 开启基于注解的缓存
  • keyGenerator: 缓存数据时key生成策略
  • serialize: 缓存数据时value序列化策略

基本环境搭建

  1. 基本就是MyBatis整合内容的回顾, 其中提到关于驼峰命名的mybatis配置可以写在application.properties文件中: mybatis.configuration.map-underscore-to-camel-case=true

@Cacheable初体验

  1. 快速体验缓存步骤:
  • 开启基于注解的缓存: 主启动类上添加@EnableCaching注解
    package com.sufe;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cache.annotation.EnableCaching;
    
    @MapperScan("com.sufe.mapper")
    @SpringBootApplication
    @EnableCaching
    public class EcologyApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(EcologyApplication.class, args);
    	}
    
    }
    
  • 标注缓存注解: @Cacheable, @CachePut, @CacheEvict
  1. @Cacheable的几个属性:
  • cacheNames/value: 指定缓存组件的名字

    • 将方法的返回结果放在哪个缓存中, 可以是字符串, 也可以是字符串数组, 可以指定多个缓存
  • key: 缓存数据使用的key, 可以使用它来指定, 默认是方法参数的值

    • 自定义key, 可以写SpEL表达式: key="#root.methodName+'['+#id+']'"
  • keyGenerator: key的生成器, 可以自己指定key的生成器, 与key两个属性选一使用即可

    • 自己编写keyGenerator, 作为一个配置类, 然后配置写成keyGenerator="myGenerator"
    @Configuration
    public class MyCacheConfig {
    
    	@Bean("myKeyGenerator")
    	public KeyGenerator keyGenerator() {
    		return new KeyGenerator() {
    			
    			@Override
    			public Object generate(Object target, Method method, Object... params) {
    				return method.getName() + "[" + Arrays.aslist(params).toString() + "]";
    			}
    		};
    	}
    }
    
  • cacheManager: 指定缓存管理器, 或者cacheResolver指定获取缓存解析器, 与cacheManager属性二选一

  • condition: 指定符合条件的情况下才缓存: 如condition="#id>0"

    • 举例: condition="#a0>1, 表示第一个参数值大于1时才进行缓存
  • unless: 否定缓存, 当unless指定的条件为true则不使用缓存, 与condition恰恰相反, 但unless可以获取到结果进行判断, 如unless="#result==null"

    • 举例: unless="#a0==2"
  • sync: 是否使用异步模式

  1. Cache SpEL available metadata: key属性的值
  • methodName: 当前被调用的方法名, 如#root.methodName
  • method: 当前被调用的方法, 如#root.method.name
  • target: 当前被调用的目标对象, 如#root.target
  • targetClass: 当前被调用的目标对象类, 如#root.targetClass
  • args: 当前被调用的方法的参数列表, 如#root.args[0]
  • caches: 当前方法调用使用的缓存列表(如@Cacheable(value={"cache1","cache2"})), 则有两个cache, 如#root.caches[0].name
  • argument name: 方法参数的名字, 可以直接用#参数名, 也可以用#p0, #a0的形式, 其中0表示参数的索引
  • result: 方法执行后的返回值(仅当方法执行之后的判断有效, 如’unless’, 'cache put’的表达式, 'cache evict’的表达式beforeInvocation=false), 示例: #result
  • 一个简单的缓存例子:
    package com.sufe.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    import com.sufe.mapper.UserMapper;
    import com.sufe.model.User;
    
    @Service
    public class UserService {
    	
    	@Autowired
    	UserMapper userMapper;
    	
    	@Cacheable(cacheNames={"user"})
    	public User getUser(int userId) {
    		System.out.println("查询"+userId+"号员工...");
    		User user = userMapper.getUserById(userId);
    		return user;
    	}
    }
    
    

缓存工作原理与@Cacheable运行流程

  1. 自动配置类: CacheAutoConfiguration
  2. 缓存的配置类: org.springframework.boot.autoconfigure.cache.XXXConfiguration, 如Generic, JCache, EhCache, Hazel, Infinispan, Couchbase, Redis等
  3. 默认生效的配置类: SimpleCacheConfiguration
  4. 给容器中注册了一个CacheManager: ConcurrentMapCacheManager
  5. 可以获取和创建ConcurrentMapCache类型的缓存组件, 它的作用是将数据保存在ConcurrentMap中
  6. @Cacheable运行流程
  • 方法运行之前, 先驱查询Cache(缓存组件), 按照cacheNames指定的名字获取(CacheManager先获取相应的缓存), 第一次获取缓存如果没有Cache组件会自动创建
  • 去Cache中查找缓存的内容, 使用一个key, 默认就是方法的参数
    • key是按照某种策略生成, 默认是使用keyGenerator生成, 默认使用SimpleKeyGenerator生成key,
    • SimpleKeyGenerator生成key的默认参数:
      • 若没有参数: key = new SimpleKey()
      • 若有一个参数: key = 参数值
      • 若有多个参数: key = new SimpleKey()
  • 没有查到缓存就调用目标方法
  • 将目标方法返回的结果放进缓存中
  1. @Cacheable标注的方法执行之前先来检查缓存中有没有这个数据, 默认按照参数的值作为key去查询缓存, 如果没有就运行方法并将结果放入缓存, 以后再来调用就可以直接使用缓存中的数据

  2. 核心:

  • 使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件
  • ke使用keyGenerator生成的, 默认是SimpleKeyGenerator

@Cacheable其他属性

  1. 详解:
  • cacheNames/value: 指定缓存组件的名字

    • 将方法的返回结果放在哪个缓存中, 可以是字符串, 也可以是字符串数组, 可以指定多个缓存
  • key: 缓存数据使用的key, 可以使用它来指定, 默认是方法参数的值

    • 自定义key, 可以写SpEL表达式: key="#root.methodName+'['+#id+']'"
  • keyGenerator: key的生成器, 可以自己指定key的生成器, 与key两个属性选一使用即可

    • 自己编写keyGenerator, 作为一个配置类, 然后配置写成keyGenerator="myGenerator"
    @Configuration
    public class MyCacheConfig {
    
    	@Bean("myKeyGenerator")
    	public KeyGenerator keyGenerator() {
    		return new KeyGenerator() {
    			
    			@Override
    			public Object generate(Object target, Method method, Object... params) {
    				return method.getName() + "[" + Arrays.aslist(params).toString() + "]";
    			}
    		};
    	}
    }
    
  • cacheManager: 指定缓存管理器, 或者cacheResolver指定获取缓存解析器, 与cacheManager属性二选一

  • condition: 指定符合条件的情况下才缓存: 如condition="#id>0"

    • 举例: condition="#a0>1, 表示第一个参数值大于1时才进行缓存
  • unless: 否定缓存, 当unless指定的条件为true则不使用缓存, 与condition恰恰相反, 但unless可以获取到结果进行判断, 如unless="#result==null"

    • 举例: unless="#a0==2"
  • sync: 是否使用异步模式

@CachePut

  1. @CachePut: 既调用方法, 又更新缓存数据
  • 场景: 更新了数据库的某个数据, 同时更新缓存, 一般用于updateUser函数

  • 运行时机:

    • 先调用目标方法
    • 将目标方法的结果缓存起来
  • 测试步骤CachePut:

    • 查询1号用户, 查到的结果会放在缓存中
    • 以后查询还是之前的结果
    • 更新1号用户的一些字段的数据, 将方法的返回值也放进了内存, 但是key与之前的Cacheable是不同的
    • 再查询1号用户, 结果是更新前的结果(失败)
  • 解决方案: @CachePut(value="user",key="#result.userId"), 指定缓存的key为更新前的key, 相当于更新缓存, 这要求@Cacheable的key是默认值, 即为@Cacheable(value="user")

    • key="#user.userId"使用的是更新函数传入参数的userId
    • key="#result.userId"使用的是返回后的userId
    • 注意@Cacheable是不能使用#result

@CacheEvict

  1. @CacheEvict: 删除缓存
  • 场景: sql删除数据

  • 用法: @CacheEvict(value="user",key="#userId")

    • key为指定要清除的缓存的key
    • 设置allEntries=true即指定清楚这个缓存中的所有数据
    • 设置beforeInvocation=false, 缓存的删除是否在方法执行之前执行, 默认是false即之后执行, 区别在于方法万一出现异常, false是不会清除缓存的
  • 到目前为止, 以User表的服务类为例对几个缓存注解的使用

    package com.sufe.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.stereotype.Service;
    
    import com.sufe.mapper.UserMapper;
    import com.sufe.model.User;
    
    @Service
    public class UserService {
    	
    	@Autowired
    	UserMapper userMapper;
    	
    	@Cacheable(value="user")
    	public User getUser(int userId) {
    		System.out.println("查询"+userId+"号用户...");
    		User user = userMapper.getUserById(userId);
    		return user;
    	}
    	
    	@CachePut(value="user",key="#result.userId")
    	public User updateUser(User user) {
    		System.out.println("更新"+user);
    		userMapper.updateUser(user);
    		return user;
    	}
    	
    	@CacheEvict(value="user",key="#userId")
    	public void deleteUser(int userId) {
    		System.out.println("删除"+userId+"号用户...");
    		userMapper.deleteUserById(userId);
    	}
    }
    

@Caching与@CacheConfig

  1. Caching注解: 查看源码知是Cacheable, CachePut, CachEvict的组合注解
  • 场景: 复杂缓存规则的函数, 如以username来查询用户
    @Caching(
    	cacheable = {
    		@Cacheable(value="user",key="#username")
    	},
    	put= {
    		@CachePut(value="user",key="#result.userId")
    		@CachePut(value="user",key="#result.email")
    	}
    )
    public User getUserByUsername(String username) {
    	return usermapper.getUserByUsername(username);
    }
    
    • 此时我们用userId去查询就可以用缓存, 但是用username去查询仍然会查询数据库, 因为CachePut注解要求一定执行方法, 虽然Cacheable做了缓存

搭建redis环境与测试

  1. 问题: 默认使用的是ConcurrentMapCacheManager==ConcurrentMapCache, 将数据保存在ConcurrentMap<Object,Object>, 实际开发中使用缓存中间件, 如redis, memcached, ehcache

  2. 整合redis进行缓存

  • Redis是一个开源(BSD许可)的, 内存中的数据结构存储系统, 可以用作数据库, 缓存和消息中间件
  • 安装redis: 使用docker安装即可
    • 下载redis镜像: 搜索hub.docker.com
      • docker pull registry.docker-cn.com/library/redis: 注意使用docker中国镜像, 要快得多
      • docker images: 查看安装情况
      • docker run -d -p 6379:6379 --name myredis registry.docker-cn.com/library/redis: docker默认端口6379, 将它映射到容器的6379
      • docker ps: 查看容器运行情况
    • 打开redis后连接它
      • name: redis
      • host: 118.24.44.169
      • port: 6379
      • auth: 可以不设密码
  • 常用redis指令: www.redis.cn/commands.html
    • APPEND key value: 给key追加值
    • BITCOUNT key [start end]: 统计字符串起始位置的字节数

Redis Template与序列化机制

  1. 引入redis的starter: 查询官网
  • 官网链接
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  1. 配置redis: 去application.properties中编写spring.redis.host=118.24.44.169

  2. Redis Template的使用

  • StringRedisTemplates: 操作键值对都是字符串的
  • RedisTemplate: 操作键值对都是对象的
  • Redis常见的五大数据类型: String, List, Set, Hash, ZSet(有序集合)
  • 简单测试:
    @Autowired
    StringRedisTemplates stringRedisTemplates
    @Autowired
    RedisTemplate redisTemplate
    
    @Test 
    public void test01() {
    	stringRedisTemplates.opsForValue().append("msg","hello"); // 给msg键追加hello, 相当于redis命令```APPEND key value```
    	stringRedisTemplates.opsForValue().get("msg"); // 获取键值
    	
    	stringRedisTemplates.opsForList().leftPush("myList","1"); // 添加1
    	stringRedisTemplates.opsForList().leftPush("myList","2"); // 添加2
    	stringRedisTemplates.opsForList().leftPop("myList"); // 获取数据
    	
    	//stringRedisTemplates.opsForSet();
    	//stringRedisTemplates.opsForHash();
    	//stringRedisTemplates.opsForZSet();
    	
    	User user = usermapper.getUserById(1);
    	redisTemplate.opsForValue.set("user",user); // 会报错, 报出未序列化的错误
    	/*
    	 * 默认如果保存对象, 使用jdk序列化机制, 序列化后的数据保存到redis中
    	 * 解决方案: User类改写成 public User implements Serializer {...}
    	 * 1. 将数据以json的方式保存
    	 * (1) 自己将对象转为json保存
    	 * (2) redis有自己的序列化规则
    	 */
    }
    
  • 自己写一个redis配置类来自定义序列化规则: 好像有点复杂
    @Configuration
    public class MyRedisConfig {
    	@Bean
    	public RedisTemplate<Object, User> userTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    		RedisTemplate<Object,User> template = new RedisTemplate<Object,User>();
    		template.setConnectionFactory(redisConnectionFactory);
    		Jackson2JsonRedisSerializer<User> serializer = new Jackson2JsonRedisSerializer<User>();
    		template.setDefaultSerializer(serializer);
    		return template;
    	}
    }
    
    • 然后直接用userTemplate来操作带有User类的键值映射即可
      s

自定义CacheManager

  1. 测试缓存:
  • 原理: CacheManager==Cache, 缓存组件来实际给缓存中存取数据
  • 引入redis依赖后就默认自动启动redis的cache配置, 容器中保存的是RedisCacheManager
  • RedisCacheManager创建RedisCache来作为缓存组件, RedisCache通过操作redis来缓存数据
  • 默认保存数据的键值对都是object, 利用序列化保存
  • 如何保存为json?
    • 引入redis的starter, cachemanager变成RedisCacheManager
    • 默认创建的RedisCacheManager操作redis时使用的是RedisTemplate<Object,Object>
    • RedisTemplate<Object,Object>默认使用的是jdk的序列化机制, 会变成unicode编码后的字符串
    • 自定义CacheManager来定义序列化机制, 希望变成json
      @Configuration
      public class MyRedisConfig {
          @Bean
          public RedisTemplate<Object, User> userTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        	  RedisTemplate<Object,User> template = new  RedisTemplate<Object,User>();
        	  template.setConnectionFactory(redisConnectionFactory);
        	  Jackson2JsonRedisSerializer<User> serializer = new Jackson2JsonRedisSerializer<User>();
        	  template.setDefaultSerializer(serializer);
        	  return template;
          }
          
          @Bean
          public RedisCacheManager UserCacheManager(RedisTemplate<Object,User> userRedisTemplate) {
              RedisCacheManager cacheManager = new RedisCacheManager(userRedisTemplate);
        	  cacheManager.setUsePrefix(true); // key多了一个前缀, 使用前缀, 默认会将CacheName作为key的前缀
          }
      }
      

10. Spring Boot消息

JMS与AMQP简介

  1. 概述
  • 大多数应用中, 可通过消息服务中间件来提升系统异步通信, 扩展解耦能力
  • 消息服务中两个重要概念:
    • 消息代理: message broker
    • 目的地: destination
  • 当消息放松这发送消息后, 将由消息代理接管, 消息代理保证消息传递到指定目的地
  • 消息队列主要有两种形式的目的地:
    • 队列: 点对点消息通信
    • 主题: 发布publish或订阅subscribe信息通信
  1. 场景:
  • 异步处理: 提升应用的运行速度
  • 应用耦合: 通过消息队列传递数据来解耦两个应用
  • 流量削峰: 秒杀场景, 将秒杀商品的请求存入消息队列, 堆满则拒绝
  1. 点对点式:
  • 消息发送者发送消息, 消息代理将其放入一个队列中, 消息接收者从队列中获取消息内容, 消息读取后被移出队列
  • 消息只有唯一的发送者和接受者, 但不是说只能有一个接收者
  1. 发布订阅式:
  • 发送者发送消息到主题, 多个接收者监听这个主题, 那么就会在消息到达同时收到消息
  1. JMS: Java消息服务
  • 基于JVM消息代理的规范, ActiveMQ, HornetMQ是JMS实现
  1. AMQP:
  • 高级消息队列协议, 也是一个消息代理的规范, 兼容JMS
  • RabbitMQ是AMQP的实现
  1. JMS对比AMQP:
  • JMS是一个java api, AMQP是网络线级协议
  • JMS不跨平台与语言, AMQP是跨平台和跨语言的
  • JMS提供两种消息模型Peer-2-Peerpub/sub; AMQP提供物种消息模型direct,fanout,topic,headers,system, 本质上来讲后四种与JMS的pub/sub区别不大, 仅是在路由机制上做了更详细的划分
  • JMS支持多种消息类型: TextMessage, MapMessage, BytesMessage, StreamMessage, ObjectMessage, Message; AMQP仅提供byte, 当实际应用有复杂消息, 将消息序列化后发送
  • JMS适合在Java体系中多个客户端交互, 无需修改代码; AMQP定义了wire-level层的协议标准, 天然跨平台跨语言
  1. Spring支持:
  • spring-jms提供对JMS支持
  • spring-rabbit提供对AMQP支持
  • 需要ConnectionFactory的实现来连接消息代理
  • 提供JmsTemplate, RabbitTemplate来发送消息
  • @JmsListener, @RabbitListener注解在方法上监听消息代理发布的消息
  • @EnableJms, @EnableRabbit开启支持
  1. SpringBoot自动配置:
  • JmsAutoConfiguration
  • RabbitAutoConfiguration
  • 导入相关场景依赖即可

RabbitMQ基本概念简介

  1. Rabbit是AMQP的开源实现
  2. 核心概念:
  • Message: 消息
  • Publisher: 消息生产者
  • Exchange: 交换器, 类似路由器的功效
  • Queue: 消息队列
  • Binding: 绑定, 消息队列和交换器之间的关联
  • Connection: 网络连接, 比如一个TCP连接
  • Channel: 信道, 多路复用连接中的一条独立的双向数据通道, 信道是建立在真实的TCP连接中的虚拟连接, 为了节省资源
  • Consumer: 消息消费者
  • Virtual: 虚拟主机, 表示一批交换器, 消息队列和相关对象
  • Broker: 表示消息队列服务器实体
  1. 流程图:
  • Publisher–>Broker(Virtual Host(Exchange–>(Binding)–>Queue))–>Connection(Channel1,Channel2,…,Channeln)–>Consumer

RabbitMQ运行机制

  1. AMQP相对于JMS增加了Exchange和Binding, 生产者把消息发布在Exchange上, 消息最终到达队列并被消费者接收, 而Binding决定交换器的消息应该绑定到哪个消息队列上
  2. Exchange类型: direct, fanout(广播模式, 转发消息最快), topic(进行模式匹配, 支持通配符#与*, 前者匹配0到多个单词, 后者匹配一个单词, 注意是单词级别的), heaers(几乎不用了)

RabbitMQ安装测试

  1. 引入依赖spring-boot-starter-amqp
  • 需要用docker下载镜像rabbitmq: docker pull registry.docker-cn.com/library/rabbitmq:3-management, 这是一个带管理界面的rabbitmq
  • docker images查看下载情况
  • docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq <imageId>, imageId从docker images里查看
  • 访问localhost:15672查看rabbitmq的管理界面
  • 在管理界面具体配置详见教程视频, 比较复杂

RabbitTemplate发送接收消息与序列化机制

  1. 新建项目, sts4引入Message中的Spring for RabbitMQ, idea引入Integration中的RabbitMQ
  • 可以看到pom.xml中引入了场景依赖
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-messaging</artifactId>
    </dependency>	
    <dependency>
    	<groupId>org.springframework.amqp</groupId>
    	<artifactId>spring-rabbit</artifactId>
    </dependency>
    
  1. 配置application.properties
spring.rabbitmq.host=118.24.44.169
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
#spring.rabbitmq.virtual-hosts
  1. 测试rabbitmq
  • 在测试模块中写入
    @Autowired RabbitTemplate rabbitTemplate
    
    /*
     * 单播(点对点)
     */
    @Test
    public void contextLoads() {
    	//Message需要自己构造一个, 定义消息体内容和消息头
    	//rabbitTemplate.send(exchange,routeKey,message);
    	
    	//object默认当成消息体, 只需要传入要发送的对象, 自动序列化发送给rabbitmq
    	//rabbitTemplate.convertAndSend(exchange,routeKey,message);
    	
    	Map<String,Object> map = new HashMap<>();
    	map.put("msg","这是第一个消息");
    	map.put("data",Array.asList("helloworld",123,true));
    	rabbitTemplate.converAndSend("exchange.direct","sufe.news",map); // 发送消息到sufe.news队列
    }
    
    @Test
    public void receive() {
    	Object o = rabbitTemplate.receiveAndConvert("sufe.news"); // 
    	System.out.prinln(o.getClass());
    	System.out.prinln(o);
    }
    
  • 如果想将消息转为json需要编写配置类:
    @Configuration
    public class MyAMQPConfig {
    	
    	@Bean
    	public MessageConverter messageConverter() {
    		return new Jackson2JsonMessageConverter();
    	}
    }
    

@RabbitListener与@EnableRabbit

  1. 监听消息队列
  • 在主程序类上添加@EnableRabbit开启监听
  • 编写一个监听服务类
    @Service
    public class BookService {
    	
    	@RabbitListener(queues="sufe.news")
    	public void receive(Book book) {
    		System.out.println("收到消息"+book);
    	}
    	
    	@RabbitListener(queues="sufe")
    	public void receive1(Message message) {
    		System.out.println(message.getBody());
    		System.out.println(message.getMessageProperties());
    	}
    }
    

AmqpAdmin管理组件的使用

以上的操作都基于已经在rabbitmq的管理界面创建好了各种exchange
如果没有可以用AmqpAdmin来创建

  1. AmqpAdmin用于创建和删除Queue, Exchange, Binding
@Autowired
AmqpAdmin amqpAdmin;

public void createExchange() {
	amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
	System.out.println("创建完成");
}

11. Spring Boot检索

Elasticsearch简介与安装

  1. Elasticsearch是目前全文搜索引擎的首选
  2. Elasticsearch是一个分布式的搜索服务, 提供Restful API, 底层基于Lucene
  3. wiki, github都是用Elasticsearch来实现全文搜索的
  4. 安装: 使用docker下载

Elasticsearch快速入门

官方文档

SpringBoot整合Jest操作Elasticsearch

  1. 引入spring-boot-starter-data-elasticsearch
  2. 安装SpringData对应版本的ElasticSearch
  3. 配置application.properties
  4. 测试Elasticsearch

这个ElasticSearch可能有些过时了, 以后用到再仔细学吧


12. Spring Boot任务

异步任务

  1. 编写service, 三秒后响应
@Service
public class AsyncService {

	@Async // 告诉Spring这是一个异步方法
	public void hello() {
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("处理数据中...");
	}
}
  1. 编写controller
public class AsyncController {
	@Autowired
	AsyncService asyncService;
	
	@GetMapping("/hello")
	public String hello() {
		asyncService.hello();
		return "success";
	}
}
  1. 主程序类上添加@EnableAysnc注解开启异步

  2. 结果访问hello页面会立即响应

定时任务

  1. 两个注解: 主程序类上添加@EnableScheduling, 具体方法上添加@Scheduled
  2. 编写service:
@Service
public class ScheduledService {
	
	// 定时任务与linux的定时任务语法类似: 秒 分 时 日 月 周几
	// 如 0 * * * * MON-FRI 周一到周五每分钟运行一次
	// 0 0/5 14,18 * * ? 每天14点整与18点整, 每个五分钟执行一次
	// 0 15 10 ? * 1-6 每个月的周一至周六10:15分执行一次
	// 0 0 2 ? * 6L 每个月最后一个周六凌晨2点执行一次
	// 0 0 2 LW * ? 每个月最后一个工作日凌晨两点执行一次
	// 0 0 2-4 ? * 1#1 每个月的第一个周一凌晨2点到4点间, 每个整点执行一次
	@Scheduled(cron="0 * * * * MON-SAT") // 告诉Spring这是一个定时任务
	public void hello() {
		System.out.println("hello...");
	}
}
  1. cron表达式:
  • 秒 0-59
  • 分 0-59
  • 小时 0-23
  • 日期 1-31
  • 月份 1-12
  • 星期 0-7或SUN-SAT, 注意0和7都是指SUN
  • 特殊字符
    • ,: 美剧
    • -: 区间
    • *: 任意
    • /: 步长
    • ?: 日或星期冲突匹配
    • L: 最后
    • W: 工作日
    • C: 和calendar联系后计算过的值
    • #: 星期, 4#2指代第二个星期四

邮件任务

邮件发送需要引入spring-boot-starter-mail
SpringBoot自动配置MailSenderAutoConfiguration
定义MailProperties内容, 配置在application.yml中
自动装配JavaMailSender
测试邮件发送

  1. 场景依赖:
<!-- 邮件服务依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-mail</artifactId>
</dependency>	
  1. 配置application.properties
spring.mail.username=1299868821@qq.com
spring.mail.password=xxxxxx # 注意这个应当是授权码, 而非登录密码, 详细需要在QQ邮箱中进行操作, 即设置-->常规-->POP3/IMPA/SMTP/Exchange/CardDAV/CalDAV服务, 然后开启POP3/SMTP与IMAP/SMTP服务
spring.mail.host=smtp.qq.com 
spring.mail.properties.mail.smtp.ssl.enable=true # 开启ssl
  1. 编写测试文件
@Autowired
JavaMailSenderImpl mailSender;
	
@Test
public void testMail() {
	SimpleMailMessage message = new SimpleMailMessage();
	message.setSubject("通知-今晚开会"); 
	message.setText("今晚7:30开会"); 
	message.setTo("caoyang@163.mail.sufe.edu.cn");
	message.setFrom("1299868821@qq.com");
	mailSender.send(message);
}

@Test
public void testMail1() {
	MimeMessage message = mailSender.createMimeMessage();
	MimeMessageHelper helper = new MimeMessageHelper(message,true);
	
	helper.setSubject("通知-今晚开会"); 
	helper.setText("<h1>今晚7:30开会</h1>"); 
	helper.setTo("caoyang@163.mail.sufe.edu.cn");
	helper.setFrom("1299868821@qq.com");
	
	// 附件
	helper.addAttachment("1.jpg",new File("image/1.jpg"));
	helper.addAttachment("2.jpg",new File("image/2.jpg"));
	
	mailSender.send(message)
}

13. Spring Boot安全

测试环境搭建

  1. 两种主流框架: shiro与spring security, 后者是springboot底层用的框架
  2. 安全框架两个点: 认证与授权

登录认证授权

  1. 引入spring security模块依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>	
  1. 编写spring security的配置
  • 它的配置都是现成的不需要自己写, 只需要写配置类
  • 配置类
    • 配置类的上面添加@EnableWebSecurity注解
    • 配置类应当要继承(extends)WebSecurityConfigurerAdapter类
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    	
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		//super.configure(http);
    		//定制请求的授权规则
    		http.authorizeRequests().antMatchers("/").permitAll()
    			.antMatchers("/level1").hasRole("VIP1")
    			.antMatchers("/level2").hasRole("VIP2")
    			.antMatchers("/level3").hasRole("VIP3");
    			
    		//开启自动配置的登录功能, 效果是如果没有权限就会专到登录页
    		http.formLogin();
    		//1. /login来到登录页
    		//2. 重定向到/login?error表示登陆失败
    		//3. 更多详细规定用到再说
    		
    		
    	}
    			
    			
    	//定义认证规则, 转到登录页后登录后就给不同的权限
    	@Override
    	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    		//super.configure(auth);
    		auth.inMemoryAuthentication()
    			.withUser("zhangsan").password("123456").roles("VIP1","VIP2")
    			.and()
    			.withUser("lisi").password("123456").roles("VIP2","VIP3")
    			.and()
    			.withUser("wangwu").password("123456").roles("VIP1","VIP3");
    	}
    }
    

权限控制与注销

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//super.configure(http);
		//定制请求的授权规则
		http.authorizeRequests().antMatchers("/").permitAll()
			.antMatchers("/level1").hasRole("VIP1")
			.antMatchers("/level2").hasRole("VIP2")
			.antMatchers("/level3").hasRole("VIP3");
			
		//开启自动配置的登录功能, 效果是如果没有权限就会专到登录页
		http.formLogin();
		//1. /login来到登录页
		//2. 重定向到/login?error表示登陆失败
		//3. 更多详细规定用到再说
		
		//开启自动配置的注销功能
		http.logout().logoutSuccessUrl("/"); // 注销成功后返回首页
		//1. 访问/logout表示用户注销, 并清空session
		//html的模板语言里应该要写<form th:action="@{/logout}" method="post"><input type="submit" value="注销"/></form>
		//2. 注销成功会返回/login?Logout页面, 可以用logoutSuccessUrl来改变注销返回的页面
	}
			
			
	//定义认证规则, 转到登录页后登录后就给不同的权限
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		//super.configure(auth);
		auth.inMemoryAuthentication()
			.withUser("zhangsan").password("123456").roles("VIP1","VIP2")
			.and()
			.withUser("lisi").password("123456").roles("VIP2","VIP3")
			.and()
			.withUser("wangwu").password("123456").roles("VIP1","VIP3");
	}
}

记住我与定制登录页

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//super.configure(http);
		//定制请求的授权规则
		http.authorizeRequests().antMatchers("/").permitAll()
			.antMatchers("/level1").hasRole("VIP1")
			.antMatchers("/level2").hasRole("VIP2")
			.antMatchers("/level3").hasRole("VIP3");
			
		//开启自动配置的登录功能, 效果是如果没有权限就会专到登录页
		http.formLogin().usernameParameter("user").passwordParameter("pwd").loginPage("/userLogin").loginProcessingUrl("/login"); // 可以自己定制登录页userLogin
		//1. /login来到登录页
		//2. 重定向到/login?error表示登陆失败
		//3. 更多详细规定用到再说
		//4. 默认post形式的/login代表处理登录
		//5. 默认表单是username与password, 可以用usernameParameter与passwordParameter方法来修改
		//6. 一旦定制loginPage, 那么loginPage的post请求就是登录, 可以用loginProcessingUrl来修改
		// 此时对应的表单HTML应该是
		/*
		 * <form th:action="@{/userlogin}" method="post">
		 *     用户名: <input name="user" /><br>
		 *     密码: <input name="pwd" /><br>
		 *     <input type="checkbox" name="remember">记住我<br>
		 *     <input type="submit" value="登录">
		 * </form>
		 */
		
		
		//开启自动配置的注销功能
		http.logout().logoutSuccessUrl("/"); // 注销成功后返回首页
		//1. 访问/logout表示用户注销, 并清空session
		//html的模板语言里应该要写<form th:action="@{/logout}" method="post"><input type="submit" value="注销"/></form>
		//2. 注销成功会返回/login?Logout页面, 可以用logoutSuccessUrl来改变注销返回的页面
		
		//开启"记住我"功能
		http.rememberMe().rememberMeParameter("remember");
		//1. 登录页面会多出一个记住我的按钮
		//2. 登陆成功后将cookie发给浏览器保存, 以后访问都会带上这个cookie, 只要通过检查就可以免登录
		//3. 注销会删除cookie
		//4. 默认是记住我的name是rememberme, 可以用rememberMeParameter方法来修改
	}
			
			
	//定义认证规则, 转到登录页后登录后就给不同的权限
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		//super.configure(auth);
		auth.inMemoryAuthentication()
			.withUser("zhangsan").password("123456").roles("VIP1","VIP2")
			.and()
			.withUser("lisi").password("123456").roles("VIP2","VIP3")
			.and()
			.withUser("wangwu").password("123456").roles("VIP1","VIP3");
	}
}

14. Spring Boot分布式

dubbo简介

  1. 分布式应用:
  • 在分布式系统中, 国内常用zookeeper+dubbo组合, 而springboot推荐使用全栈的Spring, 即SpringBoot+SpringCloud
  1. ZooKeeper是一个分布式的, 开放源码的分布式应用程序协调服务
  2. Dubbo是alibaba开源的分布式服务框架, 最大的特点是按照分层的方式来架构

docker安装zookeeper

  1. docker pull zookeeper 可以用docker中国来加速
  2. docker run --name zk01 -p 2181:2181 --restart always -d 56d44270ae3

springboot+docker+zookeeper整合

  1. 引入依赖:
<!-- 引入dubbo -->
<dependency>
	<groupId>com.alibaba.boot</groupId>
	<artifactId>dubbo-spring-boot-starter</artifactId>
	<version>0.1.0</version>
</dependency>

<!-- 引入zookeeper的客户端工具 -->
<!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
<dependency>
    <groupId>com.github.sgroschupf</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.1</version>
</dependency>
  1. 配置文件: 配置dubbo的扫描包和注册中心地址
dubbo.application.name=provider-ticket
dubbo.registry.address=zookeeper://118.24.44.169.2181
dubbo.scan.base-packages=com.sufe.ticket.services
  1. 在服务类上添加@Component@Service注解, 将服务给发布出去

  2. 引用别的工程里的服务时需要用@Reference注解放在服务类变量上

@Reference // 远程引用
TicketService ticketService;

SpringCloud的Eurake注册中心

  1. SpringCloud分布式开发五大常用组件
  • 服务发现: Netflix Eureka
  • 客服端负载均衡: Netflix Ribbon
  • 断路器: Netflix Hystrix
  • 服务网关: Netflix Zuul
  • 分布式配置: Spring Cloud Config

服务注册

暂时不做SpringCloud分布式, 需要SpringCloud基础

服务发现与消费

暂时不做SpringCloud分布式, 需要SpringCloud基础

  1. @EnableDiscoveryClient: 放在主程序类上开启发现服务功能
  2. @LoadBalanced: 使用负载均衡机制

15. Spring Boot开发热部署

devtools开发热部署

  1. 引入依赖即可
<!-- devtools热部署所需依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-devtools</artifactId>
	<optional>true</optional>
	<scope>runtime</scope>
</dependency>
  1. Spring Loaded是spring官方提供的热部署程序, 实现修改类文件的热部署
  2. JRebel 付费

16. Spring Boot监控管理

监管端点测试

  1. 引入依赖spring-boot-starter-actuator, 可以使用SpringBoot提供的准生产环境下的应用监控和管理功能, 可以通过HTTP, JMX, SSH协议来进行操作, 自动得到审计, 健康及指标信息等

  2. 步骤:

  • 引入spring-boot-starter-actuator
  • 通过http方式访问监控端点
  • 可进行shutdown(POST提交, 此端点默认关闭)
  1. 可以访问/metrics, /beans来查看应用的监控情况
  • 所有监控端点:
    • autoconfig: 所有自动配置信息
    • auditevents: 审计事件
    • beans: 所有Bean信息
    • configprops: 所有配置属性
    • dump: 线程状态信息
    • env: 当前应用环境
    • health: 应用健康信息
    • info: 当前应用信息
    • metrics: 应用的各项指标
    • mappings: 应用@RequestMapping映射路径
    • shutdown: 关闭当前应用(默认关闭)
    • trace: 追踪信息(最新的http请求)
  • 需要配置management.security.enabled=false才能访问上述节点
  • 可以配置应用信息:
    • info.app.id=hello
    • info.app.version=1.0.0
  • 可以配置git.properties文件
    • git.branch=master
    • git.commit.id=xjkd33s
    • git.commit.time=2017-12-12 12:12:56

定制端点

  1. 定制端点一般通过endpoints+端点名+属性名来设置
  2. 修改端点id: endpoints.beans.id=mybeans
  3. 开启远程应用关闭功能: endpoints.shutdown.enabled=true
  4. 关闭端点: endpoints.beans.enabled=false
  5. 开启所需端点:
  • endpoints.enabled=false
  • endpoints.beans.enabled=true
  1. 关闭http端点: management.port=-1
  • 设置端口: management.port=8181
  1. 修改端点路径: endpoints.dump.path=/du

自定义HealthIndicator

  1. 自定义健康状态指示器
  • 编写一个指示器实现HealthIndicator接口
  • 指示器的名字: XXXHealthIndicator
  • 加入到容器中: @Component
  1. 一个例子
@Component
public class MyApp HealthIndicator implements HealthIndicator {
	@Override
	public Health health() {
		//自定义检查方法
		//Health.up().build()代表健康
		return Health.down().withDetail("msg","服务异常").build();
		
	}
}

END

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐