1、SpringBoot
回顾下什么是Spring?
Spring是一个开源框架,2003年兴起的一个轻量级的java开发框架,是为了解决企业级应用开发的复杂性而创建的,简化开发
Spring是如何简化Java开发的?
为了降低java开发的复杂性,Spring采用了以下4中关键策略:
- 基于POJO的轻量级和最小侵入性编程;
- 通过IOC,依赖注入(DI)和面向接口实现松耦合
- 基于切面(AOP)和管理进行声明式编程;
- 通过切面和模板减少代码;
什么是SpringBoot?
学习过javaweb的同学就知道,开发一个web应用,从最初的servlt+tomcat跑一个helloworld都要经历一些很复杂的步骤;后来就用了框架Struts,再到SpringMVC,最后到现在的SpringBoot,过一两年可能又到其他的web框架;目前SpringBoot在当下是非常火的,那什么是SpringBoot呢?就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置,能迅速的开发web应用,几行代码开发一个http接口;
java企业级应用->J2EE->Spring->SpringBoot的过程
Springboot的核心思想:约定大于配置
Springboot的主要优点:
Springboot的主要优点:
-
为所有Spring开发者更快的入门
-
开箱即用,提供各种默认配置来简化项目配置
-
内嵌式容器简化web项目
-
没有冗余代码生成和xml配置的要求
2、微服务
什么是微服务?
微服务是一种架构风格,他要求我们在开发一个应用的时候,这个应用必须构建成一系列小服务的组合,可以通过http的方式进行互通,要说微服务架构,先得说说过去我们的单体应用架构。
单体应用架构
所谓单体应用架构(all in one)是指,我们将一个应用中的所有服务都封装成一个应用中。
无论是ERP、CRM或是其他什么系统,你都把数据库访问,web访问等等各个功能放到一个war包内
- 这样做的好处是,易于开发和测试,也十分方便部署,当需要扩展时,只需要将war包复制多份,然后放到多个服务器上,再做负载均衡就可以了。
- 单体应用架构的缺点是,哪怕我要修改一个非常小的地方,我都需要停掉整个服务,重新打包,部署这个应用war包,特别是对于一个大型应用,我们不可能把所有内容都放在一个应用里面,我们如何维护、如何分工合作都是问题。
微服务架构
所谓微服务架构,就是打破之前all in one的架构方式,把每个功能元素独立出来,把独立出来的功能元素动态组合,需要的功能元素才去拿来组合,需要多一些时可以整合多个功能元素,所以微服务架构是对功能元素进行复制,而没有对整个应用进行复制。这样的好处是:
- 节省了调用资源
- 每个功能元素的服务都是一个可替换的,可独立升级的软件代码
如何构建微服务?
一个大型系统的微服务架构,就像一个复杂交织的神经网络,每一个神经元素就是一个功能元素,他们各自完成自己的功能,然后通过http相互请求调用,比如一个电话系统,查缓存,连接数据库,浏览页面,结账,支付等服务都是一个个独立的功能服务,都被微化了,它们作为一个个微服务功能构建了一个庞大的系统,如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。
但是这种庞大的系统架构给部署和运维带来很大的难度,于是,spring为我们带来了构建大型分布式微服务的全套、全程产品
- 构建一个个功能独立的微服务应用单元,可以使用springboot,可以帮我们快速构建一个应用。
- 大型分布式网络服务的调用跟,这部分由spring cloud来完成,实现分布式
- 在分布式中间,进行流式数据计算,批处理,我们有spring cloud data flow
- spring为我们想清楚了整个从开始构建应用到大型分布式应用全流程方案
3、第一个SpringBoot程序
方式一:
可通过官网快速创建一个项目,然后再使用idea导入项目即可
https://start.spring.io/
方式二:
通过idea直接创建一个SpringBoot项目
创建完之后,直接写一个controller来测试即可
package com.ryan.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello(){
return "hello";
}
}
可以修改banner:在resources目录中新建一个banner.text,把想要的banner写进去即可,
获取banner网站推荐:https://www.bootschool.net/ascii
体验了第一个springboot程序之后↓↓↓↓
4、原理初探
为什么会这么简单呢?因为springboot中间帮我们做了很多事情,我们来看看一下
4.1、pom.xml
- spring-boot-starter-parent=》spring-boot-starter-parent:核心依赖都在父工程中
- 我们在写或者引入一些Springboot依赖的时候,不需要指定版本号,就是因为这些版本仓库
启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 启动器说白了就是Springboot的启动场景
- 比如上面这个spring-boot-starter-web,他会帮我妈自动导入web环境所有的依赖
- springboot会将所有的功能场景,都变成一个个的启动器
- 我们需要使用什么功能,就只需要找到对应的启动器就可以了
- 启动器有很多,我们可以通过官网文档可以看到:
https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/reference/html/using-spring-boot.html#using-boot-starter
4.2、主程序(重点)
package com.ryan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//标注这个类是一个springboot的应用
@SpringBootApplication
public class Springboot01HelloApplication {
//将springboot应用启动
public static void main(String[] args) {
SpringApplication.run(Springboot01HelloApplication.class, args);
}
}
这个程序看起来很简单, 但是里面的注解和启动程序并不简单啊
我们先来分析这个注解都干了什么!!
@SpringBootApplication
作用:标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;
进入这个注解,可以看到还有很多其他注解!
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
我们来看核心的几个注解即可
@ComponentScan
这个注解在Spring中很重要,他对应XML配置中的元素
作用:自动扫描并加载符合条件的组件或者bean,将这个bean定义加载到IOC容器中
@SpringBootConfiguration
作用:SpringBoot的配置类,标注在某个类上,表示这是一个SpringBoot的配置类
我们继续进去这个注解查看
@Configuration
public @interface SpringBootConfiguration {}
这里的 @Configuration,说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件;
继续点
@Component
public @interface Configuration {}
里面的@Component这就说明,启动类本身也是Spring中的一个组件,负责启动应用!
到这里,我们再回到SpringApplication注解中继续看另外一个重要注解
@EnableAutoConfiguration
@EnableAutoConfiguration:开启自动配置功能
以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我妈配置;
@EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效,我们点进去注解继续看看这个注解都干了些什么
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
首先是@AutoConfigurationPackage:自动配置包
点进这个注解看看
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}
@Import:Spring底层注解@Import,给容器导入一个组件
Registrar.class作用:将主启动类的所在包及包下所有子包里面的所有组件扫描到Spring容器
这个分析完了,我们退回上一步继续看另一个注解
@Import(AutoConfigurationImportSelector.class):给容器导入组件
AutoConfigurationImportSelector:自动配置导入选择器,那么他会导入哪些组件的选择器呢?我们点击进去这个类看源码:
- 这个类中有一个这样的方法
// 获得候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//这里的getSpringFactoriesLoaderFactoryClass()方法
//返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
- 这个方法又调用了 SpringFactoriesLoader 类的静态方法,我们进入 SpringFactoriesLoader 类的静态方法类的loadFactoryNames() 方法
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
//这里它又调用了 loadSpringFactories 方法
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
- 我们继续点击查看 loadSpringFactories 方法
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
//获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
//去获取一个资源 "META-INF/spring.factories"
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
//将读取到的资源遍历,封装成为一个Properties
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryClassName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryName = var9[var11];
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
- 发现一个多次出现的文件,spring.factories,全局搜索它
Spring.factories
我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!
WebMvcAutoConfiguration
我们在上面的自动配置类随便找一个打开看看,比如WebMvcAutoConfiguration
可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!
所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。
结论:
- SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
- 将这些值作为自动配置类导入容器,自动配置类就生效,帮我们进行自动配置工作
- 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
- 他会给容器中导入非常多的自动配置类(xxxAutoConfiguration),就是给容器中导入这个场景需要的所有组件,并配置好这些组件
- 有了自动配置类,免去了我们手动编写配置注入功能组件等的工作
现在应该大概的了解了下,SpringBoot的运行原理,后面我们还会深化一次
run方法
略
5、SpringBoot配置
配置文件
SpringBoot使用一个全局的配置文件,配置文件名称是固定的
- application.properties
- 语法结构:key=value
- application.yaml(官方推荐)
- 语法结构:key:空格value
配置文件的作用:修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了。
YAML
百度百科:YAML是"YAML Ain’t a Markup Language"(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为重点,而用反向缩略语重命名。
标记语言
我们来对比一下xml和yaml
yaml配置:
server:
port: 8080
xml配置:
<server>
<port>8080</port>
</server>
yaml语法
基础语法:
k: (空格)v
以此来表示一对键值对(空格不能省略);以空格的缩进来控制层级关系,只要左边对齐的一系列数据都是同一个层级的。
注意属性和值的大小写都是十分敏感的
值得写法
字面量: 普通的值(数字,布尔值,字符串)
k: v
字面量直接写在后面就可以,字符串默认不用加上双引号或者单引号
""双引号,不会转义字符串里面的特殊字符,特殊字符作为本身想要表示的意思;
比如name: “kuang \n shen” 输出:kuang 换行 shen
对象、map集合:比如一个student对象,有name和age属性
#对象
student:
name: ryan
age: 3
#行内写法(注意对象冒号的空格也不能省略)
student: {name: ryan,age: 8}
数组,类似markdown语法的列表语法:-空格
pets:
- cat
- dog
- bird
#行内写法
pet: [cat,dog,bird]
一般掌握以上三种就差不多够用了
6、给属性赋值的几种方式
方式一:使用原来的@Value注解
@Component
public class Dog {
//给实体类属性赋值
//方式一:使用@Value注解
@Value("旺财,你死的好惨啊")
private String name;
@Value("2")
private Integer age;
}
测试
@SpringBootTest
class Springboot02YamlApplicationTests {
//别忘了自动装配,而这里自动装配的前提是实体类中使用@Component等注解
@Autowired
private Dog dog;
@Test
void contextLoads() {
System.out.println(dog);//Dog{name='旺财,你死的好惨啊', age=2}
}
}
方式二:使用配置文件:application.yaml
@Component//注册bean
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类的所有属性和配置文件中相关的配置进行绑定;
参数prefix + "person":将配置文件中的person下面的所有属性一一对应
只有这个组件是容器的组件,才能使容器提供的@@ConfigurationProperties功能
*/
@ConfigurationProperties(prefix = "dog")
public class Dog {
private String name;
private Integer age;
}
application.yaml
# 方式二:使用yaml配置文件的方式来实现给实体类属性赋值
# 不过前提是你得在实体类中使用注解:@ConfigurationProperties(prefix = "dog")
dog:
name: "大家好,我是旺财"
age: 3
测试:略
再来试试复杂的一点的实体类
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private boolean sex;
private Date birth;
private List<Object> list;
private Map<String, Object> map;
private Dog dog;
}
application.yaml
# 复杂一点的实体类我,注意一些对象或者集合或者数组的写法
person:
name: 鸡你太美
age: 10
sex: false
birth: 2020/02/20
list:
- 唱
- 跳
- rap
map: {k1: v1,k2: v2}
dog:
name: 柯基
age: 0
测试:
@SpringBootTest
class Springboot02YamlApplicationTests {
//别忘了自动装配,而这里自动装配的前提是实体类中使用@Component等注解
@Autowired
private Person person;
@Test
void contextLoads() {
System.out.println(person);
/*
Person{name='鸡你太美',
age=10, sex=false,
birth=Thu Feb 20 00:00:00 CST 2020,
list=[唱, 跳, rap],
map={k1=v1, k2=v2},
dog=Dog{name='柯基', age=0}}
*/
}
}
注意:
- 实体类别忘了加上@Component注解,否则无法被扫描到,测试的时候,对象别忘了加上自动装配注解@Autowired
- 使用yaml的时候,别忘了在实体类中添加一个注解,并加上前缀(对象名称):@ConfigurationProperties(prefix = “person”)
对比yaml配置和@Value,发现yaml还是挺强大的
- cp只需要写一次即可,value则需要每个字段都添加
- 松散绑定:这个什么意思呢?比如我的yaml中写last-name,这个和lastName是一样的,-后面跟着的字母默认是大写的,这就是松散绑定
- JSR303数据校验,这个就是我们可以在字段增加一层过滤器验证,可以保证数据的合法性
- 复杂类型封装,yaml中可以封装对象,使用@Value就不支持
方式三:properties配置
先在实体类中添加注解@PropertySource(“classpath:xxx.properties”),属性使用SPEL表达式取出配置文件的值
@PropertySource("classpath:xxx.properties")
public class Person {
//SPEL表达式取出配置文件的值
@Value("${name}")
private String name;
private Integer age;
private boolean sex;
private Date birth;
private List<Object> list;
private Map<String, Object> map;
private Dog dog;
}
在properties配置文件中写键值对即可
name=ryan
测试:略
注意:如果想要使用properties的话,记得在idea中设置properties的编码格式为utf-8,不然会乱码
结论:
- 配置yaml和配置properties都可以获取到值,强烈推荐yaml
- 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下@Value
- 如果说,我们专门编写了一个JavaBean来和配置文件进行映射,就直接使用@ConfigurationProperties,不要犹豫!
7、JSR303数据校验
JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。
在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余和一些管理的问题,比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定。
使用:
-
启动validation自动装配(导入依赖)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
-
结合参数给对象添加校验注解
@Component
@ConfigurationProperties(prefix = "person")
@Validated
public class User {
private Integer id;
@NotBlank(message = "用户名不能为空")
private String username;
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "密码必须为8~16个字母和数字组合")
private String password;
@Email
private String email;
private Integer gender;
}
具体怎么去校验,后面再去学
8、配置文件路径
SpringBoot启动会扫描以下位置的application.properties或者application.yaml文件作为springboot的默认配置文件
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件
优先级由高到第排列,高优先级的配置会覆盖低优先级的配置
再强调一下,强烈推荐使用yaml去配置
如果有多个环境,可以这样
# 如果不同环境需要用到不同的端口,我们可以写多个端口出来,并使用---来分隔开
server:
port: 8080
# 然后通过设置使用哪个端口
spring:
profiles:
active: dev
---
server:
port: 8081
spring:
profiles: dev
---
server:
port: 8082
spring:
profiles: test
9、自动装配原理再理解
思考:配置文件中到底能写什么?怎么写?
从springboot官方文档中可以看到大量的配置, 我们不可能真的去背他的,要去了解他的一些规律和原理
以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理;
//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration
//启动指定类的ConfigurationProperties功能;
//进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
//并把HttpProperties加入到ioc容器中
@EnableConfigurationProperties({HttpProperties.class})
//Spring底层@Conditional注解
//根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
//这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(
type = Type.SERVLET
)
//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass({CharacterEncodingFilter.class})
//判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
//如果不存在,判断也是成立的
//即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
//他已经和SpringBoot的配置文件映射了
private final Encoding properties;
//只有一个有参构造器的情况下,参数的值就会从容器中拿
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}
//给容器中添加一个组件,这个组件的某些值需要从properties中获取
@Bean
@ConditionalOnMissingBean //判断容器没有这个组件?
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
return filter;
}
//..............
}
一句话总总结:根据当前不同的条件判断,决定这个配置类是否生效!
- 一旦这个配置类生效,这个配置类就会给容器添加各种组件
- 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的
- 所有在配置文件中能配置的属性都是在xxxProperties类封装着的
- 配置文件能配置什么就可以参照某个功能对应的这个属性类
例如随便点进一个JdbcTemplateAutoConfiguration
在配置文件能写的也就是上面的一些属性
这就是自动装配的原理
精髓:
- springboot启动会加载大量的自动配置类
- 我们看我们需要的功能有没有在SpringBoot默认写好的自动装配类当中
- 我们再来看这个自动装配类中到底配置了哪些组件(只要我们要用的组件存在其中,我们就需要再手动配置了)
- 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性,我们只需要在配置文件中指定这些属性的值即可
xxxAutoConfiguration:自动配置类;给容器添加组件
xxxProperties:封装配置文件中相关属性
了解:@Conditional
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
(没有条件的类)**
10、SpringBoot web开发
说到web开发,我们在springboot要思考一下如何处理以下问题:
- 导入静态资源
- 首页
- jsp,模板引擎Thymeleaf
- 装配扩展SpringMVC
- 增删改查
- 拦截器
- 国际化
从webmvcConfiguration源码中可以获取到一些如何以上问题的方法
10.1、静态资源处理
-
webjars
localhost:8080/webjars
(极少使用) -
classpath(resources)目录下的
- public, static, /**, resources
localhost:8080/
优先级:resources>static(默认)>public
- public, static, /**, resources
10.2、首页如何定制
在静态资源目录下新建一个index.html即可
10.3、thymeleaf
使用thymeleaf之前,我们要知道templates目录下的资源只能通过controller去访问
简单使用:
- 先导入相关依赖,可以直接开启一个启动器,或者直接导入相关依赖
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 编写一个controller
@Controller
public class TestController {
@RequestMapping("/t1")
public String test(Model model){
model.addAttribute("msg", "hello,thymeleaf");
return "test";
}
}
- 在templates目录下编写一个test.html,加入thymeleaf约束,并通过thymeleaf语法获取值
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div th:text="${msg}"></div>
</body>
</html>
如何通过thymeleaf表达式获取前端返回的值,先了解两种
- 普通值
- 集合遍历
@Controller
public class TestController {
@RequestMapping("/t1")
public String test(Model model){
model.addAttribute("msg", "<h1>hello,thymeleaf</h1>");
model.addAttribute("users", Arrays.asList("ryan","kobe"));
return "test";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div th:text="${msg}"></div>
<div th:utext="${msg}"></div>
<!--遍历:使用each,将users集合取出元素为user,然后再遍历每个元素的值-->
<div th:each="user:${users}" th:text="${user}"></div>
</body>
</html>
10.4、webmvc自动配置原理
在写项目之前,我们先了解一下webmvc的自动配置原理以及怎么去扩展webmvc
原理:可通过源码分析,此处略
扩展:
我们要做的就是编写一个@Configuration注解类,并且类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解;我们去自己写一个;我们新建一个包叫config,写一个类MyMvcConfig;
package com.ryan.config;
import com.ryan.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Bean;
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.WebMvcConfigurer;
//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//项目的首页定制,添加一个视图解析器
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//通过以下访问路径可以访问到index.html页面,
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
//登陆后不希望在地址栏上显示相关信息,可自定义一个地址,登陆时重定向到此即可
registry.addViewController("/main.html").setViewName("dashboard");
}
//将自定义的国际化组件放到容器中
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
//拦截器配置
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有请求,除了登陆页面,静态资源后面再继续完善配置
registry.addInterceptor(new LoginInterceptor()).
addPathPatterns("/**").
excludePathPatterns("/index.html", "/user/login","/","/css/**","/js/**","/img/**");
}
}
- 实体类
package com.ryan.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
package com.ryan.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
public class Employee {
private Integer id;
private String name;
private String email;
private Integer gender;//1表示男生,0表示女生
private Department department;
private Date birth;
public Employee(Integer id, String name, String email, Integer gender, Department department) {
this.id = id;
this.name = name;
this.email = email;
this.gender = gender;
this.department = department;
this.birth = new Date();
}
}
- dao+伪造数据库
package com.ryan.dao;
import com.ryan.pojo.Department;
import com.ryan.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@Repository
public class EmployeeDao {
@Autowired
private DepartmentDao departmentDao;
private static Map<Integer, Employee> employees = new HashMap<>();
static{
employees.put(1001,new Employee(1001,"小王", "wang@qq.com",1,new Department(101, "教学部")));
employees.put(1002,new Employee(1002,"小红", "wang@qq.com",0,new Department(102, "教研部")));
employees.put(1003,new Employee(1003,"小明", "wang@qq.com",1,new Department(103, "市场部")));
employees.put(1004,new Employee(1004,"小花", "wang@qq.com",0,new Department(104, "后勤部")));
employees.put(1005,new Employee(1005,"小张", "wang@qq.com",1,new Department(105, "技术部")));
}
//CRUD
//增加一个员工
private static Integer employeeId = 1006;
public void save(Employee employee){
if (employee.getId() == null){
employee.setId(employeeId++);
}
employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
employees.put(employee.getId(),employee);
}
//删除
public void deleteEmployeeById(Integer id){
employees.remove(id);
}
//查询所有员工
public Collection<Employee> getAllEmployee(){
return employees.values();
}
//通过id查询员工
public Employee getEmployeeById(Integer id){
return employees.get(id);
}
}
package com.ryan.dao;
import com.ryan.pojo.Department;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@Repository
public class DepartmentDao {
//使用java模拟一个数据库
private static Map<Integer, Department> departments = new HashMap<>();
static{
departments.put(101,new Department(101, "教学部"));
departments.put(102,new Department(102, "教研部"));
departments.put(103,new Department(103, "市场部"));
departments.put(104,new Department(104, "后勤部"));
departments.put(105,new Department(105, "技术部"));
}
//CRUD
//查询所有部门
public Collection<Department> getAllDepartment(){
return departments.values();
}
//通过id查询所在部门
public Department getDepartmentById(Integer id){
return departments.get(id);
}
}
-
前端模板
- 对于公共部分(导航栏+侧边栏),可抽出来代码复用
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <!-- Bootstrap core CSS --> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/css/dashboard.css}" rel="stylesheet"> </head> <body> <!--公共部分--> <!--顶部导航栏--> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar"> <a class="navbar-brand col-sm-3 col-md-2 mr-0" th:href="@{/main.html}">[[${session.loginUser}]]</a> <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search"> <ul class="navbar-nav px-3"> <li class="nav-item text-nowrap"> <a class="nav-link" th:href="@{/user/logout}">注销</a> </li> </ul> </nav> <!--侧边栏--> <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar"> <div class="sidebar-sticky" > <ul class="nav flex-column"> <li class="nav-item"> <a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"> <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path> <polyline points="9 22 9 12 15 12 15 22"></polyline> </svg> 首页 <span class="sr-only">(current)</span> </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file"> <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path> <polyline points="13 2 13 9 20 9"></polyline> </svg> Orders </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart"> <circle cx="9" cy="21" r="1"></circle> <circle cx="20" cy="21" r="1"></circle> <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path> </svg> Products </a> </li> <li class="nav-item"> <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/employees}"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> <circle cx="9" cy="7" r="4"></circle> <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> <path d="M16 3.13a4 4 0 0 1 0 7.75"></path> </svg> 员工管理 </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2"> <line x1="18" y1="20" x2="18" y2="10"></line> <line x1="12" y1="20" x2="12" y2="4"></line> <line x1="6" y1="20" x2="6" y2="14"></line> </svg> Reports </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers"> <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon> <polyline points="2 17 12 22 22 17"></polyline> <polyline points="2 12 12 17 22 12"></polyline> </svg> Integrations </a> </li> </ul> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <span>Saved reports</span> <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> </a> </h6> <ul class="nav flex-column mb-2"> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Current month </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Last quarter </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Social engagement </a> </li> <li class="nav-item"> <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> Year-end sale </a> </li> </ul> </div> </nav> </body> </html>
-
其他页面,导入公共部分后,再使用框架添加组件即可
index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>请登录</title> <!-- Bootstrap core CSS --> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/css/signin.css}" rel="stylesheet"> </head> <body class="text-center"> <form class="form-signin" th:action="@{/user/login}"> <img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72"> <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1> <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p> <input type="text" class="form-control" name="username" th:placeholder="#{login.username}" required="" autofocus=""> <input type="password" class="form-control" name="password" th:placeholder="#{login.password}" required=""> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me"> [[#{login.remember}]] </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit">[[#{login.sign}]]</button> <p class="mt-5 mb-3 text-muted">© 2017-2018</p> <!--国际化操作,thymeleaf传参使用()--> <a class="btn btn-sm" th:href="@{/index.html(l=zh_CN)}">中文</a> <a class="btn btn-sm" th:href="@{/index.html(l=en_US)}">English</a> </form> </body> </html>
dashboard.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <!--顶部导航栏--> <div th:replace="~{comment/comments::topbar}"></div> <div class="container-fluid"> <div class="row"> <!--侧边栏--> <!--为解决高亮问题,传递参数给组件--> <div th:replace="~{comment/comments::sidebar(active='main.html')}"></div> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <div class="chartjs-size-monitor" style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: hidden; pointer-events: none; visibility: hidden; z-index: -1;"> <div class="chartjs-size-monitor-expand" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;"> <div style="position:absolute;width:1000000px;height:1000000px;left:0;top:0"></div> </div> <div class="chartjs-size-monitor-shrink" style="position:absolute;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1;"> <div style="position:absolute;width:200%;height:200%;left:0; top:0"></div> </div> </div> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom"> <h1 class="h2">Dashboard</h1> <div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-group mr-2"> <button class="btn btn-sm btn-outline-secondary">Share</button> <button class="btn btn-sm btn-outline-secondary">Export</button> </div> <button class="btn btn-sm btn-outline-secondary dropdown-toggle"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> This week </button> </div> </div> <canvas class="my-4 chartjs-render-monitor" id="myChart" width="1076" height="454" style="display: block; width: 1076px; height: 454px;"></canvas> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}" ></script> <script type="text/javascript" th:src="@{/js/popper.min.js}" ></script> <script type="text/javascript" th:src="@{/js/bootstrap.min.js}" ></script> <!-- Icons --> <script type="text/javascript" th:src="@{/js/feather.min.js}" ></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/js/Chart.min.js}" ></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
add.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <!--顶部导航栏--> <div th:replace="~{comment/comments::topbar}"></div> <div class="container-fluid"> <div class="row"> <!--侧边栏--> <div th:replace="~{comment/comments::sidebar(active='list.html')}"></div> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <form th:action="@{/add}" method="post"> <div class="form-group"> <label for="name">name</label> <input type="text" class="form-control" id="name" placeholder="name" name="name"> </div> <div class="form-group"> <label for="email">email</label> <input type="email" class="form-control" id="email" placeholder="email" name="email"> </div> <div> <label>gender</label> <!--这里别忘了添加value(1代表男性,0代表女性)--> <input type="radio" name="gender" value="1">男 <input type="radio" name="gender" value="0">女 </div> <div> <label>department</label> <!--我们在controller接收的是一个employee,所以我们需要提交的是其中一个属性--> <select name="department.id"> <option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option> </select> </div> <div class="form-group"> <label for="birth">birth</label> <input type="text" class="form-control" id="birth" placeholder="birth" name="birth"> </div> <button type="submit" class="btn btn-default">添加</button> </form> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script> <script type="text/javascript" th:src="@{/js/popper.min.js}"></script> <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script> <!-- Icons --> <script type="text/javascript" th:src="@{/js/feather.min.js}"></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/js/Chart.min.js}"></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
list.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <!--顶部导航栏--> <div th:replace="~{comment/comments::topbar}"></div> <div class="container-fluid"> <div class="row"> <!--侧边栏--> <div th:replace="~{comment/comments::sidebar(active='list.html')}"></div> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <h2><a href="/toAdd" class="btn btn-sm btn-success">添加员工</a></h2> <div class="table-responsive"> <table class="table table-striped table-sm"> <thead> <tr> <th>id</th> <th>name</th> <th>email</th> <th>gender</th> <th>department</th> <th>birth</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="emp : ${employees}"> <td th:text="${emp.getId()}"></td> <td th:text="${emp.getName()}"></td> <td th:text="${emp.getEmail()}"></td> <td th:text="${emp.getGender()}==1?'男':'女'"></td> <td th:text="${emp.getDepartment().getDepartmentName()}"></td> <td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td> <td> <!--点击,restful风格传参--> <a class="btn btn-sm btn-primary" th:href="@{/toUpdate/}+${emp.getId()}">修改</a> <a class="btn btn-sm btn-danger" th:href="@{/delete/}+${emp.getId()}">删除</a> </td> </tr> </tbody> </table> </div> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script> <script type="text/javascript" th:src="@{/js/popper.min.js}"></script> <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script> <!-- Icons --> <script type="text/javascript" th:src="@{/js/feather.min.js}"></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/js/Chart.min.js}"></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
update.html
<!DOCTYPE html> <!-- saved from url=(0052)http://getbootstrap.com/docs/4.0/examples/dashboard/ --> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <title>Dashboard Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- Custom styles for this template --> <link th:href="@{/css/dashboard.css}" rel="stylesheet"> <style type="text/css"> /* Chart.js */ @-webkit-keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } @keyframes chartjs-render-animation { from { opacity: 0.99 } to { opacity: 1 } } .chartjs-render-monitor { -webkit-animation: chartjs-render-animation 0.001s; animation: chartjs-render-animation 0.001s; } </style> </head> <body> <!--顶部导航栏--> <div th:replace="~{comment/comments::topbar}"></div> <div class="container-fluid"> <div class="row"> <!--侧边栏--> <div th:replace="~{comment/comments::sidebar(active='list.html')}"></div> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <form th:action="@{/update}" method="post"> <input type="hidden" name="id" th:value="${employee.getId()}"> <div class="form-group"> <label for="name">name</label> <input type="text" class="form-control" id="name" placeholder="name" name="name" th:value="${employee.getName()}"> </div> <div class="form-group"> <label for="email">email</label> <input type="email" class="form-control" id="email" placeholder="email" name="email" th:value="${employee.getEmail()}"> </div> <div> <label>gender</label> <!--这里别忘了添加value(1代表男性,0代表女性)--> <input th:checked="${employee.getGender()==1}" type="radio" name="gender" value="1">男 <input th:checked="${employee.getGender()==0}" type="radio" name="gender" value="0">女 </div> <div> <label>department</label> <!--我们在controller接收的是一个employee,所以我们需要提交的是其中一个属性--> <select name="department.id"> <option th:selected="${employee.getDepartment().getId()==dept.getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option> </select> </div> <div class="form-group"> <label for="birth">birth</label> <input type="text" class="form-control" id="birth" placeholder="birth" name="birth" th:value="${#dates.format(employee.getBirth(),'yyyy/MM/dd')}"> </div> <button type="submit" class="btn btn-default">修改</button> </form> </main> </div> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script> <script type="text/javascript" th:src="@{/js/popper.min.js}"></script> <script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script> <!-- Icons --> <script type="text/javascript" th:src="@{/js/feather.min.js}"></script> <script> feather.replace() </script> <!-- Graphs --> <script type="text/javascript" th:src="@{/js/Chart.min.js}"></script> <script> var ctx = document.getElementById("myChart"); var myChart = new Chart(ctx, { type: 'line', data: { labels: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], datasets: [{ data: [15339, 21345, 18483, 24003, 23489, 24092, 12034], lineTension: 0, backgroundColor: 'transparent', borderColor: '#007bff', borderWidth: 4, pointBackgroundColor: '#007bff' }] }, options: { scales: { yAxes: [{ ticks: { beginAtZero: false } }] }, legend: { display: false, } } }); </script> </body> </html>
-
控制器
- login.controller
package com.ryan.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.thymeleaf.util.StringUtils; import javax.servlet.http.HttpSession; @Controller public class LoginController { @RequestMapping("/user/login") public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model, HttpSession session){ if("ryan".equals(username) && "ryan123".equals(password)){ session.setAttribute("loginUser",username); return "redirect:/main.html"; } model.addAttribute("msg", "用户名或者密码错误"); return "index"; } @RequestMapping("/user/logout") public String logout(HttpSession session){ session.invalidate(); return "redirect:/index.html"; } }
employeeController
package com.ryan.controller; import com.ryan.dao.DepartmentDao; import com.ryan.dao.EmployeeDao; import com.ryan.pojo.Department; import com.ryan.pojo.Employee; import org.aopalliance.intercept.Interceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.Banner; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import java.util.Collection; @Controller public class EmployeeController { @Autowired private EmployeeDao employeeDao; @Autowired private DepartmentDao departmentDao; @RequestMapping("/employees") public String list(Model model){ Collection<Employee> employees = employeeDao.getAllEmployee(); model.addAttribute("employees",employees); return "/emp/list"; } @RequestMapping("/toAdd") public String toAddPage(Model model){ Collection<Department> departments = departmentDao.getAllDepartment(); model.addAttribute("departments",departments); return "emp/add"; } @RequestMapping("/add") public String add(Employee employee){ System.out.println("add==>" + employee); employeeDao.save(employee); return "redirect:/employees"; } //别忘了使用restful风格接收数据 @RequestMapping("/toUpdate/{id}") public String toUpdate(@PathVariable("id") Integer id, Model model){ Employee employee = employeeDao.getEmployeeById(id); model.addAttribute("employee",employee); Collection<Department> departments = departmentDao.getAllDepartment(); model.addAttribute("departments",departments); return "emp/update"; } @RequestMapping("/update") public String update(Employee employee){ employeeDao.save(employee); return "redirect:/employees"; } @RequestMapping("/delete/{id}") public String delete(@PathVariable("id")Integer id){ employeeDao.deleteEmployeeById(id); return "redirect:/employees"; } }
-
测试:略
注意点:
- 掌握扩展mvc配置,比如视图解析器,地区解析器,拦截器等
- 掌握国际化操作(语言切换)
- 常见thymeleaf表达式要会使用,下面是从官方文档复制过来的
Simple expressions:
Variable Expressions: ${...}
Selection Variable Expressions: *{...}
Message Expressions: #{...}
Link URL Expressions: @{...}
Fragment Expressions: ~{...}
Literals
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…
Text operations:
String concatenation: +
Literal substitutions: |The name is ${name}|
Arithmetic operations:
Binary operators: + , - , * , / , %
Minus sign (unary operator): -
Boolean operations:
Binary operators: and , or
Boolean negation (unary operator): ! , not
Comparisons and equality:
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
Conditional operators:
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
- 学会抽取公共部分,然后在其他页面中插入即可, 这样可以节省大量代码量
- 学会高亮技巧(传参,三元运算符判断)
- 强化增删改查基本操作
推荐模板:layui、x-admin
11、Spring Data(整合JDBC和DRUID)
Spring Data简介
对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。
Spring Boot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。
整合JDBC
-
创建一个项目(记得勾选数据库相关的配置)
-
确保导入好相关依赖
-
编写yaml配置文件连接数据库,数据源可以通过type来修改,这里修改为druid
spring: datasource: username: root password: 1227 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
配置完这一些东西后,我们就可以直接去使用了,因为SpringBoot已经默认帮我们进行了自动配置;去测试类测试一下
package com.ryan; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @SpringBootTest class Spring04DataApplicationTests { //自动配置数据源(application.yaml) @Autowired private DataSource dataSource; @Test void contextLoads() throws SQLException { //查看默认的数据源 //经测试是使用hikari的数据源 //我们还可以通过配置来指定使用哪种数据源,例如druid:class com.alibaba.druid.pool.DruidDataSource,当然底层永远都是jdbc System.out.println(dataSource.getClass());//class com.zaxxer.hikari.HikariDataSource //获取连接,报错,需要设置时区,回到配置文件中设置时区 Connection connection = dataSource.getConnection(); System.out.println(connection);//HikariProxyConnection@350703813 wrapping com.mysql.cj.jdbc.ConnectionImpl@242ff747 connection.close(); } }
JDBCTemplate
1、有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;
2、即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
3、数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
4、Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用
5、JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类
JdbcTemplate主要提供以下几类方法:
- execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
- update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
- query方法及queryForXXX方法:用于执行查询相关语句;
- call方法:用于执行存储过程、函数相关语句。
编写一个Controller,注入 jdbcTemplate,编写测试方法进行访问测试;
package com.ryan.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RestController
public class JDBCController {
//从源码可以看到,springboot已经有默认的数据源,然后我们也配置了相关数据,所以JdbcTemplate我们可以直接使用
@Autowired
JdbcTemplate jdbcTemplate;
@RequestMapping("/query")
public List<Map<String, Object>> query(){
String sql = "select * from mybatis.user";
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}
@RequestMapping("/add")
public String add(){
String sql = "insert into mybatis.user(id,username,gender,address) values(8,'ryan','male','guangzhou')";
jdbcTemplate.update(sql);
return "insert ok";
}
@RequestMapping("/update/{id}")
public String update(@PathVariable("id") int id){
String sql = "update mybatis.user set username=?,gender=?,address=? where id="+ id;
//封装
Object[] objects = new Object[3];
objects[0] = "ryan24";
objects[1] = "female";
objects[2] = "beijing";
jdbcTemplate.update(sql,objects);
return "update ok";
}
@RequestMapping("/delete/{id}")
public String delete(@PathVariable("id") int id){
String sql = "delete from mybatis.user where id=" + id;
jdbcTemplate.update(sql);
return "delete ok";
}
}
Druid简介
Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。
Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。
使用:
-
添加依赖
-
yaml配置文件中切换数据源类型
-
测试类中测试数据源类型是否切换成功
-
现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要 自己添加 DruidDataSource 组件到容器中,并绑定属性;
-
接着配置后台监控和过滤器
package com.ryan.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DruidConfig { //绑定druid的数据源 @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource(){ return new DruidDataSource(); } //配置后台监控 //其实这里的配置相当于之前的web.xml配置,只是springboot内置了servlet,就没有了servlet,config就成了一种替代方法,想要配置什么就new什么即可 @Bean public ServletRegistrationBean statViewServlet(){ ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); //后台需要有人登陆 Map<String, String> initParameters = new HashMap<>(); //用户名和密码为固定参数,不要乱写 initParameters.put("loginUsername","ryan"); initParameters.put("loginPassword","ryan123"); //允许谁访问 initParameters.put("allow",""); bean.setInitParameters(initParameters);//初始化参数,通过源码可以看到需要传一个map return bean; } //配置过滤器 public FilterRegistrationBean webStatFilter(){ FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>(); bean.setFilter(new WebStatFilter()); Map<String, String> initParameters = new HashMap<>(); initParameters.put("exclusions","*.js,*.css,/druid/*"); bean.setInitParameters(initParameters); return bean; } }
配置完毕后,我们可以选择访问 :http://localhost:8080/druid/login.html
12、异步任务
在Java中,一般在处理类似的场景之时,都是基于创建独立的线程去完成相应的异步调用逻辑,通过主线程和不同的线程之间的执行流程,从而在启动独立的线程之后,主线程继续执行而不会产生停滞等待的情况。
使用:
-
在方法上使用**@Async**
package com.ryan.service; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class AsyncService { //在Spring中,基于@Async标注的方法,称之为异步方法;这些方法将在执行的时候,将会在独立的线程中被执行, //调用者无需等待它的完成,即可继续其他的操作。 //@Async所修饰的函数不要定义为static类型,这样异步调用不会生效 @Async public void async(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hello"); } }
-
在启动类或者controller类使用@EnableAsync注解
package com.ryan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; //在启动类或者Control类加上 @EnableAsync 注解 @EnableAsync @SpringBootApplication public class Springboot09AsyncEmailApplication { public static void main(String[] args) { SpringApplication.run(Springboot09AsyncEmailApplication.class, args); } }
-
controller
package com.ryan.controller; import com.ryan.service.AsyncService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AsyncController { @Autowired AsyncService asyncService; @GetMapping("/async") public String async(){ asyncService.async();//对于前段来说,线程睡3秒,前段页面则会白屏3秒(转圈),这个用户体验非常不好 //那我们要怎么去解决他呢?spring帮我们做了,只需要添加相关注解即可 //第一步,在async()方法上添加@Async注解 //第二步:在启动类开启异步注解@EnableAsync return "3秒后"; } }
-
测试:略
13、邮件发送
在实际项目中,经常需要用到邮件通知功能。比如,用户通过邮件注册,通过邮件找回密码等;又比如通过邮件发送系统情况,通过邮件发送报表信息等等,实际应用场景很多。这里学习以下怎么在springboot中使用邮件功能
-
在qq邮箱中开启smtp,并生成授权码(配置文件需要使用)
-
新建一个springboot+web项目
-
导入mail依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
-
配置文件
spring.mail.host=smtp.qq.com spring.mail.username=635726567@qq.com spring.mail.password=上面生成的授权码 spring.mail.protocol=smtp spring.mail.default-encoding=utf-8
-
编写测试类
package com.ryan; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.File; @SpringBootTest class Springboot09AsyncEmailApplicationTests { @Autowired//注入JavaMailSenderImpl JavaMailSenderImpl mailSender; @Test void contextLoads() { //发送一个简单的邮件 //先创建一个简单邮件实例 SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); simpleMailMessage.setSubject("ryan,恭喜您中了500万");//标题 simpleMailMessage.setText("请登陆xxx网站领取你的500万奖金,过期不候");//内容 simpleMailMessage.setTo("635726567@qq.com");//收件人 simpleMailMessage.setFrom("635726567@qq.com");//发件人 mailSender.send(simpleMailMessage);//调用mailSender发送 } @Test void contextLoads2() throws MessagingException { //发送一个复杂的邮件,包括图片,html等等 //先创建一个复杂邮件实例 MimeMessage mimeMessage = mailSender.createMimeMessage(); //复杂邮件组装器,如果需要设置替代文本或添加内联元素或附件,需要开启multipart MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); helper.setSubject("ryan我爱你"); //写一个html文本,并开启解析html helper.setText("<p style='color:red'>自学java,走上致富道路</p>", true); //添加文件 helper.addAttachment("1.jpg",new File("C:\\Users\\86131\\Pictures\\1.jpg")); helper.setTo("635726567@qq.com"); helper.setFrom("635726567@qq.com"); mailSender.send(mimeMessage); } }
总结:
- 重点需要导入mail依赖
- 然后配置文件,需要填写自己的邮箱信息
本文讲解了如何在springboot中快速的发邮件。介绍了两种发送邮件的方式:1、普通模式;2、HTML模式。其实还支持多种丰富的模式,比如模板引擎等,这里就不讲解了,其实使用起来大同小异。
14、Spring Task 定时任务
-
在上一个项目service层直接写一个方法,然后直接执行即可
package com.ryan.service; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service public class AsyncService { //在Spring中,基于@Async标注的方法,称之为异步方法;这些方法将在执行的时候,将会在独立的线程中被执行, //调用者无需等待它的完成,即可继续其他的操作。 //@Async所修饰的函数不要定义为static类型,这样异步调用不会生效 @Async public void async(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("hello"); } /* 在springboot中开启定时任务,只需要在启动类开启定时支持@EnableScheduling, 然后在方法上使用即可@Scheduled */ /**默认是fixedDelay 上一次执行完毕时间后执行下一轮*/ //cron表达式,可以百度一下,比较复杂,不需要刻意去记忆 @Scheduled(cron = "0/5 * * * * *") public void run() throws InterruptedException { Thread.sleep(6000); System.out.println(Thread.currentThread().getName()+"=====>>>>>使用cron {}"+(System.currentTimeMillis()/1000)); } /**fixedRate:上一次开始执行时间点之后5秒再执行*/ @Scheduled(fixedRate = 5000) public void run1() throws InterruptedException { Thread.sleep(6000); System.out.println(Thread.currentThread().getName()+"=====>>>>>使用fixedRate {}"+(System.currentTimeMillis()/1000)); } /**fixedDelay:上一次执行完毕时间点之后5秒再执行*/ @Scheduled(fixedDelay = 5000) public void run2() throws InterruptedException { Thread.sleep(7000); System.out.println(Thread.currentThread().getName()+"=====>>>>>使用fixedDelay {}"+(System.currentTimeMillis()/1000)); } /**第一次延迟2秒后执行,之后按fixedDelay的规则每5秒执行一次*/ @Scheduled(initialDelay = 2000, fixedDelay = 5000) public void run3(){ System.out.println(Thread.currentThread().getName()+"=====>>>>>使用initialDelay {}"+(System.currentTimeMillis()/1000)); } }
总结:
- 第一步:启动类开启定时任务支持@EnableScheduling
- 第二步:在指定的方法上使用@Scheduled注解
- 相关表达式不需要刻意去记,要用的时候再面向百度编程即可
15、springboot整合redis
SpringBoot操作数据:spring-data jpa jdbc mongodb redis!
SpringData也是和SpringBoot齐名的项目!
说明:在SpringBoot2.x之后,原来使用的jedis被替换为了lettuce
jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的问题,需要使用jedis pool连接池,更像BIO模式
lettuce:采用netty,实例可以在多个线程中进行分享,不存在线程不安全的情况,可以减少线程数据了,更像NIO模式
源码分析:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")//如果存在我们自定义的RedisTemplate,默认的就会失效
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
//默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的,记住
//两个泛型都是object,我们最常用的是String,Object,所以下面也单独题了一个方法出来
//我们一般也不会使用默认的,可以自己自定义一个
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
序列化配置,系统默认的是jdk序列化
如果保存的是一个对象,而这个对象没有序列化,则会报一个错:
org.springframework.data.redis.serializer.SerializationException
下面来整合springboot测试下
-
新建一个springboot+web项目
-
导入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.0.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ryan</groupId> <artifactId>springboot-10-redis-simple</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-10-redis-simple</name> <description>Demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
测试环境:略
-
自定义一个RedisConfig
package com.ryan.springboot10redissimple.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; //固定模板 @Configuration public class RedisConfig { //自己定义一个RedisTemplate @Bean @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { // 我们为了自己开发方便,一般直接使用 <String, Object > RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(factory); // Json序列化配置 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // String 的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化 方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
-
编写工具类
package com.ryan.springboot10redissimple.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @Component public final class RedisUtil { @Autowired // @Qualifier("redisTemplate") private RedisTemplate<String, Object> redisTemplate; // =============================common============================ /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } // ============================String============================= /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * * @param key 键 * @param delta 要增加几(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @param delta 要减少几(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } // ================================Map================================= /** * HashGet * * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map<Object, Object> hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map<String, Object> map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map<String, Object> map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } // ============================set============================= /** * 根据key获取Set中的所有值 * * @param key 键 * @return */ public Set<Object> sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) expire(key, time); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * * @param key 键 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } // ===============================list================================= /** * 获取list缓存的内容 * * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return */ public List<Object> lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * * @param key 键 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, List<Object> value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, List<Object> value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) expire(key, time); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * * @param key 键 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } }
-
编写实体类
package com.ryan.springboot10redissimple.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; import java.io.Serializable; @Component @Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private String name; private int age; }
-
测试:略
注意点:
-
主要的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
从springboot2.x开始,jedis很多东西不支持了,都是推荐使用lettuce(从源码中也可以看到)
-
原生态的redis代码非常繁琐冗余,我们首先可以自己配置一个redisTemplate,然后再写一个工具类,这些都是死代码,在企业实际开发中也是这么用的,可以大大减少在业务层的代码量
总结:redis使用起来还是比较简单的,重要的是要去思考、理解其中的思想, 这也是在面试过程中的谈资,当然这里也只是在springboot中插播一下redis,后面会再去详细解析redis
学习来源:B站up主狂神说,感觉不错,感兴趣的可以去看下