SpringBoot
内容管理
SpringBoot开发技术 — MVC、模板引擎、文件上传下载,日志
昨天耽误了很多时间在自动化测试上面,三个重要的测试注解@SpringBootTest、@WebMvcTest和@DataJpaTest,测试的时候要先建立好H2嵌入式数据库…不然一堆错
JPA实体、持久化
早期的web开发依赖的是JDBC【编程六部,注册驱动,建立Connection…】,但是这个过程只是一个连接,并且Connection是重量级的,所以后面引入池化技术封装抽象;RDBMS: 关系数据库系统; 可选的持久层框架很多: Hibernate【JPA就是以其为基础】,Mybatis【Mybatis-plus】,JdbcTemplate【RedisTemplate】…
- 以前一直使用的Mybatis,就不再介绍了,可以看之前的博客,【包括单纯的Mybatis,Spring继承,SpringBoot集成】,Hibernate是一个开源的轻量级ORM,通过实体(Entity)将数据映射到数据库,并且JPA作为全自动框架,最主要的是自定义的方法要遵守JPA规范
- JdbcTemplate: Spring提供的【RedisTemplate也是】,并非框架,而是对原生JDBC的封装: 解决了模板代码重复、手动提交事务,数据库迁移,数据库逻辑执行异常处理
实体Entity
实体来源于ER模型,实体的作用就是是用面向对象形式表达数据库,操作对象就是操作数据库;这个过程借助EmitityManager,
CrudRepository和JpaRepository
对数据库的基础操作就是直接继承这个类即可,继承之后@Repository可写可不写
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
<S extends T> List<S> saveAll(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
/** @deprecated */
@Deprecated
default void deleteInBatch(Iterable<T> entities) {
this.deleteAllInBatch(entities);
}
void deleteAllInBatch(Iterable<T> entities);
void deleteAllByIdInBatch(Iterable<ID> ids);
void deleteAllInBatch();
/** @deprecated */
@Deprecated
T getOne(ID id);
/** @deprecated */
@Deprecated
T getById(ID id);
T getReferenceById(ID id);
<S extends T> List<S> findAll(Example<S> example);
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
可以看到提供了基本的CRUD的功能,自定义的方法也要遵守JPA的规范,一般IDEA会有提示
we可以再次测试一下,再体验一下DataJpaTest
测试Test包结构与main包结构相同
如果不相同,就可能出现问题: JpaTest 时无法找到 @SpringBootConfiguration - Unable to find a @SpringBootConfiguration when doing a JpaTest
这里就是因为需要加载主类,默认时在同一包下寻找主类,所以一般需要保持main包和test包的主类是相同的
所以当讲JPa的测试类也放在indv/cfeng中,那么就成功的运行了测试的类;JpaTest是注入了Repository对象,所以加载容器
@DataJpaTest
public class ArticleRepositoryTests {
//需要测试,那么首先进行自动注入
@Resource
private BlogUserRepository blogUserRepository;
//驼峰+蛇形命名,测试一条数据的保存查询
@Test
public void saveBlogUser_thenFindIt() {
//创建一条记录
BlogUser cfeng = new BlogUser();
cfeng.setLoginName("cfeng");
cfeng.setFirstName("zhang");
cfeng.setLastName("ning");
//插入数据库,save方法
blogUserRepository.save(cfeng);
//再查询数据,需要注意查询不存在需要手动置空
BlogUser found = blogUserRepository.findById(cfeng.getId()).orElse(null);
//断言查询到;交换比较一下
assertThat(found).isEqualTo(cfeng);
assertThat(cfeng).isEqualTo(found);
}
}
现在测试就是通过的,we进行Jpa测试的时候,就是创建用户将其操作,操作之后再使用断言assertThat即可
使用Lombok插件
实体类的职责是 ----- 贫血模式【领域驱动设计】,字段的增长,getter和setter的声明非常的繁琐,可以使用Lombok进行简化
首先加入Lombok依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
然后在settings中添加Lombok的依赖即可
常使用的几个注解
- @Data : 此注解包含了@Getter和@Setter
- @Getter/@Setter就是生成对应的get和set方法
- @Accessors: 配置set/get的生成结果,比如设置chain为true,那么setter方法就会返回当前对象
- @RequireArgsConstructor/… 都是对构造器的配置;可以简化final + 构造器的自动注入
- @ToString toString方法, 还有其他的诸如equals方法的注解用到再解释
@RequestBody @RequestParam @PathVarible
@RequestBody : 接收前台传递给后台的JSON字符串的内容;只能使用Post请求
@RequestParam: 获取的是url中的具体的name的属性值,将其指定给固定的某个参数【同名的变量不需要使用,@RequestParam只要就是适用于变量名不同的时候将属性值注入】
@PathVaribel: 对应的是Restful风格的请求,获取的是路径中的参数
MVC架构
MVC【Model-View-Controller】是经典的软件架构模式,通常就是将程序的表现层逻辑分为三个模块:Model,View,Controller,这种模式便于将数据的内部表达、数据的输入以及输出拆分开
- Model: 维护数据的内部表达
- View : 数据的展示,对应的就是使用的MustCache
- Controller: 控制来自于View的输入数据,操作Model渲染View
使用MVC架构表现层,实现了高内聚松耦合,比如可以将View切换为其他的模板引擎like Thmeleaf等
模板引擎mustache
MustCache使用模板引擎,直接使用{{}}就可以快速实现字段的注入
之前已经使用过mustache的相关的标签,接下来就是简单再介绍一下
- {{tag}} : 该标签为变量,最基本的标签类型,使用之后,模板引擎将会在 内容源中也就是Model中找到键为name对应的值,如果内容源中不存在任何内容,就不会显示内容;变量的内容是经过HTML转义的,如果需要未转义的内容,需要使用三重括号: {{{}}}
model.setAttribute("xa","<table><tr><td>XSHUIH</td></tr></table>")
{{xa}} -----> XSHUTH
//就是.....
{{{xa}}} ----> 标签
-
{{#tag}}/ {{/tag}} : 这个标签之间组成的部分被称为区块section。作用就是当false、null或者空数组时,tag中的内容会被隐藏,对象或者数组就会进行显示
- 当tag对应的值为false或者空
Shown. {{#ifShow}} Nerver Shown! {{/ifShown}} ifshown: "false" 结果: Shown.
也就是该区块中的内容,类似于vue中的vue-if
- 当tag对应的是非空数组时,在区块中只需要写数组中每一个属性即可,不需要再加当前级别
{{#breathe}} {{firstName}} {{LastName}} <br/> {{/breathe}} { "breathe": [{"firstName": "XX","LastName": "YY"} .....] } 这里就是一个JSON数组,默认就会显示所有的数组成员,对于每一个成员,都会显示其相关的属性 对象类型就是JSON数组的一种特殊情况,元素个数为1
- tag对应的值为Lambda,也就是funciton函数
{{#get}} {{name}} is beautiful {{/tag}} { "name": "cfeng", "get": function() { return funciton(text,render) { return "<b>" + render(text) + "</b>" } } } 结果就是将name属性对应的值传入函数得到结果: <b>....</b>
-
{{^tag}}/{{/tag}} : 反区块,Inverted Section,作用相反,就是v-else; 当tag对应的是null,空,false时,显示内容
//所以其实vue就是以mustache为...
{{^repo}}
Hi , Where are you from ?
{{/repo}}
{
repo: false
}
会显示反区块中的内容
-
{{>tag}} : 子模板,部分:partial,调用以mustache结尾的模板,最后形成一个单页面html
-
{{!tag}} : 注释, 其实使用<!-- – >也可以
-
设置定界符,可以使用{{=<% %>=}} 和<%={{}}=%> 自定义定界符不可包含空格和等号
构建MVC架构的Web应用
这里构建mvc架构,比如像blog demo中增加一个文章录入功能,要求选择作者、填写信息;也就是add部分,MVC架构应用注重交互
比如这里增加一个文章信息录入,我们需要传输的信息只有author,正副标题和正文,那么我们首先创建封装这些属性的操作对象,【这个类不是Entity类,只是操作中产生的类,类似于Mybatis-plus中的vo,那么就放在domain中】
@Accessors(chain = true) //Lombok中设置之后就可以让set方法返回当前对象
@Setter
@Getter
public class SubmitArticleQuery {
private String title;
//副标题,摘要
private String headline;
private String content;
//作者名称
private String author;
}
之后就是设计View的部分,那么还是拆分为header,footer和关键的body,引入Jquery操作【没有前后端分离,JQuery效率还是挺高的,获取Dom很快】
header.mustache
<!-- header子模版,为单体博客页面的头部 设置lang属性指定语言,默认zh-CN,如果写en,浏览器会放翻译-->
<html lang="zh-CN">
<head>
{{!这里就直接引入网页的js即可}}
<script src="http://code.jquery.com/jquery-1.12.4.min.js"></script>
<title>{{title}}</title>
</head>
<body>
<!-- mustache和jsp有些共同点,就是实际上会整合为一个页面,也就是blog页面,但是可以将各个部分给单独拆分出来,所以这里的html和body标签就没有完全 -->
footer就和上面接上就行了,所以核心就是写body中的部分,这就是vue的组件思想的来源
{{!这个组件就是文章录入的核心组件}}
{{> header}}
{{!中间只需要写body中的部分}}
<div class="writing">
<b><a href="/">Home</a></b>
<form class="to-save">
<br> title <br>
<input type="text" name="title">
<br> headline <br>
<input type="text" name="headline">
<br/> content <br/>
<textarea rows="10" cols="70" name="content"></textarea>
<br> Author:
<select name="author">
{{!选择的作者要后台提供}}
{{#blogUsers}}
<option value="{{loginName}}">{{loginName}}</option>
{{/blogUsers}}
</select>
<br>
<input type="submit" value="提交">
</form>
</div>
<script type="text/javascript">
//获取表单的请求
$("form").submit(function () {
//构造请求体
let formObj = {};
//响应的JSON数组,使用
let formArray = $("form").serializeArray();
$.each(formArray,function (i,item) {
formObj[item.name] = item.value;
});
//使用AJAX,创建POST请求,访问8888/article
$.ajax({
type: 'POST',
url: "/article",
//将一个 JavaScript 对象或值转换为 JSON 字符串
data: JSON.stringify(formObj),
contentType: 'application/json',
success: function () {
alert(data);
}
})
})
</script>
{{> footer}}
JQuery至少还是有一定的价值的,不应该摒弃
接下来就是创建controller,上面已经有了view和model了,这里就重写一下HtmlController
/**
* 录入文章数据,writing
*/
@GetMapping("/writing")
public String writeArticle(Model model) {
//填充页面writing的数据
Iterable<BlogUser> blogUserList = blogUserRepository.findAll();
//将writing页面所需要的author和title数据填入
model.addAttribute("title","Writing blog");
model.addAttribute("blogUsers",blogUserList);
//跳转writing页面进行渲染
return "writing";
}
/**
* 写文章之后增加文章,持久化
* 增加文章成功之后应该给一个成功的页面,表明录入文章成功,这里我们就直接返回一个success字符串
*/
@PostMapping("/article")
@ResponseBody
//@RequestBody就是将返回的Json对象整个注入一个某一个对象,而不是一个一个值注入
public String saveArticle(@RequestBody SubmitArticleQuery queryArticle) {
//接收POST请求,前台传入的是author的姓名
BlogUser author = blogUserRepository.findUserByLoginName(queryArticle.getAuthorName());
if(author == null) {
//说明没有这个author,应该报错400
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"This author does not exist");
}
//Aritcle和submitArticle主要区别就是author不同,slug
Article toSave = new Article();
toSave.setAuthor(author);
toSave.setTitle(queryArticle.getTitle());
toSave.setHeadline(queryArticle.getHeadline());
toSave.setContent(queryArticle.getContent());
toSave.setSlug(CommonUtil.toSlug(queryArticle.getTitle()));
//持久化
repository.save(toSave);
//成功操作
return "Success";
}
Controller表现层不应该处理过多的业务逻辑,如果业务很复杂,就会下沉业务层
接下来就是测试一下这个controller和mustache,那么就使用@SpringBootTest进行一体化测试
/**
* 测试writing MVC部分
*/
@Test
public void submitAnArticle() {
System.out.println(">> Submit an article");
//创建一个上传的数据
SubmitArticleQuery articleQuery = new SubmitArticleQuery();
articleQuery.setAuthorName("cfeng");
articleQuery.setTitle("SpringBoot 教程");
articleQuery.setHeadline("headline-- SpringBoot进阶");
articleQuery.setContent("......此处省略一万字");
//使用template模拟请求,不然就postman,post类型需要指定post数据的数据类型,然后再指定响应的类型
ResponseEntity<String> entity = restTemplate.postForEntity("/article",articleQuery,String.class);
//如果查找不到cfeng,那么就会返回错误
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
System.out.println(entity.getBody());
}
这里先手工创建一个JSON对象,post这个JSON对象到article页面,就是增加文章页面,首先就会判断是否有该文章
{“timestamp”:“2022-06-12T14:42:18.944+00:00”,“status”:400,“error”:“Bad Request”,“path”:“/article”}
这里就可以看到,因为我的h2数据库设置的是关闭的时候删除,没有相关的用户,所以就是400
文件上传
之前在SSM中使用的是一个文件上传组件MultipartFile,springboot中,这个依赖在web的starter中
文件上传最主要的就是配置上传文件的存储在服务器的位置upload-dir
为了更方便操作这个路径,we创建一个配置文件映射类FileStorageProperties, 主启动类要Enable来指定搜索整个Properites类,会创建一个对象 【Test类会从同级的容器中获取这些对象】
file: ##自定义的文件存储位置
upload-dir: ./assets
这里的file也是一个自定义的配置内容,创建其配置类来操作整个类
@EnableConfigurationProperties({BlogProperties.class, FileStorageProperties.class}) //扫描需要进行配置文件属性注入的类 ; 会创建一个该类的对象放入容器
@ConfigurationProperties(prefix = "file") //prefix指定表明的为配置文件哪个对象
@Getter
@Setter
public class FileStorageProperties {
//文件上传组件的位置
private Setter uploadDir;
}
nio包中的Path和Paths
早期的java使用的File访问文件系统,File功能优先,出错的时候不会抛异常,不足,所以NIO引入了Path,代表平台无关的平台路径,实际引用的资源不一定存在,内存层面的,实际操作的时候才会检查磁盘, Paths和FIles为工具类(一般加s的都是),Paths包含两个返回path静态工厂方法
NIO中的path就是返回的一个路径对象
- resolve: 将相对路径解析为绝对路径,如果其中参数为String,或者其他的相对路径,就加上\,将两个路径拼接在一起
NIO中的Paths工具类的常用方法:
- toAbsolutePath: 作为绝对路径返回调用的Path对象
- normalize: 消除了所有的冗余 ---- 比如D:…\p2 变为D:\p2
- get: 根据路径获得一个Path对象【类似File】
Files用于操作文件或者目录的工具类
-
copy: 将字节从文件复制到IO流或者从IO流复制到文件
-
createDirctory: 根据Path创建一个目录
-
createFile: 更具路径创建一个文件
-
deleteIfExists: Path对应的文件/目录如果存在,执行删除操作
-
size: 返回path指代的文件的大小
自定义的文件上传业务的处理不需要controller,使用service操作
//文件存储的操作,首先对路径进行初始化,就是将配置的dir通过Paths进行转化为标准路径之后,创建目录,通过multipartFile获得上传的文件,通过UUID随机名称之后,使用Path的resolve拼接为目标路径,拼接之后再将文件流复制到目标路径,这里的选项为操作的行为,选择的是覆盖
private final Path fileStorageLocation;
/**
* @param fileStorageProperties
* @throws Exception
* 构造器的功能就是利用Files工具类把配置的路径转为Path,再使用Files创建对应的目录
*/
public FileStorageService(FileStorageProperties fileStorageProperties) throws Exception {
this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir()).toAbsolutePath().normalize();
//获取Path之后使用Files工具类创建asserts目录
try {
Files.createDirectories(this.fileStorageLocation);
}catch (IOException e) {
e.printStackTrace();
throw new Exception("Could not create hte directory where the uploaded files will be stored",e);
}
}
/**
* 上传文件,使用multipartFile,加密文件名之后将其从multipartFile移动到目标Path下
*/
public String uploadFile(MultipartFile multipartFile) {
//文件名称
String originalName = multipartFile.getOriginalFilename();
//文件的扩展名,就是.之后的部分,如果找不到.或者文件名为null,那就没有扩展名
String extName = (originalName == null || originalName.lastIndexOf(".") <= 0) ? null : originalName.substring(originalName.lastIndexOf("."));
//使用UUID进行加密,之前的使用的MD5,这里使用UUID随机生成
String fileName = UUID.randomUUID().toString() + extName;
//文件名中不能有两个..
if(fileName.contains("..")) {
throw new RuntimeException("Sorry,FileName contains invalid path sequence" + fileName);
}
//根据文件名获取到最终的Path对象,将fileName拼接在最后面
Path target = fileStorageLocation.resolve(fileName);
try{
//将上传的文件转为IO流复制到target路径文件,选项为操作的行为
Files.copy(multipartFile.getInputStream(),target, StandardCopyOption.REPLACE_EXISTING);
}catch (IOException e) {
throw new RuntimeException("Could not store file" + fileName + "please try again !", e);
}
return fileName;
}
之后就是在controller层接收上传的文件,调用service进行存储
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {
private final FileStorageService fileStorageService;
/**
* 前台上传文件发起的是post请求
* 使用MultipartFile进行操作
* 上传的name为file的数据注入给Multipart,使用fileStorageService的upload方法上传文件
* @return String 使用String.format进行格式化,返回的就是/file/文件路径
*/
@PostMapping
public String upLoadFile(@RequestParam("file")MultipartFile multipartFile) {
return String.format("/file/%s",fileStorageService.uploadFile(multipartFile));
}
//下载文件
@GetMapping("/{fileName:.+}")
public ResponseEntity<Resource> download(@PathVariable String fileName, HttpServletRequest request) {
//todo
throw new ResponseStatusException(HttpStatus.NOT_FOUND,"Under construction");
}
}
这样就可以进行文章的上传了,接下来对整个controller进行功能测试,又要借助Mockito
@ExtendWith(SpringExtension.class) //Junit4中为@RunWith(SpringRunner.class) 作用都是实现注入,将对象DI
@SpringBootTest
public class FileUpLoadControllerTests {
private MockMvc mockMvc;
//将容器对象注入
@Resource
private WebApplicationContext webApplicationContext;
@BeforeEach
public void setup() {
//首先就是创建一个mockMvc,这里就利用容器创建
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void whenUploadFile_thenReturnUrl() throws Exception {
//mockMvc模拟请求,之前测试的是get请i去,现在直接使用file
String result = mockMvc.perform(MockMvcRequestBuilders.multipart("/file")
.file(new MockMultipartFile("testName","test.txt",",multipart/form-data","hello upload".getBytes(StandardCharsets.UTF_8)))
).andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse().getContentAsString();
//判断返回的结果包含string
assertThat(result).contains("file");
System.out.println(result);
}
}
文件下载
文件下载还是借助的MultiPartFile部件,Service部分就主要是根据fileName得到最终的文件绝对路径,通过UrlReource就可以更具uri获取这个资源
/**
* @param fileName
* @return
* @throws FileNotFoundException
* 传入参数就是文件名称,将文件名称与Path组合称为最终的目标Path,之后通过UrlResource就可以根据文件路径获得文件资源,这个时候要判断
* 如果资源存在,就返回,如果不存在,就抛出异常
*/
public Resource loadFile(String fileName) throws FileNotFoundException {
//首先获得完整的文件路径
Path filePath = fileStorageLocation.resolve(fileName).normalize();
//使用UrlResource将这个路径的资源获取
try {
Resource resource = new UrlResource(filePath.toUri());
if(resource.exists()) {
return resource;
}else {
//不存在抛异常
throw new FileNotFoundException("file not found" + fileName);
}
} catch (MalformedURLException e) {
throw new FileNotFoundException(("file not found" + fileName));
}
之后在controller中就是返回包含资源的整个响应,那么返回值类型就是ResponseEntity< Resource>
@GetMapping("/{fileName:.+}")
public ResponseEntity<Resource> download(@PathVariable String fileName, HttpServletRequest request) {
//获取文件对象
Resource resource;
try {
resource = fileStorageService.loadFile(fileName);
} catch (FileNotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND,"File not found.");
}
//文件获取成功后写入响应中
return downloadFile(resource,request);
}
/**
* 为了复用,将资源写入响应体,专门的方法进行处理
*/
private ResponseEntity<Resource> downloadFile(Resource resource,HttpServletRequest request) {
String contentType = null;
//首先就是获得请求中的contentType
try {
contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
} catch (IOException e) {
System.out.println("Could not found file type");
}
//之后就是返回响应体
if(contentType == null) {
//请求中没有指定contentType,那么就设置为默认的stream
contentType = "application/octet-stream";
}
//根据contentType分别设置响应体的contentType,header和body【body就是resource,header给一个fileName】
return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION,"attachment;filename=\"" + resource.getFilename())
.body(resource);
}
这里可以将文件上传之后再下载,来测试整个部分
@Test
public void whenUploadFile_thenReturnUrl() throws Exception {
//mockMvc模拟请求,之前测试的是get请i去,现在直接使用file
String result = mockMvc.perform(MockMvcRequestBuilders.multipart("/file")
.file(new MockMultipartFile("file","test.txt",",multipart/form-data","hello cfeng".getBytes(StandardCharsets.UTF_8)))
).andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse().getContentAsString();
//判断返回的结果包含string
assertThat(result).contains("file");
System.out.println(result);
//将上传的文件下载就上面的text.txt文件
MvcResult downloadResult = mockMvc.perform(MockMvcRequestBuilders.get(result).contentType(MediaType.APPLICATION_OCTET_STREAM))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
assertThat(downloadResult.getResponse().getContentAsString()).isEqualTo("hello cfeng");
System.out.println(downloadResult.getResponse().getContentAsString());
}
测试过程是成功的,这里可以看到测试这个controller不是使用的webMvcTest,而是springBootTest,一体化测试文件的上传下载功能;
文件上传下载借助的主要的就是Path,Paths,Files和MultipartFile;前台上传的文件的name就是file
springBoot日志
为避免程序出现错误,但是不能debug的情况,那么就需要提供日志的功能
简单的预设配置
自动配置没有特殊的要求的时候,就是开箱即用的状态,这里可以测试一下,创建一个LogController来进行配置【springboot-starter中就包含spring-boot-starter-logging】其中就包括slf4j
@RestController
@RequestMapping("/log")
public class LogController {
//logger就是日志记录器,通过工厂创建一个记录器,参数class表示记录哪个类的记录,这里就记录本类
Logger logger = LoggerFactory.getLogger(LogController.class);
@GetMapping
public String justShowSomeLog() {
logger.trace("TRACE message"); //追溯信息
logger.debug("DeBug message");
logger.info("IOFO");
logger.warn("WARN");
logger.error("ERROR");
return "Cfeng's Test logger, we can check it";
}
}
访问这个测试的test,可以看到输出的日志到控制台
2022-06-13 17:01:06.896 INFO 16500 --- [ restartedMain] indv.cfeng.BlogApplication : Started BlogApplication in 2.735 seconds (JVM running for 3.36)
2022-06-13 17:02:30.194 DEBUG 16500 --- [nio-8086-exec-1] indv.cfeng.controller.LogController : DeBug message
2022-06-13 17:02:30.194 INFO 16500 --- [nio-8086-exec-1] indv.cfeng.controller.LogController : IOFO
2022-06-13 17:02:30.194 WARN 16500 --- [nio-8086-exec-1] indv.cfeng.controller.LogController : WARN
2022-06-13 17:02:30.194 ERROR 16500 --- [nio-8086-exec-1] indv.cfeng.controller.LogController : ERROR
基础配置
通过上面的例子就可以看到日志的5中等级:
- 跟踪:Trace
- 调试: Debug
- 信息:Info
- 警告:Warn
- 错误:Error
那么就可以在application.yml中进行配置;比如将等级调整为debug
######配置日志########
logging:
file:
name: cfengBlog.log
level:
indv.cfeng : debug
root : warn
config: classpath:logback-spring.xml
比图这里就是: 设置root的级别,也就是所有包的日志等级为warn,这个时候就会显示很多的日志信息
给配置项是以包为日志等级控制单位,比如可以将SpringBoot和其他的各种日志依赖提升为warn,业务代码的日志等级下调到Debug
除了上面的level配置日志等级之外,还可以指定其他的配置项改变行为
- logging.file: 指定日志输出的目标文件,为了避免歧义,高版本使用的logging.file.name
- logging.file.path: 指定日志输出的目标路径
- logging.pattern.console: 指定日志在控制台输出的格式
- logging.pattern.file: 指定日志在文件中输出的格式
详细配置
上面的配置日志文件的方式还是不能够满足要求,为了更复杂的要求,spring Boot支持独立配置,当ClassPath下面包含配置文件时,Spring Boot会默认加载他们:
- logback-spring.xml
- logback.xml
- logback-spirng.groovy
- logback.groovy
推荐使用的时第一个,进行配置文件的配置日志,可以指定logging中的config文件名称
这里可以就配置一个日志文件,设置等级等
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
<property name = "LOGS" value="./logs"/>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1.}): %msg%n%throwable
</Pattern>
</layout>
</appender>
<appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOGS}/spring-boot-logger.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>
%d %p %C{1.} [%t] %m%n
</Pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件大小达到10MB,每天翻覆 -->
<fileNamePattern>
${LOGS}/archived/spring-boot-logger-%d{yyyy-MM-dd}.%i.log
</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>
<!-- 设置全局的日志等级为info-->
<root level="info">
<appender-ref ref="RollingFile"/>
<appender-ref ref="Console"/>
</root>
<!-- 业务模块的包日志等级trace-->
<logger name="indv.cfeng"level="trace" additivity="false">
<appender-ref ref="RollingFile"/>
<appender-ref ref="Console"/>
</logger>
</configuration>
Lombok注解: @Sl4j和@Commonslog
编写日志相关信息时,上面的方法需要借助LoggerFactory获取一个Logger实例,为了消除这个模板代码,Lombok提供了两个注解: @Sl4j以及@Commonslog。使用@Sl4j修改
@Slf4j
public class LogController {
//logger就是日志记录器,通过工厂创建一个记录器,参数class表示记录哪个类的记录,这里就记录本类
// Logger logger = LoggerFactory.getLogger(LogController.class);
@GetMapping
public String justShowSomeLog() {
log.trace("TRACE message"); //追溯信息
log.debug("DeBug message");
log.info("IOFO");
log.warn("WARN");
log.error("ERROR");
使用@Commonslog替代@Sl4j,结果是相同的
在Windows平台输出彩色日志的JANSI
分别在Windows平台与类Unix平台启动Spring Boot项目,可以观察到控制台日志输出的形式存在差异,在Unix平台是日志默认彩色显示,而Windows不能,在Windows平台需要输出彩色日志,需要使用JANSI,在pom.xml中添加依赖
<!-- windows中输出彩色日志 -->
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>1.18</version>
</dependency>
同时需要在spring.xml中输出相关的信息:
<!-- 启用JANSI,输出彩色日志 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %green (%logger{15}) - %msg %n
</pattern>
</encoder>
</appender>
在Windows平台运行Spring Boot项目,控制台的日志输出也是彩色的