Springboot入门
Helloworld
依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
主程序
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}
业务程序
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "Hello,Sentiment!";
}
}
配置文件
application.properties
server.port=8888
自动装配原理
容器功能
组件添加
1、@Configuration
在Spring中各实体的都是通过bean管理的,需要配置对应的xml文件
如:
<bean id="user01" class="com.sentiment.boot.bean.User">
<property name="name" value="Sentiment"></property>
<property name="age" value="18"></property>
</bean>
而在SpringBoot中则可以通过@Configuration来自定义组件,它的默认ID为方法名即:user01、tomcatPet
@Configuration
public class MyConfig {
@Bean
public User user01(){
User user = new User("Sentiment", 18);
user.setPet(tomcatPet());
return user;
}
@Bean
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
测试
//1、从容器中获取组件为单实例的
Pet tomcat01 = run.getBean("tomcatPet", Pet.class);
Pet tomcat02 = run.getBean("tomcatPet", Pet.class);
System.out.println("组件:"+(tomcat01==tomcat02));
MyConfig bean = run.getBean(MyConfig.class);
System.out.println(bean);
//2、如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
//保持组件单实例
User user = bean.user01();
User user1 = bean.user01();
System.out.println(user == user1);
User user01 = run.getBean("user01", User.class);
Pet tom = run.getBean("tomcatPet", Pet.class);
System.out.println("用户的宠物:"+(user01.getPet() == tom));
结果:
组件:true
com.sentiment.boot.config.MyConfig$$EnhancerBySpringCGLIB$$809ee907@6bfdb014
true
用户的宠物:true
但是当@Configuration(proxyBeanMethods = false)时,结果为:
组件:true
com.sentiment.boot.config.MyConfig@408613cc
false
用户的宠物:false
这是由于proxyBeanMethods的默认值为true,但当值为true时SpringBoot总会检查这个组件是否在容器中有,因此两个bean对象都是从容器中获取的所以相等
而设为false时,就不会从容器中检查,因此相当于创建了两个实例
2、@Bean、@Component、@Controller、@Service、@Repository
跟MVC的一样
3、@Import
再MyConfig上加上@Import
@Import(User.class)
这样在加载bean时除了前边加载的user01外,还会加载User
String[] namesForType = run.getBeanNamesForType(User.class);
for (String s : namesForType) {
System.out.println(s);
}
结果:
com.sentiment.boot.bean.User
user01
4、@Conditionnal
条件装配:满足Conditional指定的条件,则进行组件注册
以@ConditionalOnBean为例,当组件中有对应的bean时,则会进行组件注册
此时Config文件中有两个bean,分别为:user01、tomcat,但我们的条件设置为当有tomcatPet组件时才会进行组件注册
@ConditionalOnBean(name="tomcatPet")
public class MyConfig {
@Bean
public User user01(){
User user = new User("Sentiment", 18);
user.setPet(tomcatPet());
return user;
}
@Bean("tomcat")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
由于两个bean中不包含tomcatPet,因此默认不会进行注入,
测试
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); boolean tomcat = run.containsBean("tomcat");
System.out.println("容器中tomcat组件:"+tomcat);
boolean user01 = run.containsBean("user01");
System.out.println("容器中user01组件:"+user01);
}
}
结果
容器中tomcat组件:false
容器中user01组件:false
而当@ConditionalOnBean改为@ConditionalOnMissingBean,即:当存在tomcatPet时注入,改为不存在时注入,结果:
容器中tomcat组件:true
容器中user01组件:true
原生配置文件引入
1、@ImportResource
假设在Spring配置文件中设置了依赖注入,SpringBoot并不会识别Spring的配置文件,这就需要@ImportResource注解进行初始化
未使用该注解前,查看师傅注入test组件
boolean test = run.containsBean("test");
System.out.println("容器中test组件"+test);
结果
容器中test组件false
此时在config文件上加上:
@ImportResource("classpath:test.xml")
此时该配置文件中的bean就被注入到了组件当中,结果:
容器中test组件true
配置绑定
读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时修改使用;
1、@Component + @ConfigurationProperties
定义一个Car类,设置properties前缀为mycar
@Component
@ConfigurationProperties(prefix ="mycar")
public class Car {
private String brand;
private Integer price;
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", price=" + price +
'}';
}
}
接着再properties文件设置,前缀为mycar的赋值参数
mycar.brand=BYD
mycar.price=10000
写个控制器直接获取car类中属性的值
@Autowired
Car car;
@RequestMapping("/car")
public Car car(){
return car;
}
访问/car路径,成功初始化属性
2、@EnableConfigurationProperties + @ConfigurationProperties
将@EnableConfigurationProperties(Car.class)绑定到配置类上即可,主要用于调用其它没有加@Component的类
自动装配原理
看 03、了解自动配置原理 (yuque.com)
配置文件
推荐使用Yaml格式
数据类型
- 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
- 对象:键值对的集合。map、hash、set、object
k: {k1: v1,k2: v2,k3: v3}
或
k:
k1: v1
k2: v2
k3: v3
- 数组:一组按次序排列的值。array、list、queue
k: [v1,v2,v3]
k:
- v1
- v2
- v3
示例
Person
@ConfigurationProperties("person")
@Component
@ToString
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
Pet
@ToString
@Data
public class Pet {
private String name;
private Double weight;
}
配置文件application.yaml
person:
userName: Sentiment
boss: false
birth: 2001/12/7
age: 20
# interests: [唱,跳,rap]
interests:
- 唱歌
- 跳
- rap
# animal: [猫,狗]
animal:
- 猫
- 狗
# score: {english: 80,math: 90}
score:
english: 80
math: 90
salarys:
- 99.9
- 999.9
pet:
name: 猫
weight: 90
allPets:
sick:
- name: 猫
weight: 90
- {name: 狗,weight: 80}
health:
- {name: 鱼,weight: 20}
写一个控制器返回person类的结果
{"userName":"Sentiment","boss":false,"birth":"2001-12-06T16:00:00.000+00:00","age":20,"pet":{"name":"猫","weight":90.0},"interests":["唱","跳","rap"],"animal":["猫","狗"],"score":{"english":80,"math":90},"salarys":[99.9,999.9],"allPets":{"sick":[{"name":"猫","weight":90.0},{"name":"狗","weight":80.0}],"health":[{"name":"鱼","weight":20.0}]}}
配置提示
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
打包配置
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
web开发
静态资源访问
1、静态资源目录
静态资源默认存放在/static
、/public
、META-INF/resources
、/resources
,默认静态映射路径为/**
原理
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
因此请求顺序为:Controller —> 静态资源 —>404
修改静态资源设置
1、匹配静态资源的URL路径模式。即请求路径中带有/res才会被当做静态资源处理
spring:
mvc:
static-path-pattern: /res/**
2、修改静态资源位置
spring:
resources:
static-locations: classpath:/test
# 可用数组形式
# static-locations: [classpath:/test,/demo]
欢迎页支持
默认欢迎页有两种:
- 静态资源路径下的 index.html
- controller处理的/index请求
需注意不要设置访问前缀,否则会加载不到
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
resources:
static-locations: classpath:/test
自定义Favicon
favicon.ico 放在静态资源目录下即可,但同样不要设置访问前缀
原理分析
- SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
- SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
往下看给容器配置了什么,找到了WebMvcAutoConfigurationAdapter资源适配器
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
其中WebMvcProperties
就是获取spring.mvc下的配置,ResourceProperties
是spring.resources
1、配置类只有一个有参构造器
//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath
//ServletRegistrationBean 给应用注册Servlet、Filter....
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
2、资源处理规则
1、首先会从资源配置文件中查看是否设置了addMapping值(默认值为:true,代表启用静态资源规则),当我们手动设为false后,下边注册流程便都不会被注册
spring:
resources:
static-locations: classpath:/test
add-mappings: false
2、首先获取cache的period,即浏览器缓存时间,配置文件:
spring:
resources:
cache:
period: 10000
接着给webjar注册请求路径/webjars/**
,对应的本地映射文件位置在classpath:/META-INF/resources/webjars/
3、获取默认静态映射路径
并与本地文件对应
这也就解释了上边默认映射路径为什么是/**
,以及默认资源访问为什么是这四个路径
3、欢迎页处理规则
欢迎页首先会判断在指定路径是否有index.html
、默认路径是否为/**
,如果都符合则跳转到index.html
如果前边条件不符合,则会进入else判断是否注册对应的/index
的controller,如果有则跳转,没有则结束
请求参数处理原理
1、请求映射
1.1、rest原理
当我们使用rest风格时,我们的提交表单时无法提交delete、put等请求数据的
需将表单修改成_method的形式,即:
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete">
<input value="DELETE" type="submit">
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="put">
<input value="PUT" type="submit">
</form>
Controller
@RestController
public class HelloController {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET";
}
@RequestMapping(value = "/user", method = RequestMethod.POST)
public String saveUser(){
return "POST";
}
@RequestMapping(value = "/user", method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE";
}
@RequestMapping(value = "/user", method = RequestMethod.PUT)
public String putUser(){
return "PUT";
}
}
但即使这样设置后,SpringBoot仍然无法识别delete、put请求,因此根据源码看下原理
实现原理
在WebMvcAutoConfiguration中定义了MethodFilter,这里的enabled的设置为false因此无法正常匹配delete、put请求
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
因此需要再配置文件中设置
spring:
mvc:
hiddenmethod:
filter:
enabled: true
接着看下为什么提交表单时,要设置成method="post"
、name="_method"
:
<form action="/user" method="post">
<input name="_method" type="hidden" value="delete">
OrderedHiddenHttpMethodFilter中没什么东西,而它继承了HiddenHttpMethodFilter,因此直接看HiddenHttpMethodFilter,其中重写了doFilterInternal方法
主要流程:
获取request请求—>
判断该请求是否为POST是否有异常(这也就解释了为什么必须是POST) —>
获取methodParam的值,而它是_method,所以获取的值就是delete(这里就解释了为什么name=“_method”) —>
将delete改为大写—>
判断这个方法是否在ALLOWED_METHODS中—>
将request、method传入HttpMethodRequestWrapper中,而wrapper重写了getmethod方法,因此method重新赋值为delete,最后通过doFilter方法,完成methodFilter
扩展
流程中提到了获取_method的值,那如何修改这种方式?
再OrderedHiddenHttpMethodFilter中可以看到,他的Bean加载机制是当环境中没有HiddenHttpMethodFilter时,才会加载,从而一步步执行到刚刚的doFilterInternal方法中
那既然这样我们可以自定义Bean对象HiddenHttpMethodFilter,在其中修改methodParam的默认值,这样在环境启动后,Springboot便不会加载OrderedHiddenHttpMethodFilter
,进而不会进行methodParam
等初始化操作,也就不会修改我们自定义的methodParam
值
@Configuration(proxyBeanMethods = false)
public class MyConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}
1.2、请求映射原理
映射是通过DispatcherServlet
实现的,所以从这看起
DispatcherServlet
通过一级级继承关系,继承了HttpServlet
,而继承它就需要实现doGet
和doPost
方法
在FrameworkServlet
发现,实现了上述方法
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
/**
* Delegate POST requests to {@link #processRequest}.
* @see #doService
*/
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
主要是调用processRequest()
,直接看调用栈最终调用到了doDispatch
doDispatch:1016, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
在doDispatch
中经过getHandler()
方法后,发现已经获取到了需要映射的getUser()方法
因此直接看getHandler中进行了什么操作
不想写了,跟着调用栈最后通过createWithResolvedBean
进行了注册,一些重要操作都在lookupHandlerMethod中,自己调试看吧
2、普通参数与基本注解
2.1、注解
1、@PathVariable 路径变量
@RestController
public class ParamController {
@GetMapping("/car/{id}/owner/{name}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("name") String name,
@PathVariable Map<String,String> pv){
HashMap<String, Object> map = new HashMap<>();
map.put("id",id);
map.put("name",name);
map.put("pv",pv);
return map;
}
}
2、@RequestHeader 获取请求头
@RestController
public class ParamController {
@GetMapping("/car/{id}/owner/{name}")
public Map<String,Object> getCar(@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> headers){
HashMap<String, Object> map = new HashMap<>();
map.put("userAgent",userAgent);
map.put("headers",headers);
3、@CookieValue 获取Cookie
@RestController
public class ParamController {
@GetMapping("/car/{id}/owner/{name}")
public Map<String,Object> getCar(@CookieValue("_ga") String _ga,
@CookieValue Cookie cookie){
HashMap<String, Object> map = new HashMap<>();
map.put("_ga",_ga);
map.put("cookie",cookie);
4、@RequestBody 获取Body数据
@PostMapping("/post")
public Map postMethod(@RequestBody String content){
HashMap<String, Object> map = new HashMap<>();
map.put("content",content);
return map;
}
5、@RequestAttribute 获取request域属性
@Controller
public class AttributeController {
@GetMapping("/goto")
public String go(HttpServletRequest request){
request.setAttribute("name","Sentiment");
request.setAttribute("age",20);
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public Map<Object, Object> success(@RequestAttribute("name") String name,
HttpServletRequest request){
HashMap<Object, Object> map = new HashMap<>();
map.put("name",name);
//除注解外也可以通过request获取
Object age = request.getAttribute("age");
System.out.println(age);
return map;
}
}
6、@MatrixVariable 矩阵变量
1、语法:/cars/sell;low=34;brand=byd,audi,yd
2、SpringBoot默认是禁用了矩阵变量的功能,因此需要手动开启
在WebMvcAutoConfiguration的configurePathMatch()
中,实例化了UrlPathHelper类
而该类中的removeSemicolonContent
默认设置了移除分号功能因此需要将其关闭
configurePathMatch
是由WebMvcConfigurer
定义的,所以需要对其进行操作,有两种方法:
1、让自定义的config类继承WebMvcConfigurer
@Configuration(proxyBeanMethods = false)
public class MyConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
2、自定义WebMvcConfigurer
@Configuration(proxyBeanMethods = false)
public class MyConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
定义完成后
若路径中两处涉及到age的值,MatrixVariable可以通过设置pathVar参数来指定具体路径
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}
路径/boss/1;age=20/2;age=10
视图解析与模板引擎
1、Thymeleaf
官方文档:教程:使用百里香叶 (thymeleaf.org)
1.1、基本语法
1、表达式
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面片段 |
2、字面量
文本值: ‘one text’ , ‘Another one!’ **,…**数字: 0 , 34 , 3.0 , 12.3 **,…**布尔值: true , false
空值: null
变量: one,two,… 变量不能有空格
3、文本操作
字符串拼接: +
变量替换: |The name is ${name}|
4、数学运算
运算符: + , - , * , / , %
5、布尔运算
运算符: and , or
一元运算: ! , not
6、比较运算
比较: > , < , >= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )
7、条件运算
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
8、特殊操作
无操作: _
1.2、设置属性值-th:attr
设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
设置多个值
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法 th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
所有h5兼容的标签写法
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
1.3、迭代
可参考(dynamic_table.html#506-517)
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
若没有标签只是纯文本形式则使用双括号的形式,例如**(common.html#539)**:
[[${session.flag.userName}]]
1.4、条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
1.5、包含模板片段
可参考(common.html)
用于将多个模板的重复内容,集合到一个模板中使用
//定义
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
//使用
<div th:insert="~{footer :: copy}"></div>
或: <div th:insert="footer :: copy"></div>
也可以用选择器
//定义
<div id="copy-section">
© 2011 The Good Thymes Virtual Grocery
</div>
//使用
<div th:insert="~{footer :: #copy-section}"></div>
或:<div th:insert="footer :: #copy-section"></div>
官方文档:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#template-layout
1.6、属性优先级
2、使用
2.1、依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2.2、自动配置好了thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }
自动配好的策略
- 1、所有thymeleaf的配置值都在 ThymeleafProperties
- 2、配置好了 SpringTemplateEngine
- 3、配好了 ThymeleafViewResolver
- 4、我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
2.3、页面开发
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<a href="www.atguigu.com" th:href="${link}">去百度</a> <br/>
<a href="www.atguigu.com" th:href="@{link}">去百度2</a>
</h2>
</body>
</html>
拦截器
1、HandlerInterceptor 接口
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Object flag = session.getAttribute("flag");
if (flag!=null){
return true;
}
request.setAttribute("msg","请重新登录");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
2、配置拦截器
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).
addPathPatterns("/**").
excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**");
}
}
文件上传
1、页面表单
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>
若多文件上传可设置
<input type="file" name="file" multiple>
2、文件上传代码
设置最大文件、最大请求
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username")String username,
@RequestPart("headerImage")MultipartFile headerImage,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
//单文件上传
if (!headerImage.isEmpty()){
String originalFilename = headerImage.getOriginalFilename();
headerImage.transferTo(new File("D:\\cache\\"+originalFilename));
}
//多文件上传
if (photos.length > 0){
for (MultipartFile photo : photos) {
if (!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("D:\\cache\\"+originalFilename));
}
}
}
异常处理
/error/下的4xx,5xx页面会被自动解析
Servlet、Filter、Listener组件注入
1、使用Servlet API
Servlet
@WebServlet("/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("This is my servlet!");
}
}
这样设置后,并不能访问到设置的路径,需在启动文件加上对应的扫描组件@ServletComponentScan(basePackages = "com.sentiment.admin")
Filter
@Slf4j
@WebFilter("/css/*")
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作中");
chain.doFilter(request,response);
}
}
Listener
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}
2、使用RegistrationBean
除@WebServlet、@WebFilter、@WebListener这些注释外,还可以使用RegistrationBean的方式:ServletRegistrationBean
, FilterRegistrationBean
,ServletListenerRegistrationBean
@Configuration
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyServletContextListener mySwervletContextListener = new MyServletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
3、扩展
前边设置了拦截器,但是为什么MyServlet的/my
没有被拦截?
在Spring流程中用的是DispatcherServlet,而在tomcat中如果多个Servlet都能处理到同一层路径,就会优先精确原则。
因此当访问/my
时,由于精确优先原则就绕过了DispatcherServlet
,便绕过了拦截器
数据操作
1、SQL
1.1、导入数据库场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
导入数据库场景后,因为官方不知道我们要用什么驱动,因此还需要导入数据库驱动
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.3</version>
</dependency>
若不设置version,springboot2.3.4的默认版本为
<mysql.version>8.0.21</mysql.version>
也可以在pom文件中重新声明版本
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.3</mysql.version>
</properties>
1.2、自动配置
-
DataSourceAutoConfiguration : 数据源的自动配置
○修改数据源相关的配置:spring.datasource
○数据库连接池的配置,是自己容器中没有DataSource才自动配置的
○底层配置好的连接池是:HikariDataSource
1.3、修改配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
1.4、测试类
@SpringBootTest
@Slf4j
class MainApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long aLong = jdbcTemplate.queryForObject("select count(*) from student ", long.class);
log.info("条数{}",aLong);
}
}
2、使用Druid数据源
官方文档:https://github.com/alibaba/druid
整合第三方技术的两种方式
- 自定义
- 找starter
2.1、自定义方法
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
1、创建数据源
默认创建方式
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />
在Springboot中可以通过自定义配置类的方式,初始化数据源
@Configuration
public class DataSourceConfig {
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
}
2、配置监控页面
配置_StatViewServlet配置 · alibaba/druid Wiki (github.com)
web.xml配置
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
SpringBoot自定义配置类
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
return registrationBean;
}
3、监控统计功能
配置_StatFilter · alibaba/druid Wiki (github.com)
此时SQL监控中,并没有记录,需要打开监控统计功能
web.xml配置
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
... ...
<property name="filters" value="stat" />
</bean>
SpringBoot自定义配置类
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setFilters("stat");
return druidDataSource;
}
由于上边设置了spring.datasource
的@ConfigurationProperties,因此直接在yaml文件中配置亦可
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
filters: stat
4、配置Web关联监控
配置_配置WebStatFilter · alibaba/druid Wiki (github.com)
web.xml配置
<filter>
<filter-name>DruidWebStatFilter</filter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
<init-param>
<param-name>exclusions</param-name>
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DruidWebStatFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
SpringBoot自定义配置
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<>(webStatFilter);
registrationBean.setUrlPatterns(Arrays.asList("/*"));
registrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return registrationBean;
}
5、配置防火墙
配置 wallfilter · alibaba/druid Wiki (github.com)
web.xml配置
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
...
<property name="filters" value="wall"/>
</bean>
SpringBoot自定义配置
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setFilters("stat,wall");
return druidDataSource;
}
或者可以用yaml文件配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
filters: stat,wall
6、配置账号密码
web.xml配置
<!-- 配置 Druid 监控信息显示页面 -->
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
<init-param>
<!-- 允许清空统计数据 -->
<param-name>resetEnable</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<!-- 用户名 -->
<param-name>loginUsername</param-name>
<param-value>druid</param-value>
</init-param>
<init-param>
<!-- 密码 -->
<param-name>loginPassword</param-name>
<param-value>druid</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
SpringBoot配置
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","admin");
return registrationBean;
}
2.2、官方starter
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
1、分析自动配置
-
扩展配置项 spring.datasource.druid
-
DruidSpringAopConfiguration.class, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
-
DruidStatViewServletConfiguration.class, 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
-
DruidWebStatFilterConfiguration.class, web监控配置;spring.datasource.druid.web-stat-filter;默认开启
-
DruidFilterConfiguration.class}) 所有Druid自己filter的配置
-
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat"; private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config"; private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding"; private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j"; private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j"; private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2"; private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log"; private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
-
2、配置示例
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
druid:
aop-patterns: com.sentiment.admin.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)
stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin
login-password: admin
resetEnable: false
web-stat-filter: # 监控web
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat: # 对上面filters里面的stat的详细配置
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
3、整合Mybatis
https://github.com/mybatis
starter
-
SpringBoot官方的Starter:spring-boot-starter-*
-
第三方的Starter: *-spring-boot-starter
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>6
</dependency>
1、配置模式
- 全局配置文件
- SqlSessionFactory: 自动配置好了
- SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
- @Import(AutoConfiguredMapperScannerRegistrar.class);
- Mapper: 只要我们写的操作MyBatis的接口标准了 @Mapper 就会被自动扫描进来
@EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{}
@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties
可以在配置文件中设置驼峰命名方式:
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
也可通过springBoot配置文件修改
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/UserMapper.xml
configuration:
map-underscore-to-camel-case: true
步骤:
- 导入mybatis官方starter
- 编写mapper接口并标注@Mapper注解
- 编写sql映射文件并绑定mapper接口
- 在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信息 (建议;配置在mybatis.configuration)
2、注解模式
直接用@Select注解代替Mapper.xml文件
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getCity(Long id);
}
3、混合模式
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getCity(Long id);
public void insert(City city);
}
Mapper.xml
<mapper namespace="com.sentiment.admin.mapper.CityMapper">
<insert id="insert">
insert into city(`name`,`state`,`country`) values(#{name},#{state},#{country})
</insert>
</mapper>
这种方式自增的id回显值为null
所以在Mapper.xml文件上,可以加上
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into city(`name`,`state`,`country`) values(#{name},#{state},#{country})
</insert>
或者直接用注解形式
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getCity(Long id);
@Insert("insert into city(`name`,`state`,`country`) values(#{name},#{state},#{country})")
@Options(useGeneratedKeys = true,keyProperty = "id")
public void insert(City city);
}
4、MyBatis-Plus 完成CRUD
参考mybatis-plus官方文档,添加User数据库信息,以及Bean中字段
快速开始 | MyBatis-Plus (baomidou.com)
UserMapper
只需要继承BaseMapper并定义好User类型即可
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
测试类
@Test
void testUserMapper(){
User user = userMapper.selectById(1L);
log.info("用户信息为:{}",user);
}
}
运行后报错,提示表中没有user_name列。
Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'user_name' in 'field list'
@TableField
这是因为之前用过User类,并且定义了userName和password属性,而mabtis-plus不允许有非数据库中字段,因此可以用注解@TableField
来解决该问题
public class User {
@TableField(exist = false)
private String userName;
@TableField(exist = false)
private String password;
@TableName
除此外BaseMapper<>中的类型,代表的就是数据库名称即:User
public interface UserMapper extends BaseMapper<User> {
而此时若我把表名换成User_tbl则需要用到注解
@TableName("user_tbl")
public class User {
@TableField(exist = false)
private String userName;
@TableField(exist = false)
private String password;
private Long id;
private String name;
private Integer age;
private String email;
}
Service层
mybatis-plus中提供了 IService接口,来定义Service层的查询方法
public interface UserService extends IService<User> {
}
这时Service接口层的实现类,就需要实现对应的方法,而mybatis-plus有提供了对应的接口ServiceImpl
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
分页功能
@GetMapping("/dynamic_table")
public String dynamic(Model model, @RequestParam(value = "pn",defaultValue = "1") long pn){
// model.addAttribute("user",list);
List<User> list = userService.list();
// model.addAttribute("users",list);
Page<User> tPage = new Page<>(pn, 2);
Page<User> page = userService.page(tPage, null);
model.addAttribute("page",page);
// page.getCurrent(); 获取当前页数
// page.getPages(); 获取总页数
// page.getRecords(); 获取所有查询记录
return "table/dynamic_table";
}
分页功能还需要自定义config,否则可能显示错误
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//设置请求的页面大于最大页后操作,true调回到首页,false继续请求默认false
// paginationInterceptor.setoverfLow(false);
//设置最大单页限制数量,默认50日条,-1不受限制paginationInterceptor.setMaxLimit( 500 ) ;
//开启count 的join优化,只针对部分left join
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setOverflow(true);
paginationInnerInterceptor.setMaxLimit(500L);
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor;
}
}
html
<div class="row-fluid">
<div class="span6">
<div class="dataTables_info" id="dynamic-table_info">当前[[${page.current}]]页
总共[[${page.pages}]]页 共[[${page.total}]]条记录
</div>
</div>
<div class="span6">
<div class="dataTables_paginate paging_bootstrap pagination">
<ul>
<li class="prev disabled"><a href="#">← Previous</a></li>
<li th:class="${num == page.current?'active':'' }"
th:each="num:${#numbers.sequence(1,page.pages)}"><a
th:href="@{/dynamic_table(pn=${num})}">[[${num}]]</a></li>
<li class="next"><a href="#">Next → </a></li>
</ul>
</div>
</div>
</div>
删除
控制器
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1")Integer pn,
RedirectAttributes ra){
userService.removeById(id);
ra.addAttribute("pn",pn);
return "redirect:/dynamic_table";
}
前端
<td>
<a class="btn btn-danger btn-sm" type="button" th:href="@{/user/delete/{id}(id=${user.id},pn=${page.current})}">删除</a>
</td>
单元测试
1、JUnit5 的变化
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。
注意:
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
现在版本:
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Test
void contextLoads() {
}
}
以前:
@SpringBootTest + @RunWith(SpringTest.class)
SpringBoot整合Junit以后。
- 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
- Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚
2、JUnit5常用注解
JUnit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- **@Test 😗*表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
- **@ParameterizedTest 😗*表示方法是参数化测试,下方会有详细介绍
- **@RepeatedTest 😗*表示方法可重复执行,下方会有详细介绍
- **@DisplayName 😗*为测试类或者测试方法设置展示名称
- **@BeforeEach 😗*表示在每个单元测试之前执行
- **@AfterEach 😗*表示在每个单元测试之后执行
- **@BeforeAll 😗*表示在所有单元测试之前执行
- **@AfterAll 😗*表示在所有单元测试之后执行
- **@Tag 😗*表示单元测试类别,类似于JUnit4中的@Categories
- **@Disabled 😗*表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- **@Timeout 😗*表示测试方法运行如果超过了指定时间将会返回错误
- **@ExtendWith 😗*为测试类或测试方法提供扩展类引用
@DisplayName("Junit5功能测试类")
public class Junit5Test {
@DisplayName("测试displayname注解")
@Test
void testDisplayName(){
System.out.println(1);
}
@BeforeEach
void testBeforeEach(){
System.out.println("测试开始了");
}
@AfterEach
void testAfterEach(){
System.out.println("测试结束了");
}
@BeforeAll
static void testBeforeAll(){
System.out.println("测试马上开始...");
}
@AfterAll
static void testAfterAll(){
System.out.println("测试马上结束....");
}
@Disabled
@Test
void testDisabled(){
System.out.println("反正也不会执行");
}
//超时会报错
@Timeout(value = 500,unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(400);
}
}
@ExtendWith
上边测试中,若使用自动装配会发现返回null,这是由于并没有把Spring的内容扩展进来,这就需要用到@ExtendWith注解
而SpringBootTest就封装了该注解,所以直接用@SpringBootTest即可
@RepeatedTest
重复执行
@RepeatedTest(5)
void testRepeateTest(){
System.out.println(5);
}
3、断言(assertions)
3.1、简单断言
用来对单个值进行简单的验证。如:
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
示例
@DisplayName("简单断言")
@Test
public void simple(){
int calc = calc(2, 3);
assertEquals(5,calc);
assertNotEquals(3,calc);
Object o1 = new Object();
Object o2 = new Object();
assertSame(o2,o2);
assertNotSame(o1,o2);
assertTrue(true == true);
assertFalse(true == false);
assertNull(null);
assertNotNull(o1);
}
int calc(int i , int j){
return i+j;
}
3.2、数组断言
assertArrayEquals
@DisplayName("数组断言")
@Test
public void array(){
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}
3.3、组合断言
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}
3.4、异常断言
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}
3.5、超时断言
Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
3.6、快速失败
通过 fail 方法直接使得测试失败
@Test
@DisplayName("快速失败")
public void shouldFail() {
fail("This should fail");
}
4、前置条件(assumptions)
package com.sentiment.admin;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Objects;
import static org.junit.jupiter.api.Assumptions.*;
@DisplayName("前置条件测试")
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV1"),
() -> System.out.println("In DEV")
);
}
}
5、嵌套测试
JUnit 5 User Guide
直接看官方示例代码吧。。
6、参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
7、迁移指南
在进行迁移的时候需要注意如下的变化:
- 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
- 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
- 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
- 把@Ignore 替换成@Disabled。
- 把@Category 替换成@Tag。
- 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。
监控指标
1、SpringBoot Actuator
1.1、简介
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1.2、使用
- 引入场景
- 访问 http://localhost:8080/actuator/**
- 暴露所有监控信息为HTTP
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
- 测试
http://localhost:8080/actuator/beans
http://localhost:8080/actuator/configprops
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/jvm.gc.pause
http://localhost:8080/actuator/endpointName/detailPath
2、Actuator Endpoint
2.1、最常使用的端点
ID | 描述 |
---|---|
auditevents | 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件 。 |
beans | 显示应用程序中所有Spring Bean的完整列表。 |
caches | 暴露可用的缓存。 |
conditions | 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
configprops | 显示所有@ConfigurationProperties 。 |
env | 暴露Spring的属性ConfigurableEnvironment |
flyway | 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway 组件。 |
health | 显示应用程序运行状况信息。 |
httptrace | 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository 组件。 |
info | 显示应用程序信息。 |
integrationgraph | 显示Spring integrationgraph 。需要依赖spring-integration-core 。 |
loggers | 显示和修改应用程序中日志的配置。 |
liquibase | 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase 组件。 |
metrics | 显示当前应用程序的“指标”信息。 |
mappings | 显示所有@RequestMapping 路径列表。 |
scheduledtasks | 显示应用程序中的计划任务。 |
sessions | 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 |
shutdown | 使应用程序正常关闭。默认禁用。 |
startup | 显示由ApplicationStartup 收集的启动步骤数据。需要使用SpringApplication 进行配置BufferingApplicationStartup 。 |
threaddump | 执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump | 返回hprof 堆转储文件。 |
jolokia | 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core 。 |
logfile | 返回日志文件的内容(如果已设置logging.file.name 或logging.file.path 属性)。支持使用HTTPRange 标头来检索部分日志文件的内容。 |
prometheus | 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus 。 |
最常用的Endpoint
- Health:监控状况
- Metrics:运行时指标
- Loggers:日志记录
2.2、Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
- health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
- 可以很容易的添加自定义的健康检查机制
2.3、管理Endpoint
禁用所有的Endpoint然后手动开启指定的Endpoint
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
1、Profile功能
为了方便多环境适配,springboot简化了profile功能。
1.1、application-profile功能
-
默认配置文件 application.yaml;任何时候都会加载
-
指定环境配置文件 application-{env}.yaml
-
激活指定环境
-
- 配置文件激活 spring.profiles.active=prod
- 命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
-
-
- 修改配置文件的任意值,命令行优先
-
-
默认配置与环境配置同时生效
-
同名配置项,profile配置优先
1.2、@Profile条件装配功能
先定义两个类分别实现Person
Boss.java
若Profile使用的是prod则加载Boss
@Profile("prod")
@ConfigurationProperties("person")
@Component
@Data
public class Boss implements Person{
private String username;
private Integer age;
}
Worker.java
若Profile使用的是test则加载worker
@Profile("test")
@ConfigurationProperties("person")
@Component
@Data
public class Worker implements Person {
private String username;
private Integer age;
}
测试
配置为:spring.profiles.active=test
@Autowired
private Person person;
@GetMapping("/person")
public String person(){
return person.getClass().toString();
}
结果:
class com.sentiment.boot.bean.Worker
1.3、profile分组
分组功能是在SpringBoot 2.4.0版本以后才有的功能
spring.profiles.active=myprod
spring.profiles.group.myprod[0]=prod
spring.profiles.group.myprod[1]=prodd
此时retrun person,结果为
{"name":"prod-张三","age":20}
2、外部化配置
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
1、外部配置源
常用:Java属性文件、YAML文件、环境变量、命令行参数;
-
环境变量:
-
@Value("${JAVA_HOME}") private String env; @GetMapping("/") private String test(){ return env; }
-
环境变量在ConfigurableApplicationContext也可获取
@SpringBootApplication public class MainApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); ConfigurableEnvironment environment = run.getEnvironment(); Map<String, Object> systemEnvironment = environment.getSystemEnvironment(); Map<String, Object> systemProperties = environment.getSystemProperties(); System.out.println(systemEnvironment); System.out.println("========================="); System.out.println(systemProperties); } }
-
-
命令行参数:如profile中的命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
2、配置文件查找位置
(1) classpath 根路径
(2) classpath 根路径下config目录
(3) jar包当前目录
(4) jar包当前目录的config目录
(5) /config子目录的直接子目录
3、配置文件加载顺序:
- 当前jar包内部的application.properties和application.yml
- 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
- 引用的外部jar包的application.properties和application.yml
- 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
4、指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
3、自定义starter
看到这里非常兴奋,有两个原因:
- 前边有了解SpringBoot的自动装配原理,而自定义starter感觉就是自动装配原理的使用体现,并且感觉这部分是SpringBoot中最核心的部分。
- 看完这节SpringBoot2的初步了解就到这了。
所以这部分我也会更认真详细的记录
首先创建两个module,分别为:starter、starter-autuconfigure
starter-autuconfigure
充当自动配置包
以sayHello()
方法为例
HelloService
这里sayHello()
会retrun prefix 和sufix,而这两个属性在HelloSerivce中并没有定义,是从properties配置文件获取的
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(String username){
return helloProperties.getPrefix() + ":"+username+"=>"+helloProperties.getSufix();
}
}
HelloProperties
用于获取application.properties中person.hello的配置
@ConfigurationProperties("person.hello")
public class HelloProperties {
private String prefix;
private String sufix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSufix() {
return sufix;
}
public void setSufix(String sufix) {
this.sufix = sufix;
}
}
HelloServiceAutoConfiguration
用于自动装配
- @EnableConfigurationProperties:获取HelloProperties中的配置
- @ConditionalOnMissingBean:由于要自动配置HelloService,所以首先要判断环境中是否有该类。
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
@ConditionalOnMissingBean(HelloService.class)
@Bean
public HelloService helloService(){
HelloService helloService = new HelloService();
return helloService;
}
}
除此外需要配置一下Springboot在启动时的自动配置,放在resources/META-INF/spring.factories下
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sentiment.hello.auto.HelloServiceAutoConfiguration
starter
starter
主要充当启动器使用
只需要将start-autoconfigure
引入到该类中即可,无需进行其他配置
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>sentiment-hello-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
starter-test
在新建一个项目,用作starter测试,引入xml文件
<dependency>
<groupId>org.example</groupId>
<artifactId>sentiment-hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
application.properties
person.hello.sufix = 123
person.hello.prefix = 345
HelloController
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@GetMapping("/hello")
public String sayHello(){
return helloService.sayHello("Sentiment");
}
}
访问/hello
后记
整个过程学习周期比预期时间长了很多,可能是因为内容的相对枯燥,全文很多地方都在进行原理分析,相对体验会差一些,但内容的质量上真的无可挑剔,从入门配置、装配原理、web开发、各组件利用、各数据库访问到最后的自定义配置,每一部分都是可圈可点,这也让我对开发有了一些理解,更好地着手于代码审计。最后贴上尚硅谷的视频链接,致敬!
参考链接:SpringBoot2核心技术与响应式编程 (yuque.com)