本文章主要记录个人使用Sparingboot,仅供参考
springboot官网:https://spring.io/projects/spring-boot
springboot中文网:http://springboot.fun/
项目搭建:
创建项目:
官网创建springboot项目
进入https://spring.io/projects
当然也可以直接进入https://start.spring.io/
然后下载下来的是一个压缩包,我们解压就得到了一个springboot项目
使用idea打开(open进去)
使用idea创建springboot项目
可以看到其实默认也是使用的springboot的server url
这里我们勾选一个web,maven会自动导入web依赖
文件夹规范
dao、controller、service等只能创建于Application同级目录下
启动横幅广告可以在resource目录下创建一个banner.txt,(判断依据:标识变为spring标识)
自行百度一些springbanner就课看到很多好看的banner,然后直接放里面就自定义了
配置文件:
application.yml
语句结构:key:空格value
application.properties
语句结构:key=value
静态资源:resource目录下
底层:
所以配置文件这三种文件都行(个人感觉yaml最轻巧)
但是yaml可以实现初始化对象
像这样
官方提示我们添加一个pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.6.0</version>
</dependency>
然后就不会报错误提示了并且可以直接进入到我们的yaml文件中
默认相关
静态资源相关
静态资源目录
/static or /public or /resource or /MATE-INF/resource
定制配置:
spring:
mvc:
static-path-pattern:
注意:定制之后不能使用欢迎页和favicon(可以处理一个/index作为欢迎页)
欢迎页
1.静态资源下的index.xml
2./index controller
favicon
静态资源路径下的favicon.ico
请求参数处理
rest风格请求
说明
传统:
/user/getById?id=1
/user/saveUser
REST:
/user/1
/user
get、post请求不变
delete请求
要求:
1.表单提交的时候method=post
2添加一个隐藏属性<input name="_methon" value="DELETE" type="hidden">
3.在spring配置中设置(默认为false)
spring:
mvc:
hiddenmethod:
filter:
enabled: true
put请求
参照delete
案例
@Controller
public class UserController {
@RequestMapping(value = "/users",method = RequestMethod.POST)
@ResponseBody
public String save(){
return "{'module':'user save'}";
}
@RequestMapping(value = "/users/{id}",method = RequestMethod.DELETE)
@ResponseBody
public String delete(@PathVariable Integer id){
System.out.println("user delete"+id);
return "{'module':'user delete'}";
}
@RequestMapping(value = "/users",method = RequestMethod.PUT)
@ResponseBody
public String update(@RequestBody User user){
System.out.println("user update"+user);
return "{'module':'user update'}";
}
@RequestMapping(value = "/users/{id}",method = RequestMethod.GET)
@ResponseBody
public String getById(@PathVariable Integer id){
System.out.println("user getById"+id);
return "{'module':'user getById'}";
}
@RequestMapping(value = "/users",method = RequestMethod.GET)
@ResponseBody
public String getAll(){
System.out.println("user getAll");
return "{'module':'user getAll'}";
}
}
说明:
@RequestParam用于接收url地址传参或表单传参
@ResponseBody用于接收json数据
@PathVariable用于接受路径参数,使用{参数名称}描述路径参数
简化写法
案例:
@RestController
//@Controller
//@ResponseBody
@RequestMapping("/books")
public class BookController {
@RequestMapping(method = RequestMethod.POST)
public String save(){
return "{'module':'book save'}";
}
@RequestMapping(method = RequestMethod.DELETE)
public String delete(@PathVariable Integer id){
System.out.println("book delete"+id);
return "{'module':'book delete'}";
}
@RequestMapping(method = RequestMethod.PUT)
public String update(@RequestBody Book book){
System.out.println("book update"+book);
return "{'module':'book update'}";
}
@RequestMapping(method = RequestMethod.GET)
public String getById(@PathVariable Integer id){
System.out.println("book getById"+id);
return "{'module':'book getById'}";
}
@RequestMapping(method = RequestMethod.GET)
public String getAll(){
System.out.println("book getAll");
return "{'module':'book getAll'}";
}
}
说明:
@Controller和@ResponseBody→@RestController,也就是@RestController可以代替@Controller和@ResponseBody
继续优化
@RestController
//@Controller
//@ResponseBody
@RequestMapping("/books")
public class BookController {
@PostMapping
public String save(){
return "{'module':'book save'}";
}
@DeleteMapping
public String delete(@PathVariable Integer id){
System.out.println("book delete"+id);
return "{'module':'book delete'}";
}
@PutMapping
public String update(@RequestBody Book book){
System.out.println("book update"+book);
return "{'module':'book update'}";
}
@GetMapping("/{id}")
public String getById(@PathVariable Integer id){
System.out.println("book getById"+id);
return "{'module':'book getById'}";
}
@GetMapping
public String getAll(){
System.out.println("book getAll");
return "{'module':'book getAll'}";
}
}
说明:
@RequestMapping(method = RequestMethod.POST→@PostMapping
…
配置相关
位置:
说明:
默认的配置文件为application.properties
可以配置哪些呢?
可以在官网找到
注意:只有pom中使用了相关的东西如mysql,redis,才能使用相关的配置
配置写法优化
说明:boot的默认是以上的方式,其实还有其他的方式配置
application.yml
application.yaml
主要区别:
application.properties
server.port=80
application.yml
server:
port: 81
application.yaml
server:
port: 82
主意:yaml语法规则
①大小写敏感
②属性层级关系使用多行描述,每行结尾使用冒号结束
③使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用tab键)
④属性值前面添加空格
⑤# 表示注释→不能有中文不然报错org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length = 1
格式相关的案例:
a:
b:
c:
d:
c: 123
#数组
likes:
- games
- music
- sleep
likes2: [games, music, sleep]
#对象
users:
- name: zhangshan
age: 18
- name: lisi
age: 20
users2: [{name: zhangshan, age: 18}, {name: lisi, age: 20}]
怎么调用配置文件中的值
现在配置文件中写了这些
country: China
province: beijing
city: beijing
area: haidian
#数组
likes:
- games
- music
- sleep
likes2: [games, music, sleep]
#对象
users:
- name: zhangshan
age: 18
- name: lisi
age: 20
users2: [{name: zhangshan, age: 18}, {name: lisi, age: 20}]
在controller中调用
@RestController
@RequestMapping("/book")
public class BookController {
@Value("${country}")
private String country1;
@Value("${user.name}")
private String name;
@Value("{likes[0]}")
private String like1;
@Value("{users[1].age}")
private String age1;
特殊使用
引用
baseDir: c:\win10
temDir: ${baseDir}\temp
需要使用转义(使用""包裹的字符串,其中的转义字符可以生效)
path: "\temp \n3"
加载全部的数据
//import org.springframework.core.env.Environment;
@Autowired
private Environment env;
@GetMapping
public String getById(){
System.out.println("springboot is running......");
System.out.println(env.getProperty("country"));
return "springboot is running......";
}
主流封装方式
yaml:
mydatasource:
driver: coc.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
bean
@Component
@ConfigurationProperties("mydatasouce")
public class MydataSource {
private String driver;
private String url;
private String username;
private String password;
@Override
public String toString() {
return "MydataSource{" +
"driver='" + driver + '\'' +
", url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public MydataSource(String driver, String url, String username, String password) {
this.driver = driver;
this.url = url;
this.username = username;
this.password = password;
}
public MydataSource() {
}
}
当多个配置文件共存的时候谁生效呢?
application.properties > application.yml > application.yaml
注意:多个配置文件存在的时候共存叠加并相互覆盖
解决在idea中springboot,yml中不弹提示
现象说明:idea认为yml或者yaml是一个配置文件
解决方式:(让idea知道这些文件是配置文件)
**注意:**当配置文件的名字不是application时,选择文件后可能不能点击ok,只需将配置文件的名字填写到如下位置便可解决
整合第三方技术
整合junit
@SpringBootTest
class Springbootdom002ApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
bookDao.save();
}
}
当test类不在引导类相同或者子目录下时(指定spring容器是哪一个)
整合mybatis
新建项目的时候勾选需要的功能
配置连接
url: jdbc:mysql://localhost:3306/zjjlt?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
创建实体类
创建dao
test
mybatis-plus整合
✓上mysqldriver
手动加入mybatispuls
maven repository
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
注意:
上述已经导入了spring-boot-starter故可将其删掉
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
接下来的yml配置,pojo等与mybatis一致
主要区别是dao的写法
@Mapper
public interface UserDao extends BaseMapper<User> {
}
注意:当数据库的表名或者字段有_时需要配置到yml
搜索关键字mp解决下划线
只要解决思路就是屏蔽驼峰命名,然后实体类与字段一样就解决了
以下是屏蔽配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
整合Druid
首先选择使用的连接池
在mvnrepository中找到druid-spring-boot-starter
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
再导入mybatis相关配置
将jdbc连接改为以下方式
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/zjjlt?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: root
password: 623642
总结:
springboot整合第三方先找到对应的starter
根据第三方的配置方式进行配置
综合使用
dom1:使用mybatis plus、lombok、elementui做一个快速开发的案例
实现功能:
①列表、新增、修改、删除、分页、查询
②异常处理
③按条件查询
创建数据库
CREATE TABLE book(
id int not null PRIMARY key AUTO_INCREMENT,
type VARCHAR(25) not null,
name VARCHAR(50) not null,
description VARCHAR(50) not null
)CHARSET=utf8;
使用lombok
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
实体类(lombk方式)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private long id;
private String type;
private String name;
private String description;
}
dao(mybatis plus方式)
@Mapper
public interface BookDao extends BaseMapper<Book> {
}
使用mybatis plus
@SpringBootTest
class Springbootdom008SsmpApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void testGetById() {
bookDao.selectById(1);
}
@Test
void testSave() {
Book book = new Book();
book.setName("Java");
book.setType("编程语言");
book.setDescription("Java是一门面向对象的编程语言");
bookDao.insert(book);
}
@Test
void testUpdate() {
Book book = new Book();
book.setId(2);
book.setName("Java002");
book.setType("编程语言002");
book.setDescription("Java是一门面向对象的编程语言002");
bookDao.updateById(book);
}
@Test
void testDelete() {
bookDao.deleteById(2);
}
@Test
void testGetAll() {
bookDao.selectList(null);
}
@Test
void testGetPage() {
IPage<Book> page = new Page<>(1, 5);
bookDao.selectPage(page, null);
System.out.println(page.getRecords());
}
@Test
void testGetby() {
QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
queryWrapper.like("name", "java002");
bookDao.selectList(queryWrapper);
}
@Test
void testGetby2() {
String name = null;
LambdaQueryWrapper<Book> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name != null, Book::getName, name);
bookDao.selectList(queryWrapper);
}
}
说明:
条件查询相关
QueryWrapper的属性值是人为加入的,编程工具无法为我们排查错误
LambdaQueryWrapper解决了这个问题,但是在处理传入的值为null时,会匹配为%null%,为此
LambdaQueryWrapper又增加了一个带三个参数的方法,当第一个参数为ture时才会使用相关操作,否则直接无条件查询
service
接口
public interface BookService {
/**
* @Description: 保存
* @version v1.0
* @author TianXinKun
*
*/
Boolean save(Book book);
/**
* @Description: 更新
* @version v1.0
* @author TianXinKun
*
*/
Boolean upadte(Book book);
/**
* @Description: 删除
* @version v1.0
* @author TianXinKun
*
*/
Boolean delete(Integer id);
/**
* @Description: 根据id查询
* @version v1.0
* @author TianXinKun
*
*/
Book getById(Integer id);
/**
* @Description: 查询所有
* @version v1.0
* @author TianXinKun
*
*/
List<Book> getAll();
/**
* @Description: getPage
* @version v1.0
* @author TianXinKun
*
*/
IPage<Book> getPage(int pageNum, int pageSize);
}
实现类
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public Boolean save(Book book) {
return bookDao.insert(book) > 0;
}
@Override
public Boolean upadte(Book book) {
return bookDao.updateById(book) > 0;
}
@Override
public Boolean delete(Integer id) {
return bookDao.deleteById(id) > 0;
}
@Override
public Book getById(Integer id) {
return bookDao.selectById(id);
}
@Override
public List<Book> getAll() {
return bookDao.selectList(null);
}
@Override
public IPage<Book> getPage(int pageNum, int pageSize) {
Page<Book> page = new Page<>(pageNum, pageSize);
return bookDao.selectPage(page, null);
}
}
test类
@SpringBootTest
public class BookServiceTestCase {
@Autowired
private BookService bookService;
@Test
void testGetById(){
System.out.println(bookService.getById(2));
}
@Test
void testSave() {
Book book = new Book();
book.setName("Java");
book.setType("编程语言");
book.setDescription("Java是一门面向对象的编程语言");
System.out.println(bookService.save(book));
}
@Test
void testUpdate() {
Book book = new Book();
book.setId(2);
book.setName("Java002");
book.setType("编程语言002");
book.setDescription("Java是一门面向对象的编程语言002");
System.out.println(bookService.upadte(book));
}
@Test
void testDelete() {
System.out.println(bookService.delete(3));
}
@Test
void testGetAll() {
System.out.println(bookService.getAll());
}
@Test
void testGetPage() {
IPage<Book> page = bookService.getPage(1, 5);
System.out.println(page.getRecords());
System.out.println(page.getPages());
System.out.println(page.getCurrent());
System.out.println(page.getSize());
System.out.println(page.getTotal());
}
}
简化service操作
service接口
public interface IBooksService extends IService<Book> {
}
service实现类
@Service
public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBooksService {
}
test类
@SpringBootTest
public class BookServiceTest {
@Autowired
private IBooksService bookService;
@Test
void testGetById(){
System.out.println(bookService.getById(2));
}
@Test
void testSave() {
Book book = new Book();
book.setName("Java");
book.setType("编程语言");
book.setDescription("Java是一门面向对象的编程语言");
System.out.println(bookService.save(book));
}
@Test
void testUpdate() {
Book book = new Book();
book.setId(2);
book.setName("Java002");
book.setType("编程语言002");
book.setDescription("Java是一门面向对象的编程语言002");
System.out.println(bookService.updateById(book));
}
@Test
void testDelete() {
System.out.println(bookService.removeById(3));
}
@Test
void testGetAll() {
System.out.println(bookService.list());
}
@Test
void testGetPage() {
Page<Book> page = new Page<>(1,5);
bookService.page(page);
System.out.println(page.getRecords());
System.out.println(page.getPages());
System.out.println(page.getCurrent());
System.out.println(page.getSize());
System.out.println(page.getTotal());
}
}
注意:
当mp里面的实现类不能满足我们的需求时只需要将service接口加上自定义的抽象方法然后在实现类中实现,需要注意的是不要与mp已有的方法名重复,避免原始提供的功能缺失。
controller
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private IBooksService booksService;
@GetMapping
public List<Book> getAll(){
return booksService.list();
}
@PostMapping
public boolean save(@RequestBody Book book){
return booksService.save(book);
}
@PutMapping
public boolean update(@RequestBody Book book){
return booksService.updateById(book);
}
@DeleteMapping("/{id}")
public boolean delete(@PathVariable("id") Integer id){
return booksService.removeById(id);
}
@GetMapping("/{id}")
public Book getById(@PathVariable("id") Integer id){
return booksService.getById(id);
}
@GetMapping("{page}/{size}")
public IPage<Book> getPage(@PathVariable int page,@PathVariable int size){
return booksService.getPage(page,size);
}
}
注意,以上接口出来的格式许多种
编写一个处理前后端协议的类
@Data
public class R {
private Boolean flag;
private Object data;
public R() {
}
public R(Boolean flag, Object data) {
this.flag = flag;
this.data = data;
}
public R(Boolean flag) {
this.flag = flag;
}
}
更改controller
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private IBooksService booksService;
@GetMapping
public R getAll(){
return new R(true,booksService.list());
}
@PostMapping
public R save(@RequestBody Book book){
return new R(booksService.save(book));
}
@PutMapping
public R update(@RequestBody Book book){
return new R(booksService.updateById(book));
}
@DeleteMapping("/{id}")
public R delete(@PathVariable("id") Integer id){
return new R(booksService.removeById(id));
}
@GetMapping("/{id}")
public R getById(@PathVariable("id") Integer id){
return new R(true,booksService.getById(id));
}
@GetMapping("{page}/{size}")
public R getPage(@PathVariable int page,@PathVariable int size){
return new R(true,booksService.getPage(page,size));
}
}
html代码
<!DOCTYPE html>
<html>
<head>
<!-- 页面meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>基于SpringBoot整合SSM案例</title>
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
<!-- 引入样式 -->
<link rel="stylesheet" href="../plugins/elementui/index.css">
<link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="../css/style.css">
</head>
<body class="hold-transition">
<div id="app">
<div class="content-header">
<h1>图书管理</h1>
</div>
<div class="app-container">
<div class="box">
<div class="filter-container">
<el-input v-model="pagination.type" placeholder="图书类别" style="width: 200px;" class="filter-item"></el-input>
<el-input v-model="pagination.name" placeholder="图书名称" style="width: 200px;" class="filter-item"></el-input>
<el-input v-model="pagination.description" placeholder="图书描述" style="width: 200px;" class="filter-item"></el-input>
<el-button @click="getAll()" class="dalfBut">查询</el-button>
<el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>
</div>
<el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row>
<el-table-column type="index" align="center" label="序号"></el-table-column>
<el-table-column prop="type" label="图书类别" align="center"></el-table-column>
<el-table-column prop="name" label="图书名称" align="center"></el-table-column>
<el-table-column prop="description" label="描述" align="center"></el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="handleUpdate(scope.row)">编辑</el-button>
<el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<div class="pagination-container">
<el-pagination
class="pagiantion"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
layout="total, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</div>
<!-- 新增标签弹层 -->
<div class="add-form">
<el-dialog title="新增图书" :visible.sync="dialogFormVisible">
<el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="图书类别" prop="type">
<el-input v-model="formData.type"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="图书名称" prop="name">
<el-input v-model="formData.name"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="formData.description" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel()">取消</el-button>
<el-button type="primary" @click="handleAdd()">确定</el-button>
</div>
</el-dialog>
</div>
<!-- 编辑标签弹层 -->
<div class="add-form">
<el-dialog title="编辑检查项" :visible.sync="dialogFormVisible4Edit">
<el-form ref="dataEditForm" :model="formData" :rules="rules" label-position="right" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="图书类别" prop="type">
<el-input v-model="formData.type"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="图书名称" prop="name">
<el-input v-model="formData.name"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="formData.description" type="textarea"></el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel()">取消</el-button>
<el-button type="primary" @click="handleEdit()">确定</el-button>
</div>
</el-dialog>
</div>
</div>
</div>
</div>
</body>
<!-- 引入组件库 -->
<script src="../js/vue.js"></script>
<script src="../plugins/elementui/index.js"></script>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script src="../js/axios-0.18.0.js"></script>
<script>
var vue = new Vue({
el: '#app',
data:{
dataList: [],//当前页要展示的列表数据
dialogFormVisible: false,//添加表单是否可见
dialogFormVisible4Edit:false,//编辑表单是否可见
formData: {},//表单数据
rules: {//校验规则
type: [{ required: true, message: '图书类别为必填项', trigger: 'blur' }],
name: [{ required: true, message: '图书名称为必填项', trigger: 'blur' }]
},
pagination: {//分页相关模型数据
currentPage: 1,//当前页码
pageSize:8,//每页显示的记录数
total:0,//总记录数
type: "",
name: "",
description: ""
}
},
//钩子函数,VUE对象初始化完成后自动执行
created() {
this.getAll();
},
methods: {
//列表
/*getAll() {
// 发送异步请求
axios.get("/books").then(res => {
// console.log(res.data);
this.dataList = res.data.data;
});
},*/
//分页查询
getAll() {
// 组织参数,拼接url请求地址
var paramStr = "?name=" + this.pagination.name
+ "&type=" + this.pagination.type
+ "&description=" + this.pagination.description;
console.log(paramStr);
axios.get("/books/" + this.pagination.currentPage + "/" + this.pagination.pageSize + paramStr).then(res => {
// console.log(res.data);
this.pagination.currentPage = res.data.data.current;
this.pagination.pageSize = res.data.data.size;
this.pagination.total = res.data.data.total;
this.dataList = res.data.data.records;
});
},
//切换页码
handleCurrentChange(currentPage) {
// 修改页码值为当前选中的页码值
this.pagination.currentPage = currentPage;
// 执行查询
this.getAll();
},
//弹出添加窗口
handleCreate() {
this.dialogFormVisible = true;
this.resetForm();
},
//重置表单
resetForm() {
this.formData = {};
},
//添加
handleAdd () { axios.post("/books", this.formData).then(res => {
// 判断当前操作是否成功
if(res.data.flag) {
// 1.关闭弹层
this.dialogFormVisible = false;
this.$message.success(res.data.msg);
} else {
this.$message.error(res.data.msg);
}
}).finally(() => {
// 2.重新加载数据
this.getAll();
});
},
//取消
cancel(){
this.dialogFormVisible = false;
this.dialogFormVisible4Edit = false;
this.$message.info("当前操作取消!");
},
// 删除
handleDelete(row) {
// console.log(row);
this.$confirm("删除的数据将不可恢复,是否继续?", "提示", {type: "info"}).then(() => {
axios.delete("/books/" + row.id).then(res => {
if (res.data.flag) {
this.$message.success(res.data.msg);
} else {
this.$message.error(res.data.msg);
}
}).finally(() => {
this.getAll();
});
}).catch(() => {
this.$message.info("取消操作");
});
},
//弹出编辑窗口
handleUpdate(row) {
// 发送异步请求
axios.get("/books/" + row.id).then(res => {
// console.log(res.data);
if (res.data.flag && res.data.data != null) {
this.dialogFormVisible4Edit = true;
this.formData = res.data.data;
} else {
this.$message.error(res.data.msg);
}
}).finally(() => {
this.getAll();
});
},
//修改
handleEdit() {
axios.put("/books", this.formData).then(res => {
// 判断当前操作是否成功
if(res.data.flag) {
// 1.关闭弹层
this.dialogFormVisible4Edit = false;
this.$message.success(res.data.msg);
} else {
this.$message.error(res.data.msg);
}
}).finally(() => {
// 2.重新加载数据
this.getAll();
});
},
//条件查询
}
})
</script>
</html>
异常处理器
更改数据协议类
@Data
public class R {
private Boolean flag;
private Object data;
private String msg;
public R( String msg) {
this.flag = false;
this.msg = msg;
}
public R() {
}
public R(Boolean flag, Object data) {
this.flag = flag;
this.data = data;
}
public R(Boolean flag) {
this.flag = flag;
}
}
异常处理器
@RestControllerAdvice
public class ProjectExceptionAdvice {
@ExceptionHandler(Exception.class)
public R doException(Exception e){
//打印异常信息
e.printStackTrace();
return new R("服务器异常,请稍后再试");
}
}
说明:
@ExceptionHandler(Exception.class)可以指定拦截的异常类型,注意,一定打印异常!!!
运维相关
配置文件相关
配置位置
在开发中
运行的配置文件是resouces目录下的
当打包后
运行jar包时是使用的config目录下的配置文件
当像如下一样将配置文件放置和jar同级目录下时,放置的配置文件生效
此时:在jar同级目录下新建config在其中的配置文件,那么首先生效的是其中的文件
在配置文件中设置区分配置
# 应用环境
spring:
profiles:
active: dev
---
# 生产环境
spring:
config:
activate:
on-profile: pro
---
# 开发环境
spring:
config:
activate:
on-profile: dev
---
# 测试环境
spring:
config:
activate:
on-profile: test
将多环境配置文件分开
说明,文件名-后面的名称即是你active设置的名称,当然properties也可以使用多文件方式,但是不支持一个配置文件多个环境方式
使用某个环境且包含其他环境:
# 应用环境
spring:
profiles:
active: dev
include: devDB devMVC
说明:
其中配置生效的顺序是:include中越后面优先级越大,也就是后加载的配置文件生效,最后加载active的配置
进阶版:
# 应用环境
spring:
profiles:
active: dev
group:
"dev": devDB,devMQ
"pro": proDB,proMQ
注意:使用这种方式active的配置是第一个加载的,也就是会被后续的配置覆盖,具体怎么查看配置的顺序
启动时:
在maven中设置配置
在pom中添加:
<!-- 设置多环境 -->
<profiles>
<profile>
<id>env_dev</id>
<properties>
<project.active>dev</project.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>env_pro</id>
<properties>
<project.active>pro</project.active>
</properties>
</profile>
</profiles>
应用环境配置更改
# 应用环境
spring:
profiles:
active: @profile.active@
group:
"dev": devDB,devMQ
"pro": proDB,proMQ
其中@profile.active@是配置文件中的<project.active>pro</project.active>
注意:idea中当切换配置的时候不会生效,那么需要手动complie
日志相关
开启日志
controller
@RestController
@RequestMapping("/book")
public class BookController {
private static final Logger log = org.slf4j.LoggerFactory.getLogger(BookController.class);
@GetMapping
public String getById(){
System.out.println("springboot is running......");
log.debug("debug......");
return "springboot is running......";
}
}
配置文件
logging:
level:
root: info
# 设置某个包的日志级别
txk.com.contrller: debug
# 设置分组的日志级别
txk: warn
iservice: error
group:
txk: com.itheima.controller,com.itheima.service,com.itheima.dao
iservice: com.alibaba
优化
日志控制类
public class BaseClass {
private Class clazz;
public static Logger log;
public BaseClass() {
this.clazz = this.getClass();
log = org.slf4j.LoggerFactory.getLogger(clazz);
}
}
controller
@RestController
@RequestMapping("/book")
public class BookController extends BaseClass {
@GetMapping
public String getById(){
System.out.println("springboot is running......");
log.debug("debug......");
return "springboot is running......";
}
}
使用lombak优化
pom
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
controller
@Slf4j
@RestController
@RequestMapping("/book")
public class BookController{
@GetMapping
public String getById(){
System.out.println("springboot is running......");
log.debug("debug......");
return "springboot is running......";
}
}
开发整合
热部署
pom添加开发者工具包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
在更改代码后点击build(Ctrl+F9)
说明:热部署restart时只加载自定义开发的内容,不会重新加载jar资源
优化:
自动build,就不需要手动build了
idea低版本 Ctrl+Shift+Alt+/ 选择registry 勾选compiler.automake.allow.when.app.running
高版本:
说明:idea检测的条件是idea失去焦点几秒后自动构建
注意:默认在resource目录下有部分目录是不参与热部署的,如果有特殊需求,可以使用以下方式修改排除项
spring:
devtools:
restart:
exclude: static/**,public/**
关闭热部署
@SpringBootApplication
public class Springbootdom002Application {
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(Springbootdom002Application.class, args);
}
}
第三方bean
松散绑定
以下是一个自定义bean
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServersConfig {
private String ipAddress;
private String port;
private String timeout;
}
而我们的配置文件可以这样写
servers:
# ipaddress: 192.168.0.1
ip-address: 192.168.0.1
# ip_ddress: 192.168.0.1
# IPADDRESS: 192.168.0.1
# IP-ADDRESS: 192.168.0.1
# IP-A_D-D_RESS: 192.168.0.1
port: 2345
timeout: -1
说明:spring为了吸引各行人员的参与,将bean的绑定忽略了大小写和-_,简称“松散绑定”
注意:这种模式不支持@Value引用单个属性
Duration的使用
前提:在设置时间相关的数值时模糊不清,搞不清状况
如下timeout的设置
servers:
# ipaddress: 192.168.0.1
ip-address: 192.168.0.1
# ip_ddress: 192.168.0.1
# IPADDRESS: 192.168.0.1
# IP-ADDRESS: 192.168.0.1
# IP-A_D-D_RESS: 192.168.0.1
port: 2345
timeout: 30
阅读性不强,于是我们可以使用duration类型来处理
先需要一个自定义bean
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServersConfig {
private String ipAddress;
private String port;
private String timeout;
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeout;
}
这样,我们设置的单位就是hours了
当然,有时间就有容量
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServersConfig {
private String ipAddress;
private String port;
private String timeout;
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeout;
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize serverSize;
}
这样的话我们现在设置的单位就是MB了
当然也可以不使用@DataSizeUnit(DataUnit.MEGABYTES)
在配置中加单位
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServersConfig {
private String ipAddress;
private String port;
private String timeout;
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeout;
private DataSize serverSize;
}
servers:
# ipaddress: 192.168.0.1
ip-address: 192.168.0.1
# ip_ddress: 192.168.0.1
# IPADDRESS: 192.168.0.1
# IP-ADDRESS: 192.168.0.1
# IP-A_D-D_RESS: 192.168.0.1
port: 2345
timeout: 30
serverTimeout: 3
serverSize: 10MB
配置文件格式校验
使用JSR303规范进行校验
添加pom
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
在需要校验的bean中添加@Validated
并且在属性添加规则
@Component
@Data
@ConfigurationProperties(prefix = "servers")
@Validated
public class ServersConfig {
private String ipAddress;
@Max(value = 65535,message = "端口号不能大于65535")
@Min(value = 0,message = "端口号不能小于0")
private String port;
private String timeout;
@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeout;
private DataSize serverSize;
}
再加上validator engine就可以了
我们使用hibernate
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
注意:yml中支持2进制,8进制,16进制,那么在有些情况就出现了问题了,
比如,mysql密码为:0123
datasource:
driverclassname: com.mysql.jdbc.Driver
password: 0123
这种就会被识别为8进制(0开头后面接0-7)
这样当将这个值传入的时候就不是我们想的值了
解决方式:
datasource:
driverclassname: com.mysql.jdbc.Driver
password: "0123"
测试相关
在当前测试用例中添加临时属性(只在该测试中有用,并且是最后加载的)
@SpringBootTest(properties = {"test.prop=hellotest"})
public class PropertiesAndArgsTest {
@Value("${test.prop}")
private String msg;
@Test
void testProperties(){
System.out.println(msg);
}
}
@SpringBootTest(args = {"--test.prop=hellotest"})
public class PropertiesAndArgsTest {
@Value("${test.prop}")
private String msg;
@Test
void testProperties() {
System.out.println(msg);
}
}
添加test的外部bean
在test文件下自定义一个bean
在测试类中@Import({MsgCofig.class})
然后就可以自动装配了
@SpringBootTest
@Import({MsgCofig.class})
public class ConfigrationTest {
@Autowired
private String msg;
@Test
void testConfigration(){
System.out.println(msg);
}
}
测试web环境
使用webEnvironment =
WebEnvironment.DEFINED_PORT,使用定义的端口启动web
WebEnvironment.RANDOM_PORT,使用随机端口启动web
WebEnvironment.NONE(默认),不启用web环境
WebEnvironment.MOCK,
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class WebTest {
@Test
void testWeb(){
System.out.println("test web");
}
}
测试请求:(通过状态码配对)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//开启虚拟调用
@AutoConfigureMockMvc
public class WebTest {
@Test
void testWeb2(@Autowired MockMvc mockMvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
//获取请求结果
ResultActions perform = mockMvc.perform(builder);
//期望返回状态码为200
ResultMatcher ok = MockMvcResultMatchers.status().isOk();
//期望返回的结果与预期比对
perform.andExpect(ok);
}
}
测试请求:执行结果匹配(字符串的匹配)
@Test
void testWeb3(@Autowired MockMvc mockMvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
//获取请求结果
ResultActions perform = mockMvc.perform(builder);
//期望返回的结果是get books
ResultMatcher ok = MockMvcResultMatchers.content().string("get books");
//期望返回的结果与预期比对
perform.andExpect(ok);
}
测试请求:执行结果匹配(json匹配)
@Test
void testWeb4(@Autowired MockMvc mockMvc) throws Exception {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");
//获取请求结果
ResultActions perform = mockMvc.perform(builder);
//期望返回的结果是get books
ResultMatcher ok = MockMvcResultMatchers.content().json("{\"name\":\"三国演义\",\"id\":1,\"description\":\"一个杀伐纷争的年代\",\"type\":\"小说\"}");
//期望返回的结果与预期比对
perform.andExpect(ok);
}
其他结果匹配同理
设置测试执行不留下数据
原理:将事物关闭
@Transactional
@SpringBootTest
@Transactional
public class DaoTest {
}
这样测试的时候数据库的数据不会被更改,而测试仍然进行,但是数据库的
设置测试数据为随机值
在test配置中生成随机值
testcase:
book:
id: ${random.int}
name: ${random.String}
uuid: ${random.uuid}
publishTime: ${random.date}
在自定义bean中装配
@Data
@Component
@ConfigurationProperties(prefix = "testcase.book")
public class BookCase {
private int id;
private String name;
private String uuid;
private String publishTime;
}
然后测试的时候就是随机值
@SpringBootTest
public class BookCaseTest {
@Autowired
private BookCase bookCase;
@Test
void testBookCase(){
System.out.println(bookCase);
}
}
当然是可以指定范围
testcase:
book:
id: ${random.int}
type: ${random.int(1, 10)}
name: test${random.String}
uuid: ${random.uuid}
publishTime: ${random.date}
其中(1, 10)可以写成[1,10],@1,10@等等都可以
数据层相关
数据源
hikariDataSource(默认使用)
Tomcat提供Datasource
Commons DBCP
持久化技术
JdbcTemplate(默认使用)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
使用方式(给一个最复杂的查询的案例)
@Test
void testJdbcTemplate(Autowired JdbcTemplate jdbcTemplate){
String sql = "select * from book";
RowMapper<Book> rm = new RowMapper<Book>() {
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
Book book = new Book();
book.setId(rs.getInt("id"));
book.setName(rs.getString("name"));
book.setDescription(rs.getString("description"));
book.setType(rs.getString("type"));
return book;
}
};
List<Book> list = jdbcTemplate.query(sql,rm);
System.out.println(list);
}
sql数据库(spring内置了三款数据库,都是java编写,都比较轻巧)
H2
HSQL
DerBy
使用H2
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
配置操作数据库
spring:
h2:
console:
# 是否启用H2控制台
enabled: true
# H2控制台访问路径
path: /h2
注意:当第一次使用时设置密码点击连接后出现以下报错
将springboot重启再登陆即可
将h2的连接信息写入配置
spring:
h2:
console:
# 是否启用H2控制台
enabled: true
# H2控制台访问路径
path: /h2
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:~/test
username: sa
password: 123456
nosql数据库
redis整合
原理相关
自动配置
bean的加载方式
xml定义方式
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"></property>
<property name="prefix" value="/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
注解方式
@Component("cat")
public class Cat {
}
在需要springboot管理的类加上即可
但是需要配置组件扫描
同时这种方式加载第三方bean可以使用以下方式:
//@Component
@Configuration
public class DbConfig {
@Bean
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
}
以上两个都是一样的效果(@Configuration指定当前类为一个配置类)
区别:@Configuration多一个参数proxyBeanMethods,默认为true,即启用代理对象:即当spring中有这个对象时就不会重新new一个对象、相反则需要一个对象就new一个。
当然也需要将这个类加入到组件扫描里面去(xml方式):
<!--初始化contextClass-->
<context:component-scan base-package="txk.com.*"></context:component-scan>
配置类(将配置文件变为配置类)
@ComponentScan({"txk.com.bean","txk.com.config"})
public class SpringConfig35 {
}
说明:这个就相当于上面的配置类
同时加载的方式更改为:
public class App1 {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig35.class);
}
}
或者
@Configuration
@ComponentScan({"txk.com.bean","txk.com.config"})
public class SpringConfig35 {
}
工厂模式(工厂bean)
工厂类:
public class DogFactoyBean implements FactoryBean<Dog>{
@Override
public Dog getObject() throws Exception {
return new Dog();
}
@Override
public Class<?> getObjectType() {
return Dog.class;
}
@Override
public boolean isSingleton() {
return true;
}
}
说明:
getObject():造出来的对象是什么
getObjectType():是个什么类型
isSingleton:造出的对象是否单例
加载bean
@ComponentScan({"txk.com.bean","txk.com.config"})
public class SpringConfig3 {
@Bean
public DogFactoyBean dog(){
return new DogFactoyBean();
}
}
说明:这种方式spring加载出来的对象不是DogFactoyBean而是FactoryBean中的泛型对象
这种方式可以在对象创建时做一系列的初始化工作比如:
@Override
public Dog getObject() throws Exception {
Dog dog = new Dog();
//初始化操作
return dog;
}
加载配置类文件
说明:升级系统需要使用的是注解方式、而又需要加载老系统的配置文件
@ImportResource("classpath:txk/com/config/beans.xml")
public class SpringConfig32 {
}
@Import
@Import(txk.com.bean.Dog.class)
public class SpringConfig34 {
}
说明:不需要对原始实体类进行修改(解耦、非侵入式),生成出来的 对象是带全路径名称的,不是定义的方法名或者类名,也可以加载配置类,并且会将配置类中的配置类也会被加载
在上下文容器对象已经初始化后手工加载
public class App1 {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig34.class);
//在上下文容器对象已经初始化后手工加载
ctx.registerBean("dog",txk.com.bean.Dog.class);
}
}
这种方式只能使用AnnotationConfigApplicationContext方式,也就是配置类方式
说明写了多个一样的,不会冲突,留下来的是最后加载的留下来了。这样,就可以更改已加载的bean的值了
@Import导入ImportSelector方式
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{"txk.com.bean.Dog"};
}
}
说明:
importingClassMetadata里面有很多方法其中:
getXXX获取什么
isXXX获取一个状态
hasXXX有没有。。
而importingClassMetadata就是描述创建类的
这样的话我们可以根据是否存在某个配置类决定配置是否添加某个配置,像以下一样:
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
boolean flag = importingClassMetadata.hasMetaAnnotation("import org.springframework.context.annotation.Configuration");
if (flag) {
return new String[]{"txk.com.bean.Cat"};
}
return new String[]{"txk.com.bean.Dog"};
}
}
简而言之:可以做动态的配置了
@Import导入ImportBeanDefinitionRegistrar方式
相对于ImportSelector,多了一个BeanDefinitionRegistry
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();
//BeanDefinition操作
registry.registerBeanDefinition("dog",beanDefinition);
}
}
说明:BeanDefinitionRegistry主要针对bean的管理的,同理,同一个后面的覆盖
@Import导入BeanDefinitionRegistryPostProcessor方式
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
说明:postProcessBeanDefinitionRegistry和registerBeanDefinitions的区别:主要解决同一个类定义的冲突问题,多人开发中同一个类被多个registerBeanDefinitions加载(在@Import配置在最后的),那么生效的只有最后一个加载的,而postProcessBeanDefinitionRegistry是在bean定义完成之后再加载的,相当于最后加载,当然上诉的在上下文容器对象已经初始化后手工加载肯定是最后加载的
bean的加载控制
适用的方式
①在上下文容器对象已经初始化后手工加载
②@Import导入ImportSelector方式
③@Import导入ImportBeanDefinitionRegistrar方式
④@Import导入BeanDefinitionRegistryPostProcessor方式
如果存在某个bean则加载某个bean(传统或者太繁琐?)
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
try {
Class<?> clazz = Class.forName("txk.com.bean.Dog");
if(clazz != null){
return new String[]{"txk.com.bean.Cat"};
}
} catch (ClassNotFoundException e) {
return new String[0];
}
return null;
}
}
@Conditional()的派生注解
先写一个@Conditional(),然后Ctrl+h
说明:@Conditional()是需要AnnotatedTypeMetadata的,当然,需要导入其对应的包,但是派生注解不需要,以下是一些例子
如果有某个类就加载
public class DbConfig {
@Bean
@ConditionalOnClass(Dog.class)
public Cat tom() {
return new Cat();
}
}
如果没有某个类就加载
@Bean
@ConditionalOnMissingClass("txk.com.bean.Test")
public Dog dog() {
return new Dog();
}
如果有某个bean就加载
@Bean
@ConditionalOnBean(name = "txk.com.bean.Cat")
public Dog dog() {
return new Dog();
}
当然可以多条件
@Bean
@ConditionalOnBean(name = "txk.com.bean.Cat")
@ConditionalOnMissingClass("test")
public Dog dog() {
return new Dog();
}
说明:
注解方式在其他加载也可以使用,像下面这样
@Component("cat")
@ConditionalOnBean(name = "txk.com.bean.Dog")
@ConditionalOnMissingClass("test")
public class Cat {
}
如果在Properties定义了类的属性值就使用、没有就使用默认的值
首先需要两个类老鼠和猫、并且再写一个装配老鼠和猫的类
@Component
@Data
public class Cat {
String name;
Integer age;
}
@Component
@Data
public class Mouse {
String name;
Integer age;
}
@Data
@ConfigurationProperties(prefix = "cartoon")
public class CartoonProperties {
private Cat cat;
private Mouse mouse;
}
然后就可以实现我们的需求了
@Component
@Data
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse {
private Cat cat;
private Mouse mouse;
private CartoonProperties cartoonProperties;
public CartoonCatAndMouse(CartoonProperties cartoonProperties){
this.cartoonProperties=cartoonProperties;
cat = new Cat();
cat.setAge(cartoonProperties.getCat()!=null && cartoonProperties.getCat().getAge()!=null?cartoonProperties.getCat().getAge():1);
cat.setName(cartoonProperties.getCat()!=null && StringUtils.hasText(cartoonProperties.getCat().getName()) ? cartoonProperties.getCat().getName():"Tom");
mouse = new Mouse();
mouse.setAge(cartoonProperties.getMouse()!=null && cartoonProperties.getMouse().getAge()!=null?cartoonProperties.getMouse().getAge():1);
mouse.setName(cartoonProperties.getMouse()!=null && StringUtils.hasText(cartoonProperties.getMouse().getName()) ? cartoonProperties.getMouse().getName():"Jerry");
}
}
总结:业务bean避免强制加载,比如使用@EnableConfigurationProperties(CartoonProperties.class)等等
源码的自动配置
前言
1.收集spring开发者的编程习惯,整理开发过程使用的常用技术列表①
2.收集常用技术的使用参数,整理开发过程中每个技术的常用设置列表②
3.初始化springboot基础环境,加载用户自定义的bean和导入的其他坐标,形成初始化环境
4.将①包含的所有技术都定义出来,在spring/springboot启动时默认全部加载
5.将①中具有使用条件的 技术约定出来,设置成按条件加载,由开发者决定是否使用该技术
6.将②作为默认配置加载(约定大于配置),减少开发者配置工作量
7.开放②的配置覆盖接口,由开发者根据自身需要决定是否覆盖默认配置
源码(具体实现)
@SpringBootApplication
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
以上为基础信息注解,略过
@SpringBootConfiguration
@Configuration:配置类
@Indexed:在springboot运行期加速启动,根据程序生成一个文件(记录bean)在spring启动的时候加载这个文件可以加快启动
@EnableAutoConfiguration
这个注解是自动配置的核心,其中包括
①@AutoConfigurationPackage:
导入了@Import(AutoConfigurationPackages.Registrar.class)
其中Registrar是:
/**
* {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
* configuration.
*/
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
ImportBeanDefinitionRegistrar请看前面介绍,new PackageImports(metadata).getPackageNames().toArray(new String[0])就是获取当前springboot启动程序所在的包名,也就是程序入口的位置,以下是debug获取到的值
然后给到了register,源码如下
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition)registry.getBeanDefinition(BEAN);
beanDefinition.addBasePackages(packageNames);
} else {
registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));
}
}
其中,出现了BasePackagesBeanDefinition源码如下:
BasePackagesBeanDefinition(String... basePackages) {
this.setBeanClass(BasePackages.class);
this.setRole(2);
this.addBasePackages(basePackages);
}
这里将basePackages加入到addBasePackages,也就是将入口所在的包设置为扫描包
②@Import(AutoConfigurationImportSelector.class):
源码中AutoConfigurationImportSelector继承了很多
AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered
其中,Aware结尾的都是资源发现,简而言之就是将springboot有的资源引入到当前类中进行使用比如以下例子:
public class TestAware implements ApplicationContextAware {
private ApplicationContext applicationContext;
public void test(){
String[] beans = applicationContext.getBeanDefinitionNames();
for (String bean : beans) {
System.out.println(bean);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
}
这样我们就可以在需要的地方使用applicationContext了。
而Ordered记录的是加载的顺序
再看ImportSelector:
public interface DeferredImportSelector extends ImportSelector {
@Nullable
default Class<? extends Group> getImportGroup() {
return null;
}
public interface Group {
void process(AnnotationMetadata metadata, DeferredImportSelector selector);
Iterable<Entry> selectImports();
public static class Entry {
private final AnnotationMetadata metadata;
private final String importClassName;
public Entry(AnnotationMetadata metadata, String importClassName) {
this.metadata = metadata;
this.importClassName = importClassName;
}
public AnnotationMetadata getMetadata() {
return this.metadata;
}
public String getImportClassName() {
return this.importClassName;
}
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (other != null && this.getClass() == other.getClass()) {
Entry entry = (Entry)other;
return this.metadata.equals(entry.metadata) && this.importClassName.equals(entry.importClassName);
} else {
return false;
}
}
public int hashCode() {
return this.metadata.hashCode() * 31 + this.importClassName.hashCode();
}
public String toString() {
return this.importClassName;
}
}
}
}
其中:void process(AnnotationMetadata metadata, DeferredImportSelector selector)
我们查看一下AutoConfigurationImportSelector 中是怎么实现的:
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
其中getAutoConfigurationEntry是这样的:
/**
* Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
* of the importing {@link Configuration @Configuration} class.
* @param annotationMetadata the annotation metadata of the configuration class
* @return the auto-configurations that should be imported
*/
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
AnnotationAttributes attributes = getAttributes(annotationMetadata)得到的是开发人员配置的exclude和excludeName(@EnableAutoConfiguration等等)
List configurations = getCandidateConfigurations(annotationMetadata, attributes)得到的是需要使用到的技术,源码如下
/**
* Return the auto-configuration class names that should be considered. By default
* this method will load candidates using {@link SpringFactoriesLoader} with
* {@link #getSpringFactoriesLoaderFactoryClass()}.
* @param metadata the source metadata
* @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
* attributes}
* @return a list of candidate configurations
*/
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
其中:loadFactoryNames是这样的
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
其中:loadSpringFactories
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
Map<String, List<String>> result = new HashMap();
try {
Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
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()) {
Map.Entry<?, ?> entry = (Map.Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
Enumeration urls = classLoader.getResources(“META-INF/spring.factories”):通过类加载器去获得外部资源
这个东西在:
那我们就知道了List configurations = getCandidateConfigurations(annotationMetadata, attributes)的集合就从这个文件中拿出来的,也就是默认加载的技术从这里加载到了内存中
现在我们随便点击一个spring.factories中的技术
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
是一个配置类,存在RedisOperations.class才加载当前类,而这个类是在org.springframework.data.redis.core下的;而有了这个不就是导入了redis嘛!EnableConfigurationProperties不就是配置了就用不配就使用默认的,具体可以查看上面EnableConfigurationProperties的使用@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
@ConditionalOnMissingBean(name = “redisTemplate”):如果系统中没有redisTemplate的bean就加载
@ConditionalOnSingleCandidate(RedisConnectionFactory.class):如果没有提供
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
主要根据指定条件扫描之后哪些东西不需要了
在我们的前言中我们是可以自己开发自己的自动配置
我们知道mybatis plus在2.5.4及其之前是没有加入到spring的默认技术中的,那看mybatis plus怎么做的我们就知道怎么弄了呗
主要看这两个,简单写一个:
这里使用了上面写过的配置类
将现有的去掉
yml配置:
spring:
autoconfigure:
exclude: txk.com.bean.CartoonCatAndMouse
入口:
@SpringBootApplication(excludeName = "txk.com.bean.CartoonCatAndMouse")
自定义starter
我们要编写自己的starter,可以参照官方的写法,可以参照mybatis的。
说明:名字格式:自定义starter名-spring-boot-starter
starter中只有pom等配置文件
autoconfigure中就是主要实现的地方,最基础的需要,spring.factories、application.yml和autoconfiguration类
还有非官方的写法,比较简单的参考druid
相对来说就是将两个文件和二为一了。
简单写一个
创建springboot项目
pom只要导入需要的:
```java
<?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>
<groupId>asia.txk</groupId>
<artifactId>ip_spring_boot_starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<spring-boot.version>2.7.14</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
</dependencies>
</project>
我这里以记录当前访问ip为例:
编写一个service
/**
* Copyright (C), 2023/9/5 16:42
*
* @author 田信坤
* Author: TianXinKun
* Date: 2023/9/5 16:42
*/
package asia.txk.service;
import asia.txk.properties.IpProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* @Description ip统计starter
* @createTime 2023/9/5 16:42
* This program is protected by copyright laws.
* @version : 1.0
*@Author TianXinKun
*/
public class IpCountService {
private Map<String,Integer> ipCountMap = new HashMap<>();
@Autowired
private HttpServletRequest httpServletRequest;
/**
* @Description: 每次调用,记录当前访问ip
* @version v2.0
* @author TianXinKun
*
*/
public void count(){
//获取当前操作的ip地址
String ip = httpServletRequest.getRemoteAddr();
Integer count= ipCountMap.get(ip);
if(count == null){
ipCountMap.put(ip,1);
}else {
ipCountMap.put(ip,ipCountMap.get(ip)+1);
}
}
@Autowired
private IpProperties ipProperties;
//定时任务
@Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?")
public void print(){
if (IpProperties.LogModle.DETAIL.getValue().equals(ipProperties.getModel())){
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+--num--+");
for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(String.format("|%18s |%5d |",key,value));
}
System.out.println("+--------------------+-------+");
}else if (IpProperties.LogModle.SIMPLE.getValue().equals(ipProperties.getModel())){
System.out.println(" IP访问监控");
System.out.println("+-----ip-address-----+");
for (String key:ipCountMap.keySet()) {
System.out.println(String.format("|%18s |",key));
}
System.out.println("+--------------------+");
}else {
System.out.println("+--------------------+");
}
if(ipProperties.getCycleReset()){
ipCountMap.clear();
}
}
}
相关的实体类:
/**
* Copyright (C), 2023/9/5 17:44
*
* @author 田信坤
* Author: TianXinKun
* Date: 2023/9/5 17:44
*/
package asia.txk.properties;
import org.apache.juli.logging.Log;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Description 功能参数
* @createTime 2023/9/5 17:44
* This program is protected by copyright laws.
* @version : 1.0
*@Author TianXinKun
*/
@ConfigurationProperties(prefix = "tools.ip")
@Component("ipProperties")
public class IpProperties {
/**
* @Description: 日志的显示周期
* @version v2.0
* @author TianXinKun
*
*/
private Long cycle = 5L;
/**
* @Description: 是否周期内重置数据
* @version v2.0
* @author TianXinKun
*
*/
private Boolean cycleReset = false;
/**
* @Description: 日志输出模式
* @version v2.0
* @author TianXinKun
*
*/
private String model = LogModle.DETAIL.value;
public Long getCycle() {
return cycle;
}
public void setCycle(Long cycle) {
this.cycle = cycle;
}
public Boolean getCycleReset() {
return cycleReset;
}
public void setCycleReset(Boolean cycleReset) {
this.cycleReset = cycleReset;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public enum LogModle{
DETAIL("detail"),
SIMPLE("simple");
private String value;
LogModle(String value){
this.value=value;
}
public String getValue(){
return value;
}
}
}
说明:
①@ConfigurationProperties(prefix = “tools.ip”)
那么yml中的配置就是这样:
tools:
ip:
cycle: 10
cycleReset: false
model: "detail"
②@Scheduled(cron = “0/#{ipProperties.cycle} * * * * ?”)
当需要灵活的更改注解中的值,使用这样的方式
③private String model = LogModle.DETAIL.value;
这里使用了枚举
编写configuration
package asia.txk.autoconfig;
import asia.txk.properties.IpProperties;
import asia.txk.service.IpCountService;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @Description 获取ip自动装配类
* @createTime 2023/9/5 16:58
* This program is protected by copyright laws.
* @version : 1.0
*@Author TianXinKun
*/
@Import({IpCountService.class, IpProperties.class})
@EnableScheduling
public class IpAutoConfiguration {
public IpCountService ipCountService(){
return new IpCountService();
}
}
使用方法(本地仓库使用)
将starter安装到本地仓库
在其他项目导入:
yml动态配置:
以上只是一个基础的starter流程,
注意:包名:也就是创建项目时填写的:group,和使用starter项目的一样时,且出现了同名类,嗯!你懂得,就像创建项目时你的group为org.springframework一样,因此,自定义starter的名字很重要
拦截器编写
实现HandlerInterceptor接口
以上面的自定义ip记录starter为例
public class IpCountInterceptor implements HandlerInterceptor {
@Autowired
private IpCountService ipCountService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ipCountService.count();
return true;
}
}
这就是一个拦截器
说明:
HandlerInterceptor有默认实现,因此不会报红:
在generate可以快捷实现:
有三个实现方法:
preHandle:在原始方法的前调用
postHandle:在原始方法的后调用,也就是在ModelAndView前调用
afterCompletion:在原始方法完成后调用,也就是在ModelAndView也完成后调用
返回值:true则请求就往后走
将拦截器加入到mvc中
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipCountInterceptor()).addPathPatterns("/**");
}
@Bean
public IpCountInterceptor ipCountInterceptor(){
return new IpCountInterceptor();
}
}
将拦截器加入到自定义的starter中
说明:这个拦截器在项目中,如果将自定义的 starter去掉项目就不能跑了,不符合spring的逻辑,直接将拦截器放在starter中,导入starter代码都不用写直接就有功能了,完美!!!!!!!!
最后自定义starter的结构就是这样的了
配置提示
我们的yml中使用的时候没有提示,接下来实现
我们导入一下这个依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
</dependency>
刷新pom然后清空项目并重新生成target,也就是以下操作:
然后,就可以在target\classes\METE-INF下有一个json文件
将其cp到stater的METE-INF下,并且将刚刚的依赖删掉重新刷新项目、重新生成target。
接下来是效果:
但是:model里面的值是固定的,我们的没有提示。
这个就只能去spring里面看看怎么写的了
在里面搜索hints,
复制一个到我们的json中,并修改
这是我的:
"hints": [
{
"name": "tools.ip.model",
"values": [
{
"value": "detail",
"description": "详细模式."
},
{
"value": "simple",
"description": "极简模式."
}
]
}
]
说明:“description”: "极简模式."有一个点结尾,
“name”: “tools.ip.model”,是properties下面的name
核心原理
springboot启动流程(整体)
@SpringBootApplication
public class Springbootdom001Application {
public static void main(String[] args) {
SpringApplication.run(Springbootdom001Application.class, args);
}
}
以上是一个springboot的启动入口:
其中run方法是这样的:
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified source using default settings.
* @param primarySource the primary source to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
再点进去:
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified sources using default settings and user supplied arguments.
* @param primarySources the primary sources to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
SpringApplication
/**
* Create a new {@link SpringApplication} instance. The application context will load
* beans from the specified primary sources (see {@link SpringApplication class-level}
* documentation for details). The instance can be customized before calling
* {@link #run(String...)}.
* @param primarySources the primary bean sources
* @see #run(Class, String[])
* @see #SpringApplication(ResourceLoader, Class...)
* @see #setSources(Set)
*/
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
在this方法中:
/**
* Create a new {@link SpringApplication} instance. The application context will load
* beans from the specified primary sources (see {@link SpringApplication class-level}
* documentation for details). The instance can be customized before calling
* {@link #run(String...)}.
* @param resourceLoader the resource loader to use
* @param primarySources the primary bean sources
* @see #run(Class, String[])
* @see #setSources(Set)
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
说明:this.resourceLoader = resourceLoader;扩大应用范围
Assert.notNull(primarySources, “PrimarySources must not be null”);判断传入启动类是否为空
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));将传入的启动类去重并存入集合,然后将集合扩大应用范围
this.webApplicationType = WebApplicationType.deduceFromClasspath();
static WebApplicationType deduceFromClasspath() {
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
return WebApplicationType.REACTIVE;
}
for (String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
return WebApplicationType.SERVLET;
}
这里主要处理当前是否需要加载一个web容器,判别方式是将指定的类名去forName,如果没有异常,也就是项目中有指定的类,那么返回特定的容器。
this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
我们可以看到使用Set names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));拿到了需要加载的name的集合,也就是加载spring.factories中的东西;List instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);根据名称匹配,将匹配上的名称集合进行一个排序,然后返回。
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
同上面一样只是现在获取的是需要加载ApplicationContextInitializer有哪些。
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
同上面一样只是现在获取的是需要加载的ApplicationListener(监听器)有哪些。
总结:getSpringFactoriesInstances就是返回传入的类对应的实例,而匹配的name集合是从spring.factories中通过forname的方式去判断的。
this.mainApplicationClass = deduceMainApplicationClass();初始化了引导类类名信息,备用
初始化数据
在run中:
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified sources using default settings and user supplied arguments.
* @param primarySources the primary sources to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
这后面还有一个.run(args)
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
long startTime = System.nanoTime();计时开始
DefaultBootstrapContext bootstrapContext = createBootstrapContext();系统引导信息对应的上下文对象
configureHeadlessProperty();模拟输入输出信号,避免出现因缺少外设导致的信号传输失败,进而引发错误。
SpringApplicationRunListeners listeners = getRunListeners(args);获取当前可使用的监听器
listeners.starting(bootstrapContext, this.mainApplicationClass);监听器执行了对应的操作
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);获取参数
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);将前期读取的数据转化为一个对象
configureIgnoreBeanInfo(environment);做了一个配置,定义了一个系统级的配置
Banner printedBanner = printBanner(environment);是否打印Banner
context = createApplicationContext();创建容器对象,根据前期配置的容器类型决定的
context.setApplicationStartup(this.applicationStartup);设置启动模式
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);对容器进行设置,参数来源与前期的设定
refreshContext(context);刷新容器环境
afterRefresh(context, applicationArguments);刷新完毕后的后处理
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);统计计时时间
if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup;判断是否记录启动时间的日志,创建日志对应的对象,输出日志信息,包含启动时间。
callRunners(context, applicationArguments);
监听机制
我们可以写一个自定义的监听器看看情况:
先看官方怎么写的
点进去
public class BackgroundPreinitializer implements ApplicationListener<SpringApplicationEvent> {
实现了这个方法,ok我们照着来呗,顺便看看event中的东西
public class MyListener implements ApplicationListener{
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println(event.getTimestamp());
System.out.println(event.getSource());
System.out.println(event.getClass());
}
}
别忘记spring.factories
org.springframework.context.ApplicationListener=\
asia.txk.listener.MyListener
启动在打印日志查看
1693969148879
org.springframework.boot.SpringApplication@401e7803
class org.springframework.boot.context.event.ApplicationStartingEvent
1693969148968
org.springframework.boot.SpringApplication@401e7803
class org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.6)
1693969149280
org.springframework.boot.SpringApplication@401e7803
class org.springframework.boot.context.event.ApplicationContextInitializedEvent
2023-09-06 10:59:09.285 INFO 8480 --- [ main] c.e.Springbootdom008SsmpApplication : Starting Springbootdom008SsmpApplication using Java 1.8.0_241 on python with PID 8480 (G:\mavenproject\springbootdom008_ssmp\target\classes started by TianXinKun in G:\mavenproject\springbootdom001)
2023-09-06 10:59:09.287 INFO 8480 --- [ main] c.e.Springbootdom008SsmpApplication : No active profile set, falling back to 1 default profile: "default"
1693969149322
org.springframework.boot.SpringApplication@401e7803
class org.springframework.boot.context.event.ApplicationPreparedEvent
2023-09-06 10:59:10.367 INFO 8480 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 80 (http)
2023-09-06 10:59:10.375 INFO 8480 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-09-06 10:59:10.375 INFO 8480 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.69]
2023-09-06 10:59:10.487 INFO 8480 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-09-06 10:59:10.487 INFO 8480 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1163 ms
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
2023-09-06 10:59:10.614 INFO 8480 --- [ main] c.a.d.s.b.a.DruidDataSourceAutoConfigure : Init DruidDataSource
2023-09-06 10:59:10.778 INFO 8480 --- [ main] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
Registered plugin: 'com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor@3eee3e2b'
Property 'mapperLocations' was not specified.
_ _ |_ _ _|_. ___ _ | _
| | |\/|_)(_| | |_\ |_)||_|_\
/ |
3.5.2
This primary key of "id" is primitive !不建议如此请使用包装类 in Class: "com.example.pojo.Book"
2023-09-06 10:59:12.124 INFO 8480 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 80 (http) with context path ''
1693969152125
org.springframework.boot.web.embedded.tomcat.TomcatWebServer@a2341c6
class org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
1693969152126
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@f0c8a99, started on Wed Sep 06 10:59:09 CST 2023
class org.springframework.context.event.ContextRefreshedEvent
2023-09-06 10:59:12.135 INFO 8480 --- [ main] c.e.Springbootdom008SsmpApplication : Started Springbootdom008SsmpApplication in 3.261 seconds (JVM running for 4.988)
1693969152136
org.springframework.boot.SpringApplication@401e7803
class org.springframework.boot.context.event.ApplicationStartedEvent
1693969152136
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@f0c8a99, started on Wed Sep 06 10:59:09 CST 2023
class org.springframework.boot.availability.AvailabilityChangeEvent
1693969152137
org.springframework.boot.SpringApplication@401e7803
class org.springframework.boot.context.event.ApplicationReadyEvent
1693969152137
org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@f0c8a99, started on Wed Sep 06 10:59:09 CST 2023
class org.springframework.boot.availability.AvailabilityChangeEvent
监听器被使用了多次,并且有不同的事件调用的。
说明:spring的监听器是用来干预初始化过程和运行过程的,而触发的条件就是不同的事件,而上述的监听器并没有指定触发事件,故只要有事件就会执行一次。
监听指定的事件:
public class MyListener implements ApplicationListener<ApplicationEvent>{
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println(event.getTimestamp());
System.out.println(event.getSource());
System.out.println(event.getClass());
}
}