【学习笔记】雷丰阳SpringBoot教程摘要
目录1. Spring Boot入门2. Spring Boot配置3. Spring Boot日志4. Spring Boot与Web开发5. Spring Boot与Docker6. Spring Boot数据访问7. Spring Boot启动配置原理8. Spring Boot自定义starters9. Spring Boot缓存10. Spring Boot消息11. Spring Bo
尝试在月底前更完, 之前看的那个教程虽然是2020年最新的, 但是感觉讲师有点半吊子, 慕名看了雷丰阳的SpringBoot教程https://www.bilibili.com/video/BV1Et411Y7tQ, 实在是精细而专业, 受益匪浅
认真要学还是亲自去看一遍网课, 本文主要是记录雷神授课时的笔记以及一些额外的细节, 相对较为详细, 可作参考
20201001更新: 都怪进巨太好看, 月底是更不玩了… 国庆再说吧…
最终还是做了个人, 赶在国庆假期最后一天把课程看完了. 后八章是进阶部分, 没有详细实践, 有些东西等要用再说吧
截至20201008, 更新完成16章111节的摘要
目录
- 1. Spring Boot入门
- 2. Spring Boot配置
- 3. Spring Boot日志
- 4. Spring Boot与Web开发
- 4.1 简介
- 4.2 webjars与静态资源映射资源
- 4.3 引入thymeleaf
- 4.4 thymeleaf语法
- 4.5 SpringMVC自动配置原理
- 4.6 扩展与全面接管SpringMVC
- 4.7 【实验】引入资源
- 4.8 【实验】国际化
- 4.9 【实验】登录和拦截器
- 4.10 【实验】Restful实验要求
- 4.11 【实验】员工列表: 公共页面抽取
- 4.12 【实验】员工列表: 链接高亮与列表完成
- 4.13 【实验】员工添加: 来到添加页面
- 4.14 【实验】员工添加: 添加完成
- 4.15 【实验】员工修改: 重用页面与修改完成
- 4.16 【实验】员工删除: 删除完成
- 4.17 错误处理原理与定制错误页面
- 4.18 定制错误数据
- 4.19 嵌入式Servlet容器配置修改
- 4.20 注册servlet三大组件
- 4.21 切换其他嵌入式Servlet容器
- 4.22 嵌入式Servlet容器自动配置原理
- 4.23 嵌入式Servlet容器自动启动原理
- 4.24 使用外部Servlet容器与JSP支持
- 4.25 外部Servlet容器启动SpringBoot应用原理
- 5. Spring Boot与Docker
- 6. Spring Boot数据访问
- 7. Spring Boot启动配置原理
- 8. Spring Boot自定义starters
- 9. Spring Boot缓存
- 10. Spring Boot消息
- 11. Spring Boot检索
- 12. Spring Boot任务
- 13. Spring Boot安全
- 14. Spring Boot分布式
- 15. Spring Boot开发热部署
- 16. Spring Boot监控管理
1. Spring Boot入门
-
Spring Boot来简化Spring应用开发, 约定大于配置, 去繁从简, just run就能创建一个独立的, 产品级别的应用
-
背景: J2EE笨重的开发, 繁多的配置, 低下的开发效率, 复杂的部署流程, 第三方技术集成难度大
-
Spring Boot问题解决: Spring全家桶时代
- Spring Boot: J2EE一站式解决方案
- Spring Cloud: 分布式整体解决方案
- Spring Boot优点:
- 快速创建独立运行的Spring项目以及主流框架的集成
- 使用嵌入式的Servlet容器, 应用无需打包成WAR包
- starters自动依赖与版本控制
- 大量的自动配置, 简化开发, 也可以修改默认值
- 无需配置XML, 无代码生成, 开箱即用
- 准生产环境的运行时应用监控
- 与云计算天然继集成
- Spring Boot与微服务: 区别于单体应用架构风格
- 2014年由Martin Fowler提出
- 一个应用应该是一组小型服务, 可以通过HTTP的方式进行互通
- 每一个功能元素欧式一个可独立替换和独立升级的软件单元
- 详细参考微服务文档: https://caoyang.blog.csdn.net/article/details/108700844
- Spring Boot前置环境约束:
jdk 1.7
版本及以上版本maven 3.x
版本spring tools suite 4.x.x
版本, 截至本文发布时spring-boot发行的最新版本是4.8.0
- 关于如何安装springboot及启动一个Helloworld项目详见https://caoyang.blog.csdn.net/article/details/108700844
- 因为笔者使用的是
sts4.8.0
, 而雷风阳使用的是sts1.5.9
, 因此上手时参考的是另一个视频教程, 后来感觉还是雷风阳的教程比较规整, 因此切换回来后跳过了入门部分的一些章节, 比如sts4从零开始创建Helloworld项目的流程以及注解器和日志等细节, 该链接中笔者较为详细地记录了这些跳过的内容, 两个版本区别可能不会太大; 下文中很多笔记都是基于链接中创建的Helloworld
项目继续完善的
2. Spring Boot配置
补充:
.properties
文件的注释是#
,.yml
文件不建议写注释
-
properties与yml配置文件详见https://caoyang.blog.csdn.net/article/details/108700844第二章内容
-
配置文件数据注入到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.properties
或application.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(); } }
- 在
- 配置文件占位符
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}
- 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
- 命令行带参数:
- properties多profile文件形式:
- 配置文件加载位置
- 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=<绝对路径>
- 即可以在打包完成后再给个新的配置文件路径
- 命令行启动jar包指令:
- 外部配置加载顺序: 官方文档中有17个, 这里选取其中常用的11个, 优先级从高到低
- 命令行参数:
java -jar spring-boot-xx-config-x.x.x-SNAPSHOT.jar --<参数名>=<参数值>
- 来自
java:com[/env
的JNDI属性 - Java系统属性(
System.getProperties()
) - 操作系统环境变量
RandomValuePropertySource
配置的random.*
属性值- jar包外部的
application-{profile}.properties
或application.yml
(带spring.profile
)配置文件 - jar包内部的
application-{profile}.properties
或application.yml
(带spring.profile
)配置文件 - jar包外部的
application.properties
或application.yml
(不带spring.profile
)配置文件 - jar包内部的
application.properties
或application.yml
(不带spring.profile
)配置文件 @Configuration
注解类上的@PropertySource
- 通过
SpringApplication.setDefaultProperties
指定的默认属性
- 自动配置原理:
- 配置文件中可以写什么东西?配置文件能配置的属性参照文档
- 自动配置原理:
- 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启动会加载大量的自动配置类
- 需要的功能有没有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 日志框架分类和选择
- 市面上的日志框架:
- 日志门面(日志的抽象层):
- JCL: Jakarta Commons Logging, 这个最后更新是2014年, 基本已经弃用了
- slf4j: Simple Logging Facade for java
- jboss-logging
- 日志实现:
- JUL:
java.util.logging
- logback
- log4j
- log4j2
- JUL:
- 选择日志门面中的一个作为框架(抽象层), 再选择日志实现中的一个来实现
SpringBoot底层是Spring框架, 默认使用的是JCL, 但SpringBoot选用的是slf4j呵呵logback
3.2 slf4j使用原理
- 如何在系统中使用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
- 遗留问题
- 开发A系统(slf4j+logback): Spring(commons-logging), Hibernate(jboss-logging), MyBatis等
- 统一日志记录, 即便是别的框架也一起使用slf4j来输出?
- 先排除其他的日志框架
- 用中间包替换原有的日志框架
- 引入slf4j其他的实现
- 详情可见: http://www.slf4j.org/images/legacy.png
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>
- SpringBoot底层也是使用slf4j+logback的方式进行日志记录
- SpringBoot也把其他的日志都替换成了slf4j
- SpringBoot使用了中间替换包
- 如果我们要引入其他框架, 一定要把这个框架的默认日志依赖移除
- 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默认配置
- 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
表示换行符
- LoggerFactory的logger对象方法(级别从低到高)
- logger.trace()
- logger.debug()
- logger.info()
- logger.warn()
- logger.error()
3.6 指定日志文件和日志Profile功能
- 指定配置
- 给类路径下放上每个日志框架自己的配置文件即可, 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:
- 举个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>
- 集成到springboot的yml格式配置文件的示例:
3.7 切换日志框架
- 可以按照slf4j的日志适配图, 进行相关的切换
- 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>
- 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 简介
- 使用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: 配置类来封装配置文件的内容
- 自己编写业务代码
- 目前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与静态资源映射资源
- 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
- 模板引擎:
- JSP
- Velocity
- Freemarket
- Thymeleaf: SpringBoot推荐, 语法简单, 功能强大
- 引入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语法
- 检查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
即可看到"成功!你好"的字样
- 注意点: 导入thymeleaf的名称空间:
- thymeleaf语法规则
th
标签: 官方文档PDF的第十章th:text
和th: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自动配置原理
- SpringBoot自动配置好了SpringMVC
- 以下是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
的所有自动场景
- 如
- 如何修改SpringMVC的默认配置
- 模式:
- SpringBoot在自动配置很多组件的时候, 先看容器中有没有用户自己配置的(
@Bean
,@Component
), 如果有就用用户配置的, 如果没有才会自动配置, 如果有些组件可以有多个(如ViewResolver
), 则会将用户配置的和自己默认的组合起来 - 在SpringBoot中会有非常多的xxxWebMvcConfigurer, 需要多留心
- SpringBoot在自动配置很多组件的时候, 先看容器中有没有用户自己配置的(
4.6 扩展与全面接管SpringMVC
- 扩展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的自动配置和我们的扩展配置都会起作用
- 全面接管SpringMVC配置
- SpringBoot对SpringMVC的自动配置不需要了, 所有都是我们自己配
- 只需要在配置类上添加
@EnableWebMvc
注解, 注意第1点里的配置文件是只能添加一个@Configuration
注解 - 一般不建议全面接管, 否则就连
index.html
也无法访问了 - 原理: 为什么
@EnableWebMvc
会使SpringMVC的默认配置都失效@EnableWebMvc
注解将WebMvcConfigurationSupport
组件导入进来- 导入的
WebMvcConfigurationSupport
组件只是SpringMVC的基本功能配置
4.7 【实验】引入资源
- 编写配置类实现路由映射
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 【实验】国际化
- 编写国际化配置文件
- 在
classpath
下新建i18n
文件夹, 其中新建三个配置文件login.properties
,login_en_US.properties
,login_zh_CN.properties
- 在里面分别填写一些默认配置数据(这是中文的模板, 还有英文的)
login.btn=登录 login.password=密码 login.remember=记住我 login.tip=请登录
- 在里面分别填写一些默认配置数据(这是中文的模板, 还有英文的)
- SpringBoot自动配置好了管理国际化资源的文件
- 在
application.properties
中进行国际化配置文件的路径配置:spring.messages.basename=i18n.login
- 去页面获取国际化的值:
- 官方文档第四章:
#{...}
获取国际化信息:<div th:text="#{login.tip}"></div>
即可 - 此时根据浏览器的语言或地区信息就可以实现中英文的切换
- 原理:
- 根据请求头中的
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
来切换语言, 而不是根据浏览器的默认语言配置得到的请求头来配置语言信息的
- 这样可以实现在访问的URL带请求字符串
4.9 【实验】登录和拦截器
- 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>
- 注意点:
- 开发时要设置
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(); } }
- 需要再去
- 拦截器:
- 首先需要创建拦截器类
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实验要求
- 实验要求:
- RestfulCRUD: CRUD满足Rest风格
- URI:
/资源名称/资源表示
- HTTP请求方式区分对资源CRUD操作
- 普通CRUD(uri来区分操作)与RestfulCRUD的比较
- 查询:
getEmp
v.s.emp---GET
- 添加:
addEmp?xxx
v.s.emp---POST
- 修改:
updateEmp?id=xxx&xxx=xx
v.s.emp/{id}---PUT
- 删除:
deleteEmp?id=xxx
v.s.emp/{id}---DELETE
- 查询:
- URI:
- 实验的请求架构:
- 查询所有员工: 请求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 【实验】员工列表: 公共页面抽取
从这边往下因为没有数据库资源和先置的几个数据模板类, 就不能跟着写代码了, 主要记一些要点
- thymeleaf公共页面元素抽取:
1. 抽取公共片段: th:fragment
<div th:fragment="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>
- 三种引入功能片段的
th
属性:
<footer th:fragment="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">
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
- 用途: 将不同页面上的公共元素或者HTML块进行共用, 方便修改也编写
4.12 【实验】员工列表: 链接高亮与列表完成
- 参数化的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'}">
- 列表循环
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 【实验】员工添加: 来到添加页面
<a th:href="@{/emp}"></a>
: uri是/emp
- 控制类中编写:
//来到员工添加页面
@GetMapping("/emp")
public String toAddPage(Model model) {
//来到添加页面, 查出所有部门, 在页面显示
Collection<Department> departments = departmentDao.getDepartment();
model.addAttribute("depts",departments); // 传到thymeleaf模板中的变量depts
return "emp/add";
}
4.14 【实验】员工添加: 添加完成
- 控制类中编写:
// 员工添加
// SpringMVC自动将请求参数和入参对象的属性进行一一绑定, 要求了请求参数的名字和javabean入参的对象里面的属性名是一样的
@PostMapping("/emp")
public String addEmp(Employee employee) {
//来到员工列表页面
System.out.println("保存的员工信息: "+employee);
employeeDao.save(employee);
// redirect是重定向到一个地址, forward是返回
//return "redirect: /emps";
return "forward: /emps";
}
- 提交的数据格式不对: 生日(日期)
- 日期格式化: SpringMVC将页面提交的值需要转换为指定的类型:
- 默认日期是按照
yyyy/MM/dd
- 通过修改
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss
来自定义日期格式
4.15 【实验】员工修改: 重用页面与修改完成
-
<a th:href="@{/emp/}+${emp.id}">
-
控制器类中编写:
//来到修改页面, 查出当前员工, 在页面回显
@GetMapping(/emp/{id}")
public String toEditPage(@PathVariable("id") Integer id) {
Employee employee = employeeDao.get(id);
model.addAttribute("emp",employee);
//回到修改页面(add是一个修改添加二合一的页面, 但是修改时页面上的表单应该有回显值, 即员工原来的信息)
return "emp/add";
}
th:checked="${emp.gender==1}"
: th:checked属性- 下拉式列表中使用
th:selected
属性
<select>
<option th:selected="${dept.id==emp.department.id}" th:values="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">
</select>
-
在修改时显示原先的员工信息, 添加则不显示:
<input type="text" th:values="${emp!=null}?${emp.lastName}">
-
在控制器中编写:
//员工修改
@PutMapping("/emp")
public String updateEmployee(Employee employee) {
System.out.println("保存的员工信息: "+employee);
employeeDao.save(employee);
return "redirect: /emps";
}
4.16 【实验】员工删除: 删除完成
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 错误处理原理与定制错误页面
- Spring默认的错误处理机制
- 默认效果:
- 返回一个默认的错误页面: Whitelabel Error Page xxx
- 如果是其他客户端访问, 默认响应一个json数据(可以使用客户端模拟器来测试)
- 原理:
- 可以参照
ErrorMvcAutoConfiguration
: 错误处理的自动配置类 - 给容器中添加了以下组件
DefaultErrorAttributes
: 在页面共享信息BasicErrorController
: 处理默认的/error
请求- 处理一: 返回HTML响应数据
produce="text/html"
- 处理二: 返回JSON响应数据
- 处理一: 返回HTML响应数据
ErrorPageCustomizer
:@Value("${error.path/error}") private String path = "/error"; //系统出现错误后来到error请求进行处理(类似web.xml注册的错误处理配置)
DefaultErrorViewResolver
- 一旦系统出现
4xx
或者5xx
之类的错误时,ErrorPageCustomizer
就会生效(定制错误的响应规则), 就会来到"/error"请求, 就会被BasicErrorController
处理 - 响应页面: 去哪个页面由
DefaultErrorResolver
解析得到的
- 可以参照
- 如何定制错误响应
- 如何定制错误的页面
- 有模板引擎的情况下: 编辑
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 定制错误数据
- 如何定制错误的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 }
- 这种写法的缺点是浏览器和客户端返回的都将是json数据, 解决方案是转发到
- 将我们的定制数据携带出去
- 出现错误以后, 会来到
/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容器配置修改
- SpringBoot默认用的是Tomcat作为嵌入式的Servlet容器
- 问题:
- 如何定制和修改Servlet容器的相关配置(如端口等)?
- 修改和
server
有关的配置即可, 如:server.port server.context-path server.tomcat.xxx
- 也可以编写一个
EmbeddedServletContainer
: 嵌入式的Servlet容器的定制器, 来修改Servlet容器的配置
- 修改和
4.20 注册servlet三大组件
由于SpringBoot默认是以jar包的方式启动嵌入式的Servlet容器, 不能在
web.xml
配置文件里注册三大组件
- 注册servlet:
ServletRegistrationBean
- 注册Filter:
FilterRegistrationBean
- 注册Listener:
ServletListenerRegistrationBean
4.21 切换其他嵌入式Servlet容器
- SpringBoot支持Jetty和Undertow两种Servlet容器
- Jetty一般用于HTTP长连接
- Undertow不支持JSP
- Tomcat是SpringBoot默认支持的容器
- 切换方法: 修改
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容器自动配置原理
- 详见
EmbeddedServletContainerAutoConfiguration
类: 嵌入式的Servlet容器自动配置类
EmbeddedServletContainerFactory
: 嵌入式的Servlet容器工厂EmbeddedServletContainer
: 嵌入式的Servlet容器- 我们对嵌入式容器的配置修改是怎么生效:
ServerProperties
,EmbeddedServletContainerCustomer
EmbeddedServletContainerCustomizer
: 定制器帮我们修改了Servlet容器的配置- 容器中导入了:
EmbeddedServletContainerCustomizerBeanPostProcessor
- 步骤总结:
- SpringBoot根据导入的以来情况, 给容器中添加响应的
EmbeddedServletContainerFactory
- 以tomcat为例就是
TomcatEmbeddedServletContainerFactory
- 以tomcat为例就是
- 容器中某个组件要创建时对象就会惊动后置处理器
EmbeddedServletContainerCustomizerBeanPostProcessor
- 只要是嵌入式的Servlet容器工厂, 后置处理器就工作
- 后置处理器, 从容器中获取所有的
EmbeddedServletContainerCustomer
, 调用定制器的定制方法
4.23 嵌入式Servlet容器自动启动原理
- 问题:
- 什么时候创建嵌入式Servlet容器工厂?
- 什么时候获取嵌入式的Servlet容器并启动Tomcat?
- 获取嵌入式的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支持
- 嵌入式的Servlet容器: 应用打包成可执行的jar包
- 优点: 简单便携
- 缺点: 默认不支持JSP, 优化定制比较复杂
- 外置的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应用原理
SpringBootServletInitializer
: 重写configureSpringApplicationBuilder
:builder.source(@SpringBootApplication)
- 启动原理:
- Servlet3.0标准
ServletContainerInitializer
赛秒所有jar包中META-INF/services/javax.servlet.ServletContainerInitializer
文件指定的类并加载 - 加载spring web包下的SpringServletContainerInitializer
- 扫描
@HandleType(WebApplicationInitializer)
- 加载SpringServletContainerInitializer并运行
onStartup
方法 - 加载
@SpringBootApplication
主类, 启动容器
5. Spring Boot与Docker
简介
- Docker是一个开源的应用容器引擎, 基于Go原因呢并村从Apache2.0协议开源
- Docker可以让开发者打包他们的应用以及依赖包到一个轻量级, 可移植的容器中, 然后发布到任何流行的Linux机器上, 也可以实现虚拟化
- 容器是完全使用沙箱机制, 相互之间不会有任何接口, 更重要的是容器性能开销极低
- 将已经安装配置好的软件打包成一个镜像, 只要在Docker运行镜像就可以直接配置好
核心概念
- docker主机(Host): 安装了docker程序的机器, 一个物理或虚拟的机器用于执行Docker守护进程和容器, API
- docker镜像(Images): docker镜像是用于创建docker容器的模板
- docker容器(Container): 容器是独立运行的一个或一组应用
- docker客户端(Client): 客户端通过命令行或者其他工具使用docker
- docker仓库(Repository): docker仓库用来保存镜像, 可以理解为代码控制中的代码仓库, docker提供了庞大的镜像集合供使用
- 使用docker的步骤:
- 安装docker
- 去docker仓库找到这个软件对应的镜像
- 使用docker运行这个镜像, 这个镜像就会生成一个docker容器
- 对容器的启动停止就是对软件的启动停止
linux环境准备
- 安装linux虚拟机
- 可以安装VMWare(重量级), Oracle VM VirtualBox(轻量级)
- 导入虚拟机文件
- 双击启动linux虚拟机, 使用账号密码登录
- 使用客户端连接linux服务器进行命令操作
- 设置虚拟机网络
- 桥接网络
- 选择网卡
- 接入网线
- 使用命令重启虚拟机的网络:
service network restart
- 查看linux的ip地址:
ip addr
docker安装启动和停止
- 在linux虚拟机上安装docker
- 查看centos版本: docker要求centos系统内核版本高于
3.10
uname -r
- 升级软件包及内核(选做):
yum update
- 安装docker:
yum install docker
- 启动docker:
start docker
- 将docker服务设为开机启动:
systemctl enable docker
docker镜像操作常用命令
- 镜像操作
- 检索:
docker search <关键字>
docker search mysql
- 本质是去dockerhub里搜索
- 拉取:
docker pull <镜像名>
docker pull mysql
也可以写全名, 默认下载最新的mysqldocker pull mysql:5.5
下载5.5版本的mysql
- 列表:
docker images
查看本地所有docker镜像 - 删除:
docker rmi <镜像id>
删除指定的本地镜像
docker容器操作常用命令
- 软件镜像(如QQ安装程序)----运行镜像----产生一个容器(正在运行的QQ)
- 步骤:
- 搜索镜像:
docker search tomcat
- 拉取镜像:
docker pull tomcat
- 运行:
docker run --name <container-name> -d <image-name>
- e.g.
docker run --name myredis -d redis
--name
: 自定义容器名-d
: 后台运行
- e.g.
- 列表:
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
: 主机端口号(映射到)容器内部的端口
- e.g.
- 容器日志:
dockers logs <container-name>或<container-id>
- 更多命令
docker安装MySQL
- 后续课程需要安装的软件:
- 安装mysql: 数据访问
- 安装redis: 缓存
- 安装rabbitmq: 消息中间件
- 安装elasticsearch: 全文检索
- 安装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数据访问
简介
- 前置技术知识: JBDC, MyBatis, Spring Data JPA
- 对于数据访问层, 无论是SQL还是NOSQL, SpringBoot默认采用整合SpringData的方式进行统一处理, 添加大量自动配置, 屏蔽了很多设置, 引入各种
xxxTemplate
,xxxRepository
来简化我们对数据访问层的操作, 对我们来说只需要进行简单的设置即可
JDBC与自动配置原理
- 新建一个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>
- 编写
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 ```
-
此时再去单元测试文件中那个输出
dataSource.getClass()
, 结果为com.alibaba.druid.pool.DruidDataSource
-
数据源配置大全
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
- 自定义数据源配置类:
@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: 基础环境搭建
- 新建项目的时候将SQL中的mybatis framework添加进来, 相当于引入以下的starter依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
- 配置druid数据源的依赖, 见上一节
- 编写druid数据源的配置文件
application.yml
, 见上一节 - 编写
DruidConfig
配置类, 见上一节 - 建表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
- 运行项目执行建表sql, 则在mysql的mybatis数据库中会新建department与employee表
- 创建department与employee的数据模型javabean类
整合MyBatis: 注解版MyBatis
- 架构步骤:
- 创建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/userquery
与localhost:8080/userinsert?username=caoyang
实现查询与插入, 删除与更新同理
- 问题点记录:
- 建表时一定注意把主键字段设为自增, 否则会插入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对象
- 略
第二步: 启动应用
- 运行run方法
事件监听机制相关测试
- 几个重要的事件回调机制:
- 配置在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)); } }
- 最后在
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
- 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
- 官方命名空间:
- 推荐使用以下命名规约
- 启动器(starter)
- 实际操作:
- 新建一个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简介
- 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.Cache
和org.springframework.cache.CacheManager
接口来统一不同的缓存技术, 并支持使用JCache(JSR-107)注解来简化开发
Cache接口为缓存的组件规范定义, 包含缓存的各种操作集合
Cache接口下Spring提供了各种XXXCache的实现, 如RedisCache, EhCacheCache, ConcurrentMapCache
每次调用需要缓存功能的方法时, Spring会检查指定参数的指定目标方法是否已经被调用过, 如果有就直接从缓存中获取方法调用后的结果, 如果没有就调用方法并缓存结果后返回给用户, 下次调用直接从缓存中读取
使用Spring缓存抽象时注意两点: 确定方法需要被缓存以及它们的缓存策略, 如何从缓存中读取之前缓存存储的数据
- 几个重要概念和缓存注解
- Cache: 缓存接口, 定义缓存操作
- CacheManager: 缓存管理器, 管理各种缓存Cache组件
@Cacheable
: 主要针对方法配置, 能够根据方法的请求参数对其结果进行缓存@CacheEvict
: 清空缓存@CachePut
: 保证方法被调用, 又希望结果被缓存, 用于缓存更新@EnableCaching
: 开启基于注解的缓存- keyGenerator: 缓存数据时key生成策略
- serialize: 缓存数据时value序列化策略
基本环境搭建
- 基本就是MyBatis整合内容的回顾, 其中提到关于驼峰命名的mybatis配置可以写在
application.properties
文件中:mybatis.configuration.map-underscore-to-camel-case=true
@Cacheable初体验
- 快速体验缓存步骤:
- 开启基于注解的缓存: 主启动类上添加
@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
@Cacheable
的几个属性:
-
cacheNames/value
: 指定缓存组件的名字- 将方法的返回结果放在哪个缓存中, 可以是字符串, 也可以是字符串数组, 可以指定多个缓存
-
key
: 缓存数据使用的key, 可以使用它来指定, 默认是方法参数的值- 自定义key, 可以写SpEL表达式:
key="#root.methodName+'['+#id+']'"
- 自定义key, 可以写SpEL表达式:
-
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() + "]"; } }; } }
- 自己编写keyGenerator, 作为一个配置类, 然后配置写成
-
cacheManager
: 指定缓存管理器, 或者cacheResolver指定获取缓存解析器, 与cacheManager属性二选一 -
condition
: 指定符合条件的情况下才缓存: 如condition="#id>0"
- 举例:
condition="#a0>1
, 表示第一个参数值大于1时才进行缓存
- 举例:
-
unless
: 否定缓存, 当unless指定的条件为true则不使用缓存, 与condition恰恰相反, 但unless可以获取到结果进行判断, 如unless="#result==null"
- 举例:
unless="#a0==2"
- 举例:
-
sync: 是否使用异步模式
- 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运行流程
- 自动配置类:
CacheAutoConfiguration
类 - 缓存的配置类:
org.springframework.boot.autoconfigure.cache.XXXConfiguration
, 如Generic, JCache, EhCache, Hazel, Infinispan, Couchbase, Redis等 - 默认生效的配置类:
SimpleCacheConfiguration
- 给容器中注册了一个CacheManager: ConcurrentMapCacheManager
- 可以获取和创建ConcurrentMapCache类型的缓存组件, 它的作用是将数据保存在ConcurrentMap中
@Cacheable
运行流程
- 方法运行之前, 先驱查询Cache(缓存组件), 按照cacheNames指定的名字获取(CacheManager先获取相应的缓存), 第一次获取缓存如果没有Cache组件会自动创建
- 去Cache中查找缓存的内容, 使用一个key, 默认就是方法的参数
- key是按照某种策略生成, 默认是使用keyGenerator生成, 默认使用SimpleKeyGenerator生成key,
- SimpleKeyGenerator生成key的默认参数:
- 若没有参数:
key = new SimpleKey()
- 若有一个参数:
key = 参数值
- 若有多个参数:
key = new SimpleKey()
- 若没有参数:
- 没有查到缓存就调用目标方法
- 将目标方法返回的结果放进缓存中
-
@Cacheable
标注的方法执行之前先来检查缓存中有没有这个数据, 默认按照参数的值作为key去查询缓存, 如果没有就运行方法并将结果放入缓存, 以后再来调用就可以直接使用缓存中的数据 -
核心:
- 使用CacheManager【ConcurrentMapCacheManager】按照名字得到Cache【ConcurrentMapCache】组件
- ke使用keyGenerator生成的, 默认是SimpleKeyGenerator
@Cacheable其他属性
- 详解:
-
cacheNames/value
: 指定缓存组件的名字- 将方法的返回结果放在哪个缓存中, 可以是字符串, 也可以是字符串数组, 可以指定多个缓存
-
key
: 缓存数据使用的key, 可以使用它来指定, 默认是方法参数的值- 自定义key, 可以写SpEL表达式:
key="#root.methodName+'['+#id+']'"
- 自定义key, 可以写SpEL表达式:
-
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() + "]"; } }; } }
- 自己编写keyGenerator, 作为一个配置类, 然后配置写成
-
cacheManager
: 指定缓存管理器, 或者cacheResolver指定获取缓存解析器, 与cacheManager属性二选一 -
condition
: 指定符合条件的情况下才缓存: 如condition="#id>0"
- 举例:
condition="#a0>1
, 表示第一个参数值大于1时才进行缓存
- 举例:
-
unless
: 否定缓存, 当unless指定的条件为true则不使用缓存, 与condition恰恰相反, 但unless可以获取到结果进行判断, 如unless="#result==null"
- 举例:
unless="#a0==2"
- 举例:
-
sync: 是否使用异步模式
@CachePut
@CachePut
: 既调用方法, 又更新缓存数据
-
场景: 更新了数据库的某个数据, 同时更新缓存, 一般用于updateUser函数
-
运行时机:
- 先调用目标方法
- 将目标方法的结果缓存起来
-
测试步骤CachePut:
- 查询1号用户, 查到的结果会放在缓存中
- 以后查询还是之前的结果
- 更新1号用户的一些字段的数据, 将方法的返回值也放进了内存, 但是key与之前的Cacheable是不同的
- 再查询1号用户, 结果是更新前的结果(失败)
-
解决方案:
@CachePut(value="user",key="#result.userId")
, 指定缓存的key为更新前的key, 相当于更新缓存, 这要求@Cacheable的key是默认值, 即为@Cacheable(value="user")
key="#user.userId"
使用的是更新函数传入参数的userIdkey="#result.userId"
使用的是返回后的userId- 注意
@Cacheable
是不能使用#result
的
@CacheEvict
@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
- 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环境与测试
-
问题: 默认使用的是ConcurrentMapCacheManager==ConcurrentMapCache, 将数据保存在
ConcurrentMap<Object,Object>
, 实际开发中使用缓存中间件, 如redis, memcached, ehcache -
整合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, 将它映射到容器的6379docker ps
: 查看容器运行情况
- 打开redis后连接它
- name: redis
- host:
118.24.44.169
- port: 6379
- auth: 可以不设密码
- 下载redis镜像: 搜索hub.docker.com
- 常用redis指令: www.redis.cn/commands.html
APPEND key value
: 给key追加值BITCOUNT key [start end]
: 统计字符串起始位置的字节数
Redis Template与序列化机制
- 引入redis的starter: 查询官网
- 官网链接
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置redis: 去
application.properties
中编写spring.redis.host=118.24.44.169
-
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
- 然后直接用userTemplate来操作带有User类的键值映射即可
自定义CacheManager
- 测试缓存:
- 原理:
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简介
- 概述
- 大多数应用中, 可通过消息服务中间件来提升系统异步通信, 扩展解耦能力
- 消息服务中两个重要概念:
- 消息代理: message broker
- 目的地: destination
- 当消息放松这发送消息后, 将由消息代理接管, 消息代理保证消息传递到指定目的地
- 消息队列主要有两种形式的目的地:
- 队列: 点对点消息通信
- 主题: 发布publish或订阅subscribe信息通信
- 场景:
- 异步处理: 提升应用的运行速度
- 应用耦合: 通过消息队列传递数据来解耦两个应用
- 流量削峰: 秒杀场景, 将秒杀商品的请求存入消息队列, 堆满则拒绝
- 点对点式:
- 消息发送者发送消息, 消息代理将其放入一个队列中, 消息接收者从队列中获取消息内容, 消息读取后被移出队列
- 消息只有唯一的发送者和接受者, 但不是说只能有一个接收者
- 发布订阅式:
- 发送者发送消息到主题, 多个接收者监听这个主题, 那么就会在消息到达同时收到消息
- JMS: Java消息服务
- 基于JVM消息代理的规范, ActiveMQ, HornetMQ是JMS实现
- AMQP:
- 高级消息队列协议, 也是一个消息代理的规范, 兼容JMS
- RabbitMQ是AMQP的实现
- JMS对比AMQP:
- JMS是一个java api, AMQP是网络线级协议
- JMS不跨平台与语言, AMQP是跨平台和跨语言的
- JMS提供两种消息模型
Peer-2-Peer
和pub/sub
; AMQP提供物种消息模型direct
,fanout
,topic
,headers
,system
, 本质上来讲后四种与JMS的pub/sub
区别不大, 仅是在路由机制上做了更详细的划分 - JMS支持多种消息类型: TextMessage, MapMessage, BytesMessage, StreamMessage, ObjectMessage, Message; AMQP仅提供byte, 当实际应用有复杂消息, 将消息序列化后发送
- JMS适合在Java体系中多个客户端交互, 无需修改代码; AMQP定义了wire-level层的协议标准, 天然跨平台跨语言
- Spring支持:
spring-jms
提供对JMS支持spring-rabbit
提供对AMQP支持- 需要ConnectionFactory的实现来连接消息代理
- 提供JmsTemplate, RabbitTemplate来发送消息
@JmsListener
,@RabbitListener
注解在方法上监听消息代理发布的消息@EnableJms
,@EnableRabbit
开启支持
- SpringBoot自动配置:
- JmsAutoConfiguration
- RabbitAutoConfiguration
- 导入相关场景依赖即可
RabbitMQ基本概念简介
- Rabbit是AMQP的开源实现
- 核心概念:
- Message: 消息
- Publisher: 消息生产者
- Exchange: 交换器, 类似路由器的功效
- Queue: 消息队列
- Binding: 绑定, 消息队列和交换器之间的关联
- Connection: 网络连接, 比如一个TCP连接
- Channel: 信道, 多路复用连接中的一条独立的双向数据通道, 信道是建立在真实的TCP连接中的虚拟连接, 为了节省资源
- Consumer: 消息消费者
- Virtual: 虚拟主机, 表示一批交换器, 消息队列和相关对象
- Broker: 表示消息队列服务器实体
- 流程图:
- Publisher–>Broker(Virtual Host(Exchange–>(Binding)–>Queue))–>Connection(Channel1,Channel2,…,Channeln)–>Consumer
RabbitMQ运行机制
- AMQP相对于JMS增加了Exchange和Binding, 生产者把消息发布在Exchange上, 消息最终到达队列并被消费者接收, 而Binding决定交换器的消息应该绑定到哪个消息队列上
- Exchange类型: direct, fanout(广播模式, 转发消息最快), topic(进行模式匹配, 支持通配符
#与*
, 前者匹配0到多个单词, 后者匹配一个单词, 注意是单词级别的), heaers(几乎不用了)
RabbitMQ安装测试
- 引入依赖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发送接收消息与序列化机制
- 新建项目, 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>
- 配置
application.properties
spring.rabbitmq.host=118.24.44.169
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
#spring.rabbitmq.virtual-hosts
- 测试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
- 监听消息队列
- 在主程序类上添加
@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来创建
- AmqpAdmin用于创建和删除Queue, Exchange, Binding
@Autowired
AmqpAdmin amqpAdmin;
public void createExchange() {
amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
System.out.println("创建完成");
}
11. Spring Boot检索
Elasticsearch简介与安装
- Elasticsearch是目前全文搜索引擎的首选
- Elasticsearch是一个分布式的搜索服务, 提供Restful API, 底层基于Lucene
- wiki, github都是用Elasticsearch来实现全文搜索的
- 安装: 使用docker下载
Elasticsearch快速入门
SpringBoot整合Jest操作Elasticsearch
- 引入
spring-boot-starter-data-elasticsearch
- 安装SpringData对应版本的ElasticSearch
- 配置
application.properties
- 测试Elasticsearch
这个ElasticSearch可能有些过时了, 以后用到再仔细学吧
12. Spring Boot任务
异步任务
- 编写service, 三秒后响应
@Service
public class AsyncService {
@Async // 告诉Spring这是一个异步方法
public void hello() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("处理数据中...");
}
}
- 编写controller
public class AsyncController {
@Autowired
AsyncService asyncService;
@GetMapping("/hello")
public String hello() {
asyncService.hello();
return "success";
}
}
-
主程序类上添加
@EnableAysnc
注解开启异步 -
结果访问hello页面会立即响应
定时任务
- 两个注解: 主程序类上添加
@EnableScheduling
, 具体方法上添加@Scheduled
- 编写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...");
}
}
- 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
测试邮件发送
- 场景依赖:
<!-- 邮件服务依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
- 配置
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
- 编写测试文件
@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安全
测试环境搭建
- 两种主流框架: shiro与spring security, 后者是springboot底层用的框架
- 安全框架两个点: 认证与授权
登录认证授权
- 引入spring security模块依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 编写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简介
- 分布式应用:
- 在分布式系统中, 国内常用zookeeper+dubbo组合, 而springboot推荐使用全栈的Spring, 即SpringBoot+SpringCloud
- ZooKeeper是一个分布式的, 开放源码的分布式应用程序协调服务
- Dubbo是alibaba开源的分布式服务框架, 最大的特点是按照分层的方式来架构
docker安装zookeeper
- docker pull zookeeper 可以用docker中国来加速
- docker run --name zk01 -p 2181:2181 --restart always -d 56d44270ae3
springboot+docker+zookeeper整合
- 引入依赖:
<!-- 引入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>
- 配置文件: 配置dubbo的扫描包和注册中心地址
dubbo.application.name=provider-ticket
dubbo.registry.address=zookeeper://118.24.44.169.2181
dubbo.scan.base-packages=com.sufe.ticket.services
-
在服务类上添加
@Component
与@Service
注解, 将服务给发布出去 -
引用别的工程里的服务时需要用@Reference注解放在服务类变量上
@Reference // 远程引用
TicketService ticketService;
SpringCloud的Eurake注册中心
- SpringCloud分布式开发五大常用组件
- 服务发现: Netflix Eureka
- 客服端负载均衡: Netflix Ribbon
- 断路器: Netflix Hystrix
- 服务网关: Netflix Zuul
- 分布式配置: Spring Cloud Config
服务注册
暂时不做SpringCloud分布式, 需要SpringCloud基础
服务发现与消费
暂时不做SpringCloud分布式, 需要SpringCloud基础
@EnableDiscoveryClient
: 放在主程序类上开启发现服务功能@LoadBalanced
: 使用负载均衡机制
15. Spring Boot开发热部署
devtools开发热部署
- 引入依赖即可
<!-- devtools热部署所需依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>runtime</scope>
</dependency>
- Spring Loaded是spring官方提供的热部署程序, 实现修改类文件的热部署
- JRebel 付费
16. Spring Boot监控管理
监管端点测试
-
引入依赖
spring-boot-starter-actuator
, 可以使用SpringBoot提供的准生产环境下的应用监控和管理功能, 可以通过HTTP, JMX, SSH协议来进行操作, 自动得到审计, 健康及指标信息等 -
步骤:
- 引入
spring-boot-starter-actuator
- 通过http方式访问监控端点
- 可进行shutdown(POST提交, 此端点默认关闭)
- 可以访问
/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
定制端点
- 定制端点一般通过endpoints+端点名+属性名来设置
- 修改端点id:
endpoints.beans.id=mybeans
- 开启远程应用关闭功能:
endpoints.shutdown.enabled=true
- 关闭端点:
endpoints.beans.enabled=false
- 开启所需端点:
endpoints.enabled=false
endpoints.beans.enabled=true
- 关闭http端点:
management.port=-1
- 设置端口:
management.port=8181
- 修改端点路径:
endpoints.dump.path=/du
自定义HealthIndicator
- 自定义健康状态指示器
- 编写一个指示器实现HealthIndicator接口
- 指示器的名字: XXXHealthIndicator
- 加入到容器中:
@Component
- 一个例子
@Component
public class MyApp HealthIndicator implements HealthIndicator {
@Override
public Health health() {
//自定义检查方法
//Health.up().build()代表健康
return Health.down().withDetail("msg","服务异常").build();
}
}
END
更多推荐
所有评论(0)