【springBoot】文件上传、下载,SpringBoot日志和Mustache

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项目,控制台的日志输出也是彩色的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值