文章目录
一、基础篇
1.创建SpringBoot工程的方式
1.1 方式1:使用Spring Initializr方式
-
打开IDEA,创建一个空工程
-
检查Maven的配置是否正确
-
在工程选择新建模块
-
选择Spring Initializr方式
1.2 方式2:阿里云版本
问题说明:Spring Initializr默认使用的是国外的镜像网站,有时候访问不了这个时候怎么创建工程?
-
打开IDEA,创建一个空工程
-
检查Maven的配置是否正确
-
在工程选择新建模块
-
选择Spring Initializr方式
-
URL输入:
https://start.aliyun.com/
-
剩下的都是一样的
1.3 方式3:导入已有项目或模板
-
创建一个Spring Boot的空工程
-
在本地的文件管理器中打开,只需要剩下src目录中的东西和pom文件,其他的全部删除
-
等到需要使用的时候直接在文件夹中复制一份,修改文件夹的名字
-
然后修改pom文件中的artifactId为对应的模块名
-
删除pom中的name和description标签
-
然后在idea中打开即可
2.入门案例
-
在主类的包下新建一个controller包
-
创建一个controller,类上加上@RestController注解
-
在类上加上@RequestMapping注解,值是访问的前置地址
-
创建一个方法,方法上加上对应的Mapping注解,表示访问的具体地址
@RestController @RequestMapping("/admin/dish") public class DishController { @Autowired private DishService dishService; @PostMapping public Result add(@RequestBody DishDTO dishDTO) { dishService.insert(dishDTO); return Result.success(); } @GetMapping("/page") public Result<PageResult> queryPage(DishPageQueryDTO dishPageQueryDTO) { PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); } @DeleteMapping public Result deleteDish(@RequestParam List<Long> ids){ dishService.delete(ids); return Result.success(); } @PutMapping public Result update(@RequestBody DishDTO dishDTO){ dishService.update(dishDTO); return Result.success(); } }
-
访问对应的地址就可以发送到对应的请求:localhost:端口号/请求路径
3.入门案例解析
3.1 parent模块
定义了可能使用到的所有依赖以及版本信息,到我们使用的时候,直接继承parent模块可以避免多个依赖使用相同的技术的时候产生版本冲突
总结:parent模块只定义了依赖(dependencyManager标签)和版本信息,但是并没有使用,是为了**减少依赖冲突**
3.2 stater依赖
是一组依赖的集合,定义了当前stater使用的所有依赖坐标,达到减少依赖配置的目的,用来**简化依赖配置**的
3.3 引导主类
主类的main方法就是创建并初始化了Spring容器,引导类默认扫描的是主类所在包下的所有文件
3.3 内嵌服务器
Spring Boot支持了三种服务器:tomcat、jetty、undertow
3.4 排除不想要的依赖
只是说有这个方法,但是实际使用中并不会使用
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-stater-web</artifactId>
<!--排除了tomcat服务器-->
<exclusions>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-stater-tomcat</artifactId>
</exclusions>
</dependency>
</dependencies>
4.配置文件介绍
SpringBoot的默认配置文件在resources下面,名字是application.properties。
配置文件中的属性是和依赖关联的,如果没有对应的依赖,那么对应的配置文件中的属性也会失效。
4.1 修改配置案例
# 服务器端口配置
server.port=8080
#修改Springboot项目启动的命令行图形
spring.banner.image.location=abc.png
#日志级别(默认info级别)
logging.leval.root=info
4.2 配置文件介绍
-
SpringBoot提供了三种格式的配置文件:
properties
、yml(推荐使用)
、yaml
-
如果三种配置文件同时存在,配置文件的加载的顺序:yaml>yml>properties,配置文件的的优先级:properties>yml>yaml
-
同一个属性的配置,优先级高的会覆盖优先级低的,最终三个配置文件会叠加生效
-
简单来说:如果有相同的属性,高优先级会覆盖低优先级的值,如果存在不同的属性,那么会生效
例如:三个都配置了端口号,那么properties中配置的端口号会生效。如果properties和yml没有配置banner,但是yaml配置了,那么banner属性也会生效
4.3 小技巧:配置文件属性提示消失解决方案
- 点击右上角的工程结构按钮
- 点击左侧的Facets,选择当前工程
- 点击右侧的绿叶标志
- 然后点击加号,将失效的配置文件加进去
4.4 yml文件的格式
格式说明:
- 属性使用冒号结束
- 属性和属性值之间需要加空格
- #表示注释
- 使用缩进表示层级关系,同层级左侧对齐,缩进只允许用空格(不允许使用tab)
- 转义字符如果失效,可以将值使用双引号引起来
#常规赋值
baseDir: /usr/local
user:
name: yfj
age: 18
city: 河北
#给数组中添加普通值
hobby: [java,mysql,redis]
#给数组中添加对象[大括号里面不用加空格]
hobby: [{name:yfj,age:23,tall:190},{name:yfj1,age:25,tall:175},{name:yfj1,age:3,tall:100}]
#值包含转义字符【就用双引号】
tempdir: "${baseDir}/temp"
4.5 读取配置文件的数据
4.5.1 读取yml中的单个值到指定类
使用@Value注解,注解的值是${ }
4.5.2 读取yml的全部值
SpringBoot提供了一个Environment对象,用来接收配置文件的全部属性,使用的时候只需要使用@Autowired注解将对象注入,然后就可以使用这个对象
@Compont
public void Test{
@Autowired
private Environment env;
public void test1(){
env.getProperty("server.port");//这样就能直接使用
}
}
4.5.3 读取yml中的对应数据到类中【最常用】
- 先在yml中定义好属性和值
- 然后创建一个类,类中的属性名要和yml中的属性名对应
- 给属性添加get、set方法
- 类上添加@Component注解
- 类上添加@ConfigurationProperties注解,值是属性名前缀
#配置文件案例
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sky_take_out
username: root
password: 123456
//对应的类
@Compont
@ConfigurationProperties("datasource.druid")//对应前缀
@Date//自动创建get和set方法
public class Source{
private String driver-class-name;//对应yml中的属性,名字要一样
private String url;
private String username;
private String password;
}
4.6 yml中变量的引用
如果多个属性的值的某一部分是公共的,那么可以先定义一个属性,属性的值是公共部分,然后再在其他属性中使用${ }引用即可
#定义一个被引用的变量
baseDir: c:\windows
#引用变量
tempDir: ${baseDir}\temp
downDir: ${baseDir}\Download
#说明:使用引号包裹的转义字符会生效
tempDir2: "${baseDir}\temp"
6. 整合第三方技术(基础篇)
6.1 整合Juint
说明:SpringBoot默认就会导入Test依赖,所以就不需要导入依赖了
- SpringBoot整合Junit,就是使用了@SpringBootTest注解
- 然后将被测试的类使用@Autowired注解注入
- 然后方法上添加@Test注解,直接调用对象的方法就可以了
其他说明:
如果测试类在引导类所在的包下,那么可以直接使用。
如果测试类不在引导类的包下,那么需要使用@SpringBootTest注解的classes属性指定引导类
@SpringBootTest(classes=SpringBoot05.class) class SpringBoot05Test{ }
6.2 整合MyBatis
SpringBoot整合MyBatis需要做的就是一下几点:
- 核心配置:数据库连接的相关信息(练什么?连谁?什么权限)
- 映射文件:SQL映射(xml/注解)
-
导入SpringBoot整合MyBatis的依赖、MySQL驱动依赖
-
配置相关信息
#1.配置数据库相关信息 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=UTC username: root password: 123456 #2.mybatis的相关配置 mybatis: #映射文件的位置 mapper-locations: classpath:mapper/*.xml #配置了这个resultType中就不需要写全类名了(配不配都行) type-aliases-package: com.sky.entity configuration: #开启驼峰命名 map-underscore-to-camel-case: true
-
创建对应的实体类
-
创建Mapper接口,接口上添加@Mapper注解
6.3 整合MyBatis—Plus
-
导入SpringBoot整合MyBatis-Plus的依赖、MySQL驱动依赖
-
编写配置信息
#配置数据库相关信息 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=UTC username: root password: 123456
-
然后就可以正常使用了
6.4 整合Druid
-
导入SpringBoot整合MyBatis-Plus的依赖、MySQL驱动依赖、Druid依赖
-
编写配置信息【相对于上面的,配置文件多了一层druid属性】
#配置数据库相关信息 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=UTC username: root password: 123456
-
其他的就是正常使用
7.项目整合使用案例
7.1 导入需要使用的依赖
- web依赖
- mysql驱动
- MyBatis依赖
- Druid依赖
- Lombok依赖
7.2 Lombok开发实体类
说明:Lombok是一个java类库,提供了一组注解用来简化pojo的开发。需要导入依赖。
Lombok的注解:
@Getter:生成所有属性的get方法
@Setter:生成所有属性的set方法
@Data:生成Get、Set、toString、hashcode方法【因此这个注解最常使用,但是注意没有生成有参构造方法,无参构造方法是实体类自带的】
@NoArgsConstructor:生成无参构造器【但是实体类自带无参构造器】
@AllArgsConstructor:生成带所有参数的构造器
@Data
public class CategoryDTO implements Serializable {
//主键
private Long id;
//类型 1 菜品分类 2 套餐分类
private Integer type;
//分类名称
private String name;
//排序
private Integer sort;
}
7.3 数据层的开发
-
编辑配置文件
#1.配置数据库相关信息 spring: datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=UTC username: root password: 123456 #2.mybatis的相关配置 mybatis: #映射文件的位置 mapper-locations: classpath:mapper/*.xml #配置了这个resultType中就不需要写全类名了(配不配都行) type-aliases-package: com.sky.entity configuration: #开启驼峰命名 map-underscore-to-camel-case: true
-
创建mapper包,创建mapper接口(加@Mapper注解)
-
编写对应的方法
@Mapper public interface DishFlavorMapper { @Select("select * from dish_flavor where dish_id = #{id}") List<DishFlavor> queryById(Long id); }
-
测试mapper接口的方法
7.4 业务层的开发
- 创建service包,创建service接口,编写方法
- 创建serviceimpl包,创建serviceimpl类实现对应的接口,类上加@Service方法
- 注入Mapper对象,实现接口的方法
- 测试方法
7.5 表现层开发
- 创建controller包
- 创建接口类,添加@RestController注解,添加@RequestMapping注解
- 注入service对象
- 编写对应的方法
- 进行测试
@RestController("manageShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺操作接口")
public class ShopController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping("/status")
@ApiOperation("获取营业状态")
public Result<Integer> getStatus(){
System.out.println(redisTemplate);
Integer shop_status = (Integer) redisTemplate.opsForValue().get("shop_status");
return Result.success(shop_status);
}
@PutMapping("/{status}")
@ApiOperation("设置营业状态")
public Result setStatus(@PathVariable Integer status){
redisTemplate.opsForValue().set("shop_status",status);
return Result.success();
}
}
7.6 前端页面开发
说明:
实际开发中前端界面是单独放在服务器中的,并不会放在我们的后端项目中。但是由于我们这是初步使用,所以前后端代码都放在了一个工程中。
单体工程的页面放置在resources目录下的static目录中
7.6.1 前端界面拷贝到项目中
将前端的文件全部拷贝到resources目录下的static目录中
如何访问页面:
例如:前端html界面在resources下的static下的pages中
在浏览器中直接输入localhost:端口号/pages/XXX.html即可
7.6.2 axios发送异步请求
//在Vue的方法中
methods:{
getALL(){
//发送异步请求:Get请求就是get方法、Put请求就是put方法,另两个也一样
//请求的值是后端接口
//then方法是获取到数据后的操作
axios.get("/books").then((res)=>{
//dataList是Vue定义的一个变量
//第一个data表示vue收到的数据
//第二个表示数据中的data属性(我们Result类中封装的)
this.dataList=res.data.data;
});
}
}
7.6.3 前端添加功能
-
找到button按钮可以看到
<el-button>
标签的@click属性已经绑定了对应的方法,所以只需要完善这个方法即可<el-button type="primary" class="butT" @click="handleCreate()">添加</el-button>
-
弹出添加界面
//Vue中定义了一个dialogFormVisible属性默认为false //dialogFormVisible用来控制弹层是否显示,true表示显示,false表示关闭 //dialogFormVisible属性用于el-dialog标签中的 :visible.sync属性 <el-dialog title="添加" :visible.sync="dialogFormVisible">
//打开弹窗的函数 handleCreate(){ //点击按钮后显示添加数据的弹层 this.dialogFormVisible=true;//将这个值改为true就可以弹出界面 }
-
点击确定按钮发送数据给后端
//确定按钮 <el-button type="primary" @click="handleadd()">确定</el-button>
//上面已经看到点击确定按钮后会调用handleadd()方法,所以我们就晚上这个方法 handleadd(){ //这里的formdata就是我们发送的数据,已经被Vue定义 axios.post("/books",this.formData).then((res)=>{ if(res.data.flag){//flag是后端发送的数据,如果flag表示添加成功 //添加成功就关闭弹层 this.dialogFormVisible=true; this.$message.success("添加成功");//绿色的提示框 }else{ this.$message.error("添加失败");//红色的提示框 } }).finally(()=>{ //不管添加成功和是被都重新加载数据 this.getALL(); }); }
//后面发现添加成功再打开,表单的数据还存在,因此需要重置表单。 //每次打开的时候重置表单 //重置表单的函数 resetForm(){ this.formData={}; } //在打开的时候就调用这个函数 //打开弹窗的函数 handleCreate(){ //点击按钮后显示添加数据的弹层 this.dialogFormVisible=true;//将这个值改为true就可以弹出界面 this.resetForm(); }
//取消按钮:关闭弹窗 cancel(){ this.this.dialogFormVisible=false; this.$message.info("添加取消");//灰色的提示框 }
7.6.4 前端删除功能
el表达式会封装所有数据,我们删除的时候可以把行号传递给后端
<template slot-scop="scop"><!--这里表示数据封装到了scop中--> <!--这里表示删除一行--> <el-button type="danger" size="mini" @click="handleDelete(scop.row)">删除</el-button>
handleDelete(row){
//then表示点击了确定
this.$confirm("提示内容","提示框标题",{type="info"}).then(()=>{
//根据行数据row获取当前行id
axios.delete("/books"+row.id).then((res)=>{
if(res.data.flag){
this.$message.success("删除成功");
}else{
this.$message.error("删除失败");
}
}).finally(()=>{
this.getALL();
});
//catch表示点击了取消
}).catch(()=>{
this.$message.info("取消删除");
});
}
7.6.5 修改功能
-
先根据id查询出数据
-
如果数据能查出来并且不为空则弹出修改框
-
如果不能查到数据或者数据为空则刷新数据
7.6.5 修改数据
7.6.6 前端分页
8.全局异常处理器
- 创建一个异常处理类,加上@RestControllerAdvice注解
- 定义一个方法,方法的参数值就是处理异常的种类
- 方法上加@ExceptionHandler注解(能处理什么异常靠他决定)
- 方法体就是相关操作(记录日志、通知运维、通知开发等)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler//不加值表示通用的,加值表示处理指定的异常
public Result exceptionHandler(Exception ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
}
9.静态资源访问
静态资源目录:默认在resources/static或resources/public
静态资源的访问使用:IP地址:端口号/静态资源名
例如:localhost:8080/hello.jpg
10.拦截器
拦截器说明:SpringMVC提供的拦截器有三种方法:preHandler(目标方法处理之前)、postHandler(目标方法执行之后)、afterCompletion(页面处理完成之后)。会在目标请求的对应阶段执行。
-
创建一个类实现HandlerInterceptor接口
-
重写接口的对应方法【preHandler、postHandler、afterCompletion】
-
创建一个配置类加上@Configuration注解,实现WebMvcConfigurer接口
-
配置类中重写addInterceptors方法,调用参数中registry对象的addInterceptor方法,方法中new一个我们写的拦截器
-
然后接着调用addPathPatterns方法设置拦截的路径
-
然后接着调用excludePathPatterns方法设置放行的路径(不需要处理的路径,一般是静态方法)
public class TestInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果登录了就不拦截 HttpSession session = request.getSession(); if (session.getAttribute("userName")!=null){//登录成功的用户名放到session域里面了 return true;//返回true就是放行 } //如果没有登录就进行拦截 request.setAttribute("msg","请先登录!!!"); request.getRequestDispatcher("/").forward(request,response); return false;//返回false就是不放行 } }
@Configuration public class MyConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new TestInterceptor()) .addPathPatterns("/**")//设置拦截路径 /**表示所有路径(包括静态资源) .excludePathPatterns("/","/login","/css/**","/js/**"); //放行的请求路径 //访问首页、登录发送的请求也要放行 //【因为静态文件实在static下面的所以直接/包名就可以了】 } }
11.文件上传
-
表单使用post请求
<form method="post" th:action="@{/file}" enctype="multipart/form-data"><!--文件上传的表单头固定这么写--> <input type="file" name="file"><br><!--单文件上传--> <input type="file" name="files" multiple><br><!--多文件上传--> <input type="submit" value="提交"> </form>
-
controller方法中的参数使用MultipartFile
/** * 处理表单提交的信息 * MultipartFile 会自动封装上传的单个文件 * MultipartFile [] 会自动封装上传的多文件 * @param file * @param files * @return */ @PostMapping("/file") public String field(@RequestParam("file") MultipartFile file, @RequestParam("files") MultipartFile[] files) throws IOException { System.out.println("单文件的大小:"+file.getSize()); System.out.println("多文件的数量:"+files.length); //将文件保存到本地 if (!file.isEmpty()){//如果文件不为空,就保存 String originalFilename = file.getOriginalFilename();//原始文件名 file.transferTo(new File("C:\\Users\\17716\\Desktop\\"+originalFilename));//名字可以使用uuid } if (files.length>0){//判断数组里面有没有东西 for (MultipartFile multipartFile : files) { if (!multipartFile.isEmpty()){//判断每个文件是否有东西 String originalFilename = multipartFile.getOriginalFilename(); multipartFile.transferTo(new File("C:\\Users\\17716\\Desktop\\"+originalFilename)); } } } return "redirect:/success.html"; }
-
配置文件中可以设置文件大小
#设置单文件大小 spring.servlet.multipart.max-file-size=10MB #设置总文件大小 spring.servlet.multipart.max-request-size=100MB
二、实用篇
1.运维实用篇
1.1 工程打包与运行
1.1.1 Windows版本
-
打开IDEA,点击Maven
-
点击右上角的蓝色圆圈跳过测试(不然测试中的代码会执行,影响到数据库)
-
执行maven周期里的package插件,就会在target中生成jar包(或者输入maven命令打包)
mvn package
-
在资源管理其中打开这个文件夹,在地址栏输入cmd
-
输入命令即可执行(执行这个命令的时候pom文件中需要有maven-plugin插件依赖)
java -jar XXX.jar
1.1.2 Windows版本的问题
-
出现端口冲突
首先应该查看主机已经占用的端口
#查看所有的端口占用情况 netstat -ano #查看指定端口的专用情况 netstat -ano | findstr "端口号"
然后查看占用端口的是什么程序
tasklist | findstr "查出来的PID"
如果可以就杀死占用端口的进程
taskkill /f /pid "查出来的PID"
1.1.3 Linux版本
-
打开IDEA,点击Maven
-
点击右上角的蓝色圆圈跳过测试(不然测试中的代码会执行,影响到数据库)
-
执行maven周期里的package插件,就会在target中生成jar包(或者输入maven命令打包)
mvn package
-
将这个jar包上传到usr目录中
-
Linux系统中创建数据库
-
输入命令后台执行jar包
#> server.log 表示将日志输出到这个文件中(自动创建) #2>&1 & 表示将标准错误重定向到标准输出 nohup java -jar XXX.jar > server.log 2>&1 &
-
结束进程
#查询进程的进程号 ps -ef | grep "java -jar" #杀死进程 kill -9 进程号
1.1.4 脚本自动部署
-
将本地代码推送到远程仓库
-
在Linux中安装Git
yum install git
-
拉取远程仓库代码
git clone 项目地址
-
在Linux中安装Maven【自己找安装包】
-
修改系统环境变量
vim /etc/profile #加入下面的代码 export MAVEN_HOME=maven地址 #下面这一段前半部分应该已经写了,只需要添加对应的 export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH
-
重新加载配置文件
source /etc/profile
-
测试Maven是否正常运行
mvn -version
-
修改Maven配置文件设置本地仓库地址
#指定本地仓库的位置(文件夹需要提前创建) <localRepository>本地仓库位置</localRepository>
-
编写脚本
#!/bin/sh echo ================================= echo 自动化部署脚本启动 echo ================================= echo 停止原来运行中的工程 APP_NAME=helloworld tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'` if [ ${tpid} ]; then echo 'Stop Process...' kill -15 $tpid fi sleep 2 tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'` if [ ${tpid} ]; then echo 'Kill Process!' kill -9 $tpid else echo 'Stop Success!' fi echo 准备从Git仓库拉取最新代码 cd /usr/local/HelloWorld echo 开始从Git仓库拉取最新代码 git pull echo 代码拉取完成 echo 开始打包 output=`mvn clean package -Dmaven.test.skip=true` cd target echo 启动项目 nohup java -jar HelloWorld-1.0-SNAPSHOT.jar &> helloworld.log & echo 项目启动完成
-
为用户授予执行权限
chmod 777 脚本名
-
执行脚本
./脚本名
1.2 临时属性
现在面临这样一个场景:我的项目已经打包出来了,而且定义的服务器端口是8080,当我部署当服务器上的时候发现端口冲突了,并且已经运行的程序很重要不能停止,这个时候该怎么办?
jar包在部署运行的时候可以设置临时属性覆盖配置文件中的对应内容
#案例(原本的jar包端口号是80,现在我们需要指定为8080)
# 格式:java -jar XXX.jar --临时属性=值
#临时属性的书写格式是properties类型的
java -jar XXX.jar --server.port=8080
#设置多个临时属性(使用多个--即可)
java -jar XXX.jar --service.port=8080 --spring.datasource.druid.password=123456
临时属性的原理:主类上有个args数组参数,我们控制台输入的参数会传入到args中,springBoot的run方法如果传进args了参数,那么就会覆盖原有的参数值。
1.3 不同人员使用的配置文件
SpringBoot支持多套配置文件,例如:我们开发的时候使用一套配置文件,项目经理还可以使用另一套配置文件在idea中覆盖,运维上线的时候还可以使用一套,运维的组长还可以使用一套
-
程序员用的配置文件:
就是平常resources文件夹下的配置文件
-
项目经理的配置文件:
就是放到resources下config目录下的配置文件
- 在resources文件夹下新建一个名为
config
的文件夹 - 然后在里面新建一个名为
application.yml
的配置文件 - 里面编写的配置文件会覆盖外面对应的属性
- 最终两个配置文件都会生效,但是优先级高的是config里面的配置文件
- 在resources文件夹下新建一个名为
-
运维的配置文件:
项目部署的时候将配置文件放到和jar包同一个文件夹内的
-
运维组长的配置文件:
项目部署时jar包同级目录
config
文件夹内的配置文件生效级别(高–低):运维组长>运维>项目经理>程序员
1.4 自定义配置文件名
上面的1.3中的情况默认都是以application命名的,那么别人以特殊手段用application文件搞我怎么办。我们可不可以自定义配置文件名?
-
方法1:上线的时候使用临时属性指定配置文件名
#例如:jar包的同级有一个配置文件XXX.yml,要求使用这个上线 java -jar abc.jar --spring.config.name=xxx
-
方法2:上线的时候使用临时属性指定配置文件路径
#jar包的同级有一个配置文件bank.yml,要求使用这个上线 java -jar abc.jar --spring.config.location=/usr/project/bank.yml
1.5 多环境配置文件
1.5.1 多环境多文件开发
主配置文件用于设置全局信息,新建的配置文件用于设置经常变化的配置信息
-
resources下有多个配置文件,主配置以application命名,新建的用-XXX命名
-
主配置文件中定义需要调用的配置文件
spring: profiles: active: dev #这个意思是调用application-dev配置文件
1.5.2 多环境分组管理
说明:实际开发中回根据功能对配置文件中的信息进行拆分(数据库一个、缓存一个。。。),并制作成独立的配置文件
-
命名规则:
- application-devDB.yml
- application-devRedis.yml
- application-devMVC.yml
-
多文件的使用:
spring: profiles: active: dev #这个意思是调用application-dev配置文件 include: devDB,devRedis,devMVC #意思是同时包含这几个环境(必须以dev开头)
说明:使用include的时候active写的什么,后面的include就必须以什么开头,因此后面引申出了group配置替换include
(注意:前面的先加载,后面的后加载,同一个属性后面的覆盖前面的)
spring: profiles: active: dev #这个意思是调用application-dev配置文件 group: "opt": optDB,optRedis,optMVC "dev": devDB,devRedis,devMVC
说明:上面的这个配置,表示dev开头的生效,opt开头的不生效**(配置文件加载顺序也发生了变化:最后面级别最高)**
1.5.3 Maven与Boot多环境配置控制
- 当Maven与SpringBoot同时对环境进行控制时,以Maven为主,SpringBoot使用@XXX@读取Maven对应的配置属性值
- 基于SpringBoot读取Maven配置属性值的前提下,如果在idea下测试工程时,pom文件每次更新后需要手动执行compile方可生效
1.6 日志
1.6.1 日志基础操作
日志的作用:编程期调试代码,运营期记录信息(记录日常运行重要信息,记录应用报错信息,记录运维过程数据)
-
创建日志对象
private static final Logger log=LoggerFactory.getLogger(BookController.class);
-
记录日志
log.debug("xxx"); log.info("xxx");//默认级别是info级别,所以debug不会执行 log.warn("xxx"); log.error("xxx");
-
设置日志级别的方式
logging: level: #root表示工程中的全都设为这个级别 root: info #默认就是这个,不用配置也就是这个
1.6.2 快速创建日志对象
问题说明:1.6.1中我们每次使用的时候都得写那一行代码,那么有没有简单的方法直接调用日志方法呢?
-
导入lombok依赖
-
在需要使用的类上加@Slf4j注解
-
需要使用的时候直接调用对应的方法
log.debug("xxx"); log.info("xxx"); log.warn("xxx"); log.error("xxx");
1.6.3 日志输出格式控制
-
日志的输出格式:
-
设置日志消息格式(知道就行,用不着自己写):
# %d表示时间 %p表示级别 %t线程名 %c表示类名 %m表示日志信息 #重要说明:按照上面的自己造一个就可以 logging: pattern: console: "%d %clr(%5p) ---[%16t] %clr(%-40.40c){red} :%m %n"
1.6.4 文件记录日志
-
设置日志文件:
logging: file: name: server.log
-
设置日志文件大小、滚动文件名
logging: logback: rollingpolicy: #设置每个日志最大多大 max-file-size: 4KB #设置文件名:%d表示时间,%i表示是第几个 file-name-pattern: server.%d(yyyy-MM-dd).%i.log
2.开发实用篇
2.1 热部署
什么是热部署?为什么需要热部署?
我们编写boot程序的时候,测试某个功能,发现有bug,修改后端bug后需要重启服务器,测试功能。
热部署的作用就是当我们的后端代码发生变化的时候,服务器会自动重启,因此改完代码可以直接去前端测试功能,不需要重新启动后端项目。
说明:热部署仅仅加载当前开发者自定义的开发资源,不加载jar资源
2.1.1 开启热部署
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency>
-
启动项目
-
每次修改项目后点击IDEA中的Buid——Build Project(快捷键:Ctrl+F9)
-
如果不想每次都手动刷新可以打开这个选项
-
然后按住Ctrl+ALT+Shift,选择Registry,勾选下图所示的选项
-
使用的时候等5s就会自动刷新
2.1.2 使用说明
-
下图所示的目录默认不参与热部署
-
可以在配置文件中配置不参与热部署的文件类型
spring: devtools: restart: # 设置不参与热部署的文件/文件夹 #/**表示文件夹 exclude: public/**,static/**
2.1.3 关闭热部署
spring:
devtools:
restart:
#false 关闭热部署
enabled: false
2.2 参数校验
在web实际开发过程中,后端往往需要对前端传过来的数据做校验,虽然可以接受参数后手动校验,但是太麻烦了,因此springboot提供了一个校验框架。
-
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
-
在实体类中使用提供的注解
@Data public class User { @NotNull(message = "id不能为空")//message是发生错误时的提示信息 private Long id; @NotEmpty(message = "姓名不能为空") @Length(min = 2,message = "姓名不能小于两位") private String name; @Min(value = 16,message = "年龄必须大于16") private int age; }
-
在需要检验的地方添加@Validation注解
- 不给参数加@Validation注解,里面的属性校验注解不会生效。
- @Validation可以加到参数上,也可以加到实体(Pojo)类上
@PostMapping("/search") public Result search(@RequestBody @Validated User user) { return taskService.findTaskAuditByFilter(user); }
-
然后可以使用全局异常处理器捕获异常信息
-
提供的注解
@Null 验证对象是否为null @NotNull 验证对象是否不为null, 无法查检长度为0的字符串 @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. @NotEmpty 检查约束元素是否为NULL或者是EMPTY. Booelan检查 @AssertTrue 验证 Boolean 对象是否为 true @AssertFalse 验证 Boolean 对象是否为 false 长度检查 @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 @Length(min=, max=) Validates that the annotated string is between min and max included. 日期检查 @Past 验证 Date 和 Calendar 对象是否在当前时间之前 @Future 验证 Date 和 Calendar 对象是否在当前时间之后 @Pattern 验证 String 对象是否符合正则表达式的规则 数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null @Min 验证 Number 和 String 对象是否大等于指定的值 @Max 验证 Number 和 String 对象是否小等于指定的值 @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度 @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度 @Digits 验证 Number 和 String 的构成是否合法 @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。 @Range(min=, max=) 检查数字是否介于min和max之间. @Range(min=10000,max=50000,message="range.bean.wage") private BigDecimal wage; @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证) @CreditCardNumber 信用卡验证 @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。 @ScriptAssert(lang= ,script=, alias=) @URL(protocol=,host=, port=,regexp=, flags=)
3整合第三方技术
3.1 整合Redis
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
修改配置参数
#连接的库号 spring.redis.database=0 # 修改为自己真实IP spring.redis.host=ip地址 spring.redis.port=端口号 spring.redis.password=密码 #连接池大小 spring.redis.lettuce.pool.max-active=8 spring.redis.lettuce.pool.max-wait=-1ms spring.redis.lettuce.pool.max-idle=8 spring.redis.lettuce.pool.min-idle=0
-
操作接口
-
具体操作
//1.注入RedisTemplate对象 private RedisTemplate redisTemplate; @Test void testString(){ //2.具体操作redistemplate.opsFor操作类型 //opsForValue就是String类型 ValueOperations valueOperations = redisTemplate.opsForValue(); valueOperations.set("name","胡歌"); System.out.println(valueOperations.get("name")); }
3.2 整合MongoDB
是个什么东西、怎么安装去专门的课程学习,这里只讲怎么和boot整合
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-,ongodb</artifactId> </dependency>
-
修改配置参数
-
具体操作
//1.注入MongoTemplate对象 //2.具体操作
3.3 整合ElasticSearch
是个什么东西、怎么安装去专门的课程学习,这里只讲怎么和boot整合
-
导入依赖
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </dependency>
-
具体操作
//1.创建客户端对象【不能注入】 //2.具体操作 //3.关闭客户端
3.4 整合缓存框架
缓存框架的作用:可以使用注解将数据写入到缓存中,方便基础业务的开发
3.4.1 SpringCache
如果项目中没有其他缓存技术,开启SpringCache后,缓存数据默认存到一个map中。
如果项目中使用了其他缓存技术,并且想让SpringCache将缓存写入对应的缓存技术里面,那么还需要在配置文件中进行配置
可以看苍穹外卖笔记
整合memcached:可以看黑马的视频www.bilibili.com/video/BV15b4y1a7yG
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-stater-cache</artifactId> </dependency>
-
编写配置文件
如果不存到其他技术中,那么可以不编写配置文件
spring: cache: <!--使用redis--> type: redis reids: <!--是否使用前缀--> use-key-prefix: true <!--key的前缀--> key-prefix: xxx_ <!--是否缓存空值--> cache-null-values: true <!--key的存活时间--> time-to-live: 10s
-
在启动类上添加@EnableCaching注解开启SpringCahce
-
然后在需要缓存的地方加上SpringCache提供的注解
注解 说明 @EnableCaching 开启SpringCache缓存功能(加在启动类上) @Cacheable 方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中(加在方法上) @CachePut 将方法的返回值放到缓存中(加在方法上) @CacheEvict 将一条或多条数据从缓存中删除(加在方法上) -
使用的格式
@CachePut(key=“#id”)//属性key表示放到map中的key,#表示动态查询 //【这个注解还有增加前缀的属性,自己去查】 public Book getById(Integer id){ }
3.4.2 整合jetCache
jetCache对Spring
Cache进行了封装,增强SpringCache的功能,具体怎么使用,这里就不详细写了
3.4.3 整合j2Cache
可以自己去看黑马的视频www.bilibili.com/video/BV15b4y1a7yG
3.5 整合定时任务框架
3.5.1 Quartz
相关概念:
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-stater-quartz</artifactId> </dependency>
-
创建一个任务类,继承QuartzJobBean,实现里面的一个方法,方法里面指定要做的事【截图的任务是定时打印】
这个类不需要加到容器中
-
创建配置类,里面配置两个类:工作明细和触发器
-
然后启动项目就可以了
3.5.2 Spring Task
这是一个定时任务框架,比Quartz好用,详细使用方法可以看苍穹外卖笔记中的
-
启动类上加@EnableScheduling注解
-
创建一个类,加入容器管理,编写任务方法,在方法上加@Scheduled注解,注解的值时corn表达式
-
如果有特殊需要可以在配置文件中配置【没有需要不用配置】
-
然后启动项目就可以
3.6 整合JavaMail
SMTP:是一个发送电子邮件的协议
POP3:是一个接收电子邮件的协议,3代表版本
IMAP:是POP3的替代协议
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-stater-mail</artifactId> </dependency>
-
编写配置文件
spring: mail: #指定使用邮件的厂家 host: smtp.qq.com #发送邮件的邮箱账号 username: 11111@qq.com #发送邮件的邮箱的密码 #[不是qq密码,是加密之后的,需要自己去查] password:
-
开启邮箱的SMTP/POP服务
-
在需要使用的地方发送简单邮件【纯文字类型的】
//1.注入对象 @AutoWired private JavaMailSender javaMailSender; //2.设置基本信息【发件人、收件人、邮件名、邮件内容】 String sendAddress="xxx@xxx.com";//发送人:可以从配置文件中读取 String receiveAddress="xxx@xxx.com";//收件人:可以从方法参数中读取 String title="xxxx";//邮件标题 String body="xxx";//邮件内容 //3.调用方法发送 public void sendMail(){ //创建一个简单消息对象 SimpleMailMessage mailMessage=new SimpleMailMessage(); //封装消息内容 mailMessage.setFrom(sendAddress); mailMessage.setTo(receiveAddress); mailMessage.setSubject(title);//设置邮件标题 mailMessage.setText(body); //发送消息 javaMailSender.send(mailMessage);//传入消息对象 }
-
在需要使用的地方发送复杂邮件【带图片的】
//该定义还得定义,这里省略了,去看上一步的 String body1="<a href="www.baidu.com">点开有惊喜</a>";//发送之后会解析成链接 String body2="<img src="xxxx.com"/>点开有惊喜</a>";//发送之后会解析成链接 public void sendMail() throws Exception{ //创建一个消息对象,不再是SimpleMailMessage类型的了 //这个对象需要通过JavaMailSender对象获得 MimeMessage mimeMessage=javaMailSender.createMimeMessage(); //创建一个MimeMessageHelper对象,构造参数填写mimeMessage MimeMessageHelper helper=new MimeMessageHelper(mimeMessage); //通过MimeMessageHelper对象封装消息内容 helper.setFrom(sendAddress); helper.setTo(receiveAddress); helper.setSubject(title);//设置邮件标题 //helper.setText(body);发送普通文本 //helper.setText(body1,true);//发送链接 helper.setText(body2);//发送图片 //发送消息 javaMailSender.send(mimeMessage);//传入消息对象 }
-
发送带附件的
//该定义还得定义,这里省略了,去看上一步的 String body="xxx";//邮件内容 public void sendMail() throws Exception{ //创建一个消息对象,不再是SimpleMailMessage类型的了 //这个对象需要通过JavaMailSender对象获得 MimeMessage mimeMessage=javaMailSender.createMimeMessage(); //创建一个MimeMessageHelper对象,构造参数填写mimeMessage,后面的true表示允许发送附件 MimeMessageHelper helper=new MimeMessageHelper(mimeMessage,true); //通过MimeMessageHelper对象封装消息内容 helper.setFrom(sendAddress); helper.setTo(receiveAddress); helper.setSubject(title);//设置邮件标题 helper.setText(body);//发送普通文本 //发送附件 File f1=new File("c:xxx.txt"); helper.addAttachMent("文件名",File); //发送消息 javaMailSender.send(mimeMessage);//传入消息对象 }
3.7 整合消息队列
消息队列的概念这个黑马教程讲的很好:www.bilibili.com/video/BV15b4y1a7yG
消息队列中的概念:
- 发送方:通常叫做生产者
- 接收方:通常叫做接收者
消息的类型:
同步消息:收到消息发送产生的结果之后才再次发送消息
异步消息:发送之后不需要对方的回应
3.7.1 ActiveMQ
-
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-stater-activemq</artifactId> </dependency>
-
修改配置文件
spring: activemq: broker-url: tcp://localhost:61616
-
Service层注入对象即可
@Atuowired private JmsMessagingTemplate messagingTemplate;
-
放消息
//covertAndSend这个方法会将传入的参数转换成Message类型的,然后在发送到消息队列 //第一个参数指定的是放到哪一个队列中 messagingTemplate.convertAndSend("order.queue",obj);
-
接收消息
//receiveAndConvert这个方法会接收消息,然后将消息转换成我们指定的类型 //第一个参数指定的是从哪一个队列取出 messagingTemplate.receiveAndConvert("order.queue",XXX.class);
-
自动消费消息
第9步是我们自己手动消费消息,但是在实际开发中都是自动处理消息。
消息自动消费是jms提供了一个监听器,使用@JmsListener(destination=“消息队列”)注解,使用@SendTo注解还可以把消息接着转发给其他消息队列
@Component public class xxxx{ //自动处理消息1 @JmsListener(destination="xxxx") public void receive1(X x){ } //将方法的返回值发送到另一个队列 @JmsListener(destination="xxxx1") @SendTo("")//将receive2的返回值发送到xxxx2消息队列 public String receive2(X x){ } }
3.7.2 RabbitMQ
-
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-stater-amqp</artifactId> </dependency>
-
修改配置文件
spring: rabbitmq: host: localhost port: 5627
-
编写配置类
@Configuration public class RabbitMQConfiguration{ //1.定义消息队列 @Bean public Queue XXXQueue(){ return new Queue("定义队列名"); } //2.创建交换机(一个交换机可以绑定多个消息队列) @Bean public DirectExchange xxxxExchange(){ return new DirectExchange("定义交换机名xxxx"); } //3.绑定交换机 @Bean Binding bindingDirect(){ //第一个参数是创建交换机的方法 //第二个参数是创建消息队列的方法 //第三个参数是给这个绑定起一个名字 return BindingBuilder.bind(XXXQueue()).to(xxxxExchange()).with("xxxx"); } }
-
注入依赖
@AutoWired private AmqpTemplate amqpTemplate;
-
调用方法
//第三个参数是要传递的参数 amqpTemplate.convertAndSend("交换机名","绑定名",data);
-
创建一个自动处理消息的监听器
@Component public class xxxx{ //注解的参数是接受的消息队列名 @RabbitListener(queues="xxxx") //方法的参数是接收的参数 public void receive1(XXX xx){ } }
3.7.3 RocketMQ
-
导入依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency>
-
修改配置文件
rocketmq: name-server: localhost:9876 #给生产者设置分组 producer: group: xxxx
-
注入依赖
@Autowired private RocketMQTemplate rocketMQTemplate;
-
生产消息
rocketMQTemplate.convertAndSend("发送到哪个队列",传递的值);
-
消费消息
//使用监听器自动消费消息(需要实现接口) @Component @RocketMQMessageLinstener(topic="消费队列名",consumerGroup="消费者所属的组") public class MessageListener implements RocketMQLinstener<String>{ //实现里面的方法 }
3.7.4 Kafka
- 导入依赖
- 编写配置文件
- 使用
3.8 整合监控平台
应用程序将可监控的参数暴漏出来
监控程序将可监控程序的参数整合显示
3.8.1 可视化监控平台Spring Boot Admin
3.8.1.1 服务端
-
导入依赖
<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>和boot版本号对应</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
-
启动类添加@EnableAdminServer注解
-
启动客户端、启动服务端
-
访问服务端web页面即可
3.8.1.2 客户端
-
导入依赖
<dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>和boot版本号对应</version> </dependency>
-
修改配置文件
spring: boot: admin: client: url: http://服务端地址 server: port:xxx #配置显示信息 management: endpoint: health: #将health所有的属性暴漏出来[如果需要开放其他断电,就将那个端点设为true] show-details: always endpoints: web: exposure: #web页面可以请求哪些【】 include: "*"
-
启动项目
三、原理篇
0.前置知识
-
ApplicationContextInitializer:可以对容器上下文环境做一些操作,例如运行环境属性注册等。在IOC容器创建完成之后执行
使用方法: 1.自定义类,实现ApplicationContextInitializer接口的Initializer方法 2. 然后在MATE-INF/spring.factories文件中指定实现类
-
ApplicationListener:监听容器的事件,当执行某个事件的时候可以触发监听器的对应操作。可以用来加载资源、开启定时任务等
使用方法: 1.自定义类,实现ApplicationListener接口 2.在MATE-INF/spring.factories文件中指定实现类
-
BeanFactory:Bean容器的根接口,提供Bean对象的创建、配置、依赖管理等功能
-
BeanDefinition:用于描述Bean的基本信息,例如:名称、属性、接口、注解等等。在Spring中,Bean创建之前都需要封装成对应的BeanDefinition,然后根据BeanDefinition进一步创建Bean对象
-
BeanFacotoryPostProcessor:是Bean工厂的后置处理器,通常用于新增BeanDefiniton。当BeanFactory准备好了后(还没有创建bean之前),会调用该类的postProcessBeanFactory方法。
例子:一个类没有加任何注解,他就不能自动加入容器。但是我们可以使用这个东西将这个类加入容器
-
Aware:是感知接口。bean实现该接口后,spring程序可以在执行过程中的对应阶段执行bean的对应方法。
使用方法:自定义一个类,加入容器,然后实现该接口的子接口中的方法即可
-
InitializingBean:初始化接口。当bean被实例好之后,会回调里面的函数,通常用于资源的加载
使用方法:自定义一个类,加入容器,然后实现该接口的方法即可
-
DisposableBean:销毁接口。当bean销毁之前,会回调里面的函数,经常用于资源的释放
使用方法:自定义一个类,加入容器,然后实现该接口的方法即可
-
BeanPostProcessor:bean的后置处理器。当bean对象初始化之前以及初始化之后,会调用该接口的对应方法
postProcessBeforeInitialization:bean对象初始化之前调用 postProcessAfterInitialization:Bean对象初始化之后调用
1.自动配置原理
1.1 设计思想
-
整理开发中常用的技术【技术集A】
-
整理常用技术的参数【设置集B】
-
初始化springboot基础环境,加载用户自定义的bean和导入其他坐标,形成初始环境【也就是我们自己的项目】
-
将技术集A包含的所有技术都定义出来,在Spring/SpringBoot启动时全部加载
-
将技术集A中具有使用条件的技术约定出来,设置成按条件加载,由开发者决定是否使用该技术【@ConditionalOnXXXX注解控制】
例如:我的项目不需要Redis,加载Redis干嘛,因此使用注解控制是否加载
-
将设置集B作为默认配置加载(约定大于配置),减少开发者配置工作量
-
开放设置集B的配置覆盖接口,由开发者根据自身需要决定是否覆盖默认配置
1.2 配置原理
- SpringBoot项目的启动类上有一个注解
@SpringBootApplication
@SpringBootApplication
注解是一个合成注解,由@SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
三个注解合成- 其中
@EnableAutoConfiguration
是自动配置的核心注解,该注解通过@Import注解读取jar包里面的META-INF/spring.factories
文件,加载配置类。 - 这些配置类中会包含一些条件注解例如@ConditionalOnClass、@ConditionalIOnBean。通过条件注解判断是否将其导入Spring容器中。
2.自定义stater
案例:统计IP的访问次数
基本思路:
- 当导入依赖的时候,回去他的MATE-INF目录下的spring.factories文件中读取自动配置类的地址
- 读取到自动配置类的地址后会找到对应的类
- 根据条件加载指定的类【@ConditionalOnClass等】
2.1 基本功能实现
-
创建一个SpringBoot项目【可以把pom文件中不需要的干掉、test模块也可以干掉、启动类也可以干掉】
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> </dependencies>
-
编写业务类【根据需要添加依赖】
/** * 默认不需要加入容器【因为自动配置类里面使用@Bean注解加进去了】 */ public class IpCountService { private Map<String,Integer> ipMap=new HashMap<String,Integer>(); @Autowired private HttpServletRequest request;//这个对象由使用该stater的项目提供 public void ipCount(){ String ipAddress = request.getRemoteAddr();//ip地址 System.out.println("stater获取了访问地址:"+ipAddress); Integer count = ipMap.get(ipAddress); if (count==null){//如果区出次数来是空,则说明之前没有访问过 ipMap.put(ipAddress,1); }else {//如果不为空则说明之前访问过 ipMap.put(ipAddress,count+1); } }
<dependencies> <!--添加了web依赖,因为前面需要HttpServletRequest对象--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
编写自动配置类【需要开启什么功能,在配置类上开启】
@Configuration public class Ipautofiguration { @Bean //将自定义的业务类交给自动控制类管理 public IpCountService ipCountService(){ return new IpCountService(); } }
-
在resources下创建MATE-INF/spring.factories文件,编辑文件指定自动配置类是哪个
#autoconfigure.EnableAutoConfiguration用来指定自动配置文件在哪里 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ fun.altman/autoconfiguration.Ipautofiguration#自定义的自动配置类全路径
-
将自定义的stater安装到仓库中
-
在其他项目中导入自定义的stater,然后使用
2.2 开启额外的功能
说明:开启额外的功能需要在自动配置类上设置
需求:开启定时任务,定时打印ip的访问次数
-
自定义stater中导入依赖【这里使用定时任务,因此不需要导入依赖】
-
开启定时任务【因为没有启动类,因此在配置类上开启】
@Configuration @EnableScheduling//开启定时任务 public class Ipautofiguration { @Bean //将自定义的业务类交给自动控制类管理 public IpCountService ipCountService(){ return new IpCountService(); } }
-
在指定方法上开启定时任务
/** * 默认不需要加入容器【因为自动配置类里面使用@Bean注解加进去了】 */ public class IpCountService { private Map<String,Integer> ipMap=new HashMap<String,Integer>(); @Autowired private HttpServletRequest request;//这个对象由使用该stater的项目提供 public void ipCount(){ String ipAddress = request.getRemoteAddr();//ip地址 System.out.println("stater获取了访问地址:"+ipAddress); Integer count = ipMap.get(ipAddress); if (count==null){//如果区出次数来是空,则说明之前没有访问过 ipMap.put(ipAddress,1); }else {//如果不为空则说明之前访问过 ipMap.put(ipAddress,count+1); } } @Scheduled(cron = "0/5 * * * * ?") public void printInfo(){ for (Map.Entry<String, Integer> entry : ipMap.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("IP地址:"+key+"访问次数:"+(value==null?0:value)); } } }
-
测试使用
2.3 stater从项目配置文件中读取配置
说明:
- 如果在配置类中使用@EnableConfigurationProperties指定读取信息的属性类,那么属性类不需要加@Component注解。【但是其他地方需要动态读取的时候会出现问题】
- 如果配置类中不使用@EnableConfigurationProperties指定读取信息的属性类,那么属性类需要加@Component注解,然后配置类中使用@Import将属性类加入容器【这种不会出现动态读取错误】
- 注解的value需要动态读取属性类的属性时可以使用
${配置文件的key:默认值}$
,如果动态读取类的值使用#{bean的名字.属性名}
-
在stater中创建一个属性类,读取信息
//这里使用@Component注解是因为不加它,不让用@ConfigurationProperties注解 @Component("ipProperties") @ConfigurationProperties("fun.altman.ip") public class IpProperties { /** * 刷新的秒数,默认值是5,如果配置文件有对应的值会覆盖 */ private Integer second=5; /** * 日志输出模式,默认值是all,如果配置文件有对应的值会覆盖 */ private String model=Model.ALL.value; public Integer getSecond() { return second; } public void setSecond(Integer second) { this.second = second; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } /** * 内部枚举类 */ public enum Model { ALL("all"), SIMPLE("simple"); private String value; Model(String value) { this.value = value; } public String getValue() { return value; } } }
-
配置类中将属性类加入容器
@Configuration @EnableScheduling //因为项目的包只会扫描项目它的包,不会扫描stater的包,因此需要使用@Import加入容器 @Import(IpProperties.class) public class Ipautofiguration { @Bean //将自定义的业务类交给自动控制类管理 public IpCountService ipCountService(){ return new IpCountService(); } }
-
将属性类的值读取到注解的value中
/** * 默认不需要加入容器【因为自动配置类里面使用@Bean注解加进去了】 */ public class IpCountService { @Autowired private IpProperties ipProperties; private Map<String,Integer> ipMap=new HashMap<String,Integer>(); @Autowired private HttpServletRequest request;//这个对象由使用该stater的项目提供 public void ipCount(){ String ipAddress = request.getRemoteAddr();//ip地址 System.out.println("stater获取了访问地址:"+ipAddress); Integer count = ipMap.get(ipAddress); if (count==null){//如果区出次数来是空,则说明之前没有访问过 ipMap.put(ipAddress,1); }else {//如果不为空则说明之前访问过 ipMap.put(ipAddress,count+1); } } @Scheduled(cron = "0/#{ipProperties.second} * * * * ?") public void printInfo(){ if (ipProperties.getModel().equals("all")){ System.out.println("日志详细模式"); } if (ipProperties.getModel().equals("simple")){ System.out.println("日志简洁模式"); } for (Map.Entry<String, Integer> entry : ipMap.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("IP地址:"+key+"访问次数:"+(value==null?0:value)); } } }
2.4 yml文件中显示字段提示
-
在stater中加入下面的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency>
-
然后清空安装一下stater
-
在stater的target目录下会出现下面这个文件,将这个文件复制到resources下的MATE-INF文件夹下
-
然后将刚才添加的依赖删除,刷新pom文件【这个我删了,项目使用的时候就没有提示了】
-
如果需要提示,可以在那个json文件的hints中写值,具体怎么写可以参考下面的这个案例
2.5 最后说明
如果需要统计所有的方法,可以加一个拦截器,拦截器也可以在stater中定义,定义方法和在springboot中一样。相对于在boot中加拦截器,在stater中的拦截器可以减少代码侵入
3.SpringBoot启动流程
SpringBoot的启动,本质就是加载各种配置信息,然后初始化IOC容器并返回的过程
首先,当我们启动项目的时候,启动类中会调用SpringApplication.run()方法,这个方法由两部分构成分别是new SpringApplication()方法和run()方法。
new SpringApplication方法的功能是创建SpringApplication对象。在创建对象的过程中会做三个事情:
-
确认应用的类型【web应用,还是其他的应用】。一般情况下是servlet类型,这种类型的应用将来会自动启动tomcat
-
从spring.factories中加载ApplicationContextInitialier、加载ApplicationListener
-
记录主启动类,将来做包扫描使用
当对象创建好之后,会调用对象的run方法。run方法会做以下几个事情:
- 准备环境对象Enviroment,里面会封装当前应用运行环境的参数,例如环境变量等
- 打印banner
- 实例化容器context,仅仅是创建ApplicationContext对象
- 容器创建好之后,为容器设置Environment、BeanFacotryPostProcessor,并加载主类对应的BeanDefinition
- 刷新容器,这里会真正创建bean实例
- 最后返回容器
4.IOC容器初始化流程
IOC容器的初始化,核心工作实在是在AbstarctApplicationContext的refresh方法中完成的,它主要做了如下几件事情:
- 准备BeanFactory,在这里会设置很多属性,例如:类加载器、Environment等
- 执行BeanFactory的后置处理器,在这一阶段会扫描要放入容器中的bean信息 ,得到对应的beanDefinition【只扫描,不创建】
- 注册BeanPostProcessor【bean的后置处理器】,我们自定义的BeanPostProcessor就是在这一步被加载的
- 启动tomcat
- 实例化容器中非懒加载的单例bean,多例bean和懒加载的bean不会在这个阶段被实例化,会在将来用到的时候再创建
- 当容器初始化完毕之后,会执行扫尾工作,比如清除缓存等
5.Bean生命周期
Bean的生命周期总的来说有4个阶段,分别是:创建对象,初始化对象,使用对象以及销毁对象,而且这些工作大部分是交给Bean工厂的doCreateBean方法完成的
首先再创建对象阶段,先通过构造方法实例化对象,然后进行依赖注入
其次在对象创建完毕之后,会进行一些初始化操作
- 执行aware感知接口的回调方法
- 执行BeanPostProcessor【bean的后置处理器】的postProcessBeforeInitialization方法
- 执行InitializingBean接口的回调,这一步如果bean中有方法加了@PostConstruct注解会先执行注解标注的方法
- 执行BeanPostProcessor【bean的后置处理器】的postProcessAfterInitialization方法
以上扩展点执行完毕之后,bean的初始化也就完毕了,接下来开发人员就可以使用对象。
最后在容器销毁之前会先销毁对象,此时会执行DisposableBean接口的回调,这一步如果bean中有方法加了@PreDestory注解会先执行标注的方法
6.Bean的循环依赖
什么是循环依赖?
循环依赖是依赖闭环的问题,例如A依赖B,B又依赖A。如下图
Spring容器的三级缓存【一般情况下前两级缓存就能解决,第三级缓存是为了解决动态代理问题】
Bean的循环依赖指的是A依赖B,B又依赖A这样的依赖闭环问题,在Spring中,通过三个对象缓存区来解决循环依赖问题,这三个缓存区被定义到了DefaultsingletonBeanRegistry中,分别是singletonobjects用来存储创建完毕的Bean,earlvsingletonobjecs用来存储未完成依赖注入的Bean,还有singletonfactories用来存储创建Bean的工厂。假如说现在A依赖B,B依赖A,整个Bean的创建过程是这样的:
首先,调用A的构造方法实例化A,当前的A还没有处理依赖注入,暂且把它称为半成品,此时会把半成品A封装到一个ObjectFactory中,并存储到springFactories缓存区
接下来,要处理A的依赖注入了,由于此时还没有B,所以得先实例化一个B,同样的,半成品B也会被封装到objectFactory中并存储到springFactory缓存区
紧接着,要处理B的依赖注入了,此时会找到springFactories中A对应的objecFactory,调用它的getObject方法得到刚才实例化的半成品A(如果需要代理对象,则会自动创建代理对象,将来得到的就是代理对象),把得到的半成品A注入给B,并同时会把半成品存入到earlvsingletonobjects中,将来如果还有其他的类循环依赖了A,就可以直接从earSingletonobiects中找到它了,那么此时springFactories中创建A的ObjectFactory也可以删除了
至此,B的依赖注入处理完了后,B就创建完毕了,就可以把B的对象存入到singletonobjects中了,并同时删除掉springFactories中创建B的ObjectFactoryB创建完毕后,就可以继续处理A的依赖注入了,把B注入给A,此时A也创建完毕了,就可以把A的对象存储到singletonObjects中,并同时删除掉earlySingletonObjects中的半成品A
截此为止,A和B对象全部创建完毕,并存储到了singletonobiects中,将来通过容器获取对象,都是从singletonObejcts中获取
7.SpringMVC执行流程
使用了SpringMVC后,所有的请求都需要经过DispatcherServlet前端控制器,该类中提供了一个doDispatch方法,有关请求处理和结果响应的所有流程都在该方法中完成。
首先,借助于HandlerMapping处理器映射器得到处理器执行链,里面封装了HandlerMethod代表目标controler的方法,同时还通过一个集合记录了要执行的拦截器
接下来,会根据HandlerMethod获取对应的HandlerAdapter处理器适配器,里面封装了参数解析器以及结果处理器
然后,执行拦截器的preHandle方法
接下来是核心,通过HandlerAdapter处理器适配器执行目标controler的方法,在这个过程中会通过参数解析器和结果处理器分别解析浏览器提交的数据以及处理Controller方法返回的结果
然后,执行拦截器的postHandle方法,
最后处理响应,在这个过程中如果有异常抛出,会执行异常的逻辑,这里还会执行全局异常处理器的逻辑,并通过视图解析器ViewResolver解析视图,再渲染视图,最后再执行拦截器的aftercompletion