Spring Boot 使用 slf4j 日志
项目中使用的是 slf4j 的 logback 来输出日志,效率比较高,Spring Boot 提供了一套日志系统,logback 是最优的选择。
外观模式
为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。可以降低访问复杂系统的内部子系统时的复杂度,简化客户端之间的接口。属于 23 种设计模式中的结构型设计模式。
优点: 1、减少系统相互依赖。 2、提高灵活性。 3、提高了安全性。
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
产品接口
public interface Shape {
public void draw();
}
定义子系统的外观或者门面。该子系统中包括多个产品组件
class Circle implements Shape{
public void draw() {
System.out.println("绘制一个圆形");
}
}
class Square implements Shape{
public void draw() {
System.out.println("绘制一个正方形");
}
}
class ShapeFacade {
private Circle circle;
private Square square;
public ShapeFacade(){
circle=new Circle();
square=new Square();
}
public void drawCircle(){
circle.draw();
}
public void drawSquare(){
square.hashCode();
}
public void drawCircleAndSqure(){
circle.draw();
square.draw();
}
}
slf4j 介绍
SLF4J 即简单日志门面,不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单 Facade,允许最终用户在部署其应用时使用其所希望的日志系统。
这里的意思就是:只需要按统一的方式写记录日志的代码,而无需关心日志是通过哪个日志系统,以什么风格输出的。因为它们取决于部署项目时绑定的日志系统。例如,在项目中使用了 slf4j 记录日志,并且绑定了 log4j,即导入相应的依赖,则日志会以 log4j 的风格输出;后期需要改为以 logback 的风格输出日志,只需要将 log4j替换成 logback 即可,不用修改项目中的代码。这对于第三方组件的引入的不同日志系统来说几乎零学习成本,况且它的优点不仅仅这一个而已,还有简洁的占位符的使用和日志级别的判断。
slf4j-log4j 通过 slf4j 调用 log4j 的实现
- 1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- 排除自带的logback依赖 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j</artifactId>
<version>1.3.8.RELEASE</version>
</dependency>
- 2.在 resources 根目录下创建一个 log4j 的配置文件 log4j.properties
log4j.rootLogger=DEBUG, stdout 根日志记录器,参数 1 为需要输出的日志等级,
参数 2 为日志输出的目标地名称 stuout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender 设置 stdout 是控制台
输出
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 配置日志输出的
格式
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
- 3.编程中使用日志记录器输出用户自定义日志信息
Log4j 输出的目的地
org.apache.log4j.ConsoleAppender(控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
application.yml 文件中对日志的配置:
logging:
config: logback.xml
level: 针对不同的包可以设置不同的日志输出等级,基本格式为【包名称: 等级】
com.yan.dao: trace
logging.config 是用来指定项目启动的时候,读取哪个配置文件,这里指定的是日志配置文件是根路径下的logback.xml 文件,关于日志的相关配置信息,都放在 logback.xml 文件中了。logging.level 是用来指定具体的mapper 中日志的输出级别,例如配置表示 com.yan.dao 包下的所有 mapper 日志输出级别为 trace,会将操作数据库的 sql 打印出来,开发时设置成 trace 方便定位问题,在生产环境上,将这个日志级别再设置成 error
级别即可。
常用的日志级别按照从高到低依次为:ERROR、WARN、INFO、DEBUG 和 TRACE。可以通过日志输出等级来控制日志输出的详细程度。
logback.xml 配置文件解析
在 application.yml 文件中,指定了日志配置文件 logback.xml,logback.xml 文件中主要用来做日志的相关配置。在 logback.xml 中可以定义日志输出的格式、路径、控制台输出格式、文件大小、保存时长等。
定义日志输出格式和存储路径
<configuration> 可以理解为定义常量,name 就是常量名称,value 就是对应的值
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level
%logger{36} - %msg%n" />
<property name="FILE_PATH" value="D:/logs/demo.%d{yyyy-MM-dd}.%i.log" />
</configuration>
配置文件的含义:首先定义一个格式,命名为 LOG_PATTERN,该格式中%date 表示日期,%thread 表示线程名,%-5level 表示级别从左显示 5 个字符宽度,%logger{36}表示 logger 名字最长 36 个字符,%msg 表示日志消息,%n 是换行符。然后再定义一下名为 FILE_PATH 文件路径,日志都会存储在该路径下。%i 表示第 i 个文件,当日志文件达到指定大小时,会将日志生成到新的文件里,这里的 i 就是文件索引,日志文件允许的大小可以设置。这里需要注意的是,不管是 windows 系统还是 Linux 系统,日志存储的路径必须要是绝对路径。
定义控制台输出
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder><!-- 按照上面配置的 LOG_PATTERN 来打印日志 -->
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
</configuration>
使用节点设置个控制台输出 class="ch.qos.logback.core.ConsoleAppender"的配置,定义为CONSOLE。使用上面定义好的输出格式 LOG_PATTERN 来输出,使用${}引用进来即可。
定义日志文件的相关参数
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按照上面配置的 FILE_PATH 路径来保存日志 -->
<fileNamePattern>${FILE_PATH}</fileNamePattern>
<maxHistory>15</maxHistory> 日志保存 15 天
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize> 单个日志文件的最大,超过则新建日志文件存储
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<!-- 按照上面配置的 LOG_PATTERN 来打印日志 -->
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
</configuration>
定义日志输出级别
<configuration>
<logger name="com.yan" level="INFO" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
Spring Boot 的项目属性配置
在项目中很多时候需要用到一些配置的信息,这些信息可能在测试环境和生产环境下会有不同的配置,后面根据实际业务情况有可能还会做修改,针对这种情况不能将这些配置在代码中写死,最好就是写到配置文件中。比如可以把这些信息写到 application.yml 文件中。
在具体应用中实际上 application.properties 和 application.yml 都可以使用,并允许同时使用。如果同时进行配置,且配置冲突,则 properties 优先于 yml。
少量配置信息的情形
例如在微服务架构中,最常见的就是某个服务需要调用其他服务来获取其提供的相关信息,那么在该服务的配置文件中需要配置被调用的服务地址,比如在当前服务里需要调用订单微服务获取订单相关的信息,假设订单服务的端口号是 8002,那可以做配置。
server:
port: 8001
# 配置微服务的地址
url: # 自定义的订单微服务的地址。不是系统预定义的配置,所以不会出现任何提示
orderUrl: http://localhost:8002
然后在业务代码中如何获取到这个配置的订单服务地址呢?可以使用@Value 注解来解决。在对应的类中加上
一个属性,在属性上使用@Value 注解即可获取到配置文件中的配置信息
@RestController
@RequestMapping("/test")
public class ConfigController {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigController.class);
@Value("${url.orderUrl}") 其中的内容为 SpEL 即 Spring 表达式语言,可以获取配置文件中 url.orderUrl
的值
private String orderUrl;
@RequestMapping("/config")
public String testConfig() {
LOGGER.info("=====获取的订单服务地址为:{}", orderUrl);
return "success";
}
}
@Value 注解上通过${key}即可获取配置文件中和 key 对应的 value 值。启动一下项目在浏览器中输入localhost:8080/test/config 请求服务后,可以看到控制台会打印出订单服务的地址:
=====获取的订单服务地址为:http://localhost:8002
说明成功获取到了配置文件中的订单微服务地址,在实际项目中也是这么用的,后面如果因为服务器部署的原因,需要修改某个服务的地址,那么只要在配置文件中修改即可。
多个配置信息的情形
这里再引申一个问题,随着业务复杂度的增加,一个项目中可能会有越来越多的微服务,某个模块可能需要调用多个微服务获取不同的信息,那么就需要在配置文件中配置多个微服务的地址。可是在需要调用这些微服务的代码中,如果这样一个个去使用@Value 注解引入相应的微服务地址的话,太过于繁琐,也不科学。所以在实际项目中,业务繁琐,逻辑复杂的情况下,需要考虑封装一个或多个配置类。
例如:假如在当前服务中,某个业务需要同时调用订单微服务、用户微服务和购物车微服务,分别获取订单、用户和购物车相关信息,然后对这些信息做一定的逻辑处理。那么在配置文件中,需要将这些微服务的地址都配置好。
orderUrl: http://localhost:8002 订单微服务的地址,注意 key 值不能重复,但是允许 key 对应的值是集合
类型
userUrl: http://localhost:8003 用户微服务的地址
shoppingUrl: http://localhost:8004 购物车微服务的地址
实际上在实际业务中,可能远远不止这三个微服务,甚至十几个都有可能。对于这种情况可以先定义一个实际上在实际业务中,可能远远不止这三个微服务,甚至十几个都有可能。对于这种情况可以先定义一个个专门用于存储配置参数的类:
@Component 定义受管 bean,否则 Spring 无法注入配置参数值
@ConfigurationProperties(prefix = "url") 用于读取配置信息,声明配置以 url 开头。需要解析的 key 是以 url. 开始的
public class MicroServiceUrl { 专门用于存储配置信息的类
private String orderUrl; 其中的属性名称和 key 对应的除去 url.部分之外的内容一致,例如这里对应
url.orderUrl
private String userUrl;
private String shoppingUrl;
// 省去 get 和 set 方法
}
使用@ConfigurationProperties 注解并且使用 prefix 来指定一个前缀,然后该类中的属性名就是配置中去掉前缀后的名字,一一对应即可。即:前缀名+属性名就是配置文件中定义的 key。同时,该类上需要加@Component注解,把该类作为组件放到 Spring 容器中,让 Spring 去管理,使用的时候直接注入即可。需要注意的是,使用@ConfigurationProperties 注解需要导入它的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
注意:使用@Value 获取配置数据和通过配置参数 bean 的方式获取配置数据,两者不互斥,可以同时使用。
@RestController
@RequestMapping("/test")
public class TestController {
@Value("${url.orderUrl}")
private String orderURL;
@Autowired
private MicroServiceUrl microServiceUrl;
private static final Logger logger= LoggerFactory.getLogger(TestController.class);
@GetMapping("/params")
public String params(){
logger.warn("========="+orderURL);
logger.error("========="+microServiceUrl.getOrderUrl());
logger.error("========="+microServiceUrl.getUserUrl());
return "success";
}
}
指定项目配置文件
在实际项目中,一般有两个环境:开发环境和生产环境。开发环境中的配置和生产环境中的配置往往不同,比如环境、端口、数据库、相关地址等。实际上不可能在开发环境调试好之后,部署到生产环境后,又要将配置信息全部修改成生产环境上的配置,这样太麻烦,也不科学。最好的解决方法就是开发环境和生产环境都有一套对用的配置信息,然后当在开发时,指定读取开发环境的配置,当将项目部署到服务器上之后,再指定去读取生产环境的配置。
Spring Boot 中的 MVC 支持
Spring Boot 的 MVC 支持主要来最常用的几个注解,包括@RestController 用于声明控制器、@RequestMapping用于实现方法映射地址、@PathVariable 用于接受路径中的参数、@RequestParam 用于接受 request 请求中的参数以及@RequestBody 用于接受 application/json 类型的请求信息。主要掌握几个注解常用的使用方式和特点。
@RestController
@RestController 是 Spring Boot 新增的一个注解,等价于@Controller+@ResponseBody
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
String value() default "";
}
@RequestMapping
@RequestMapping 是一个用来处理请求地址映射的注解,它可以用于类上,也可以用于方法上。在类的级别上的注解会将一个特定请求或者请求模式映射到一个控制器之上,表示类中的所有响应请求的方法都是以该地址作为父路径;在方法的级别表示进一步指定到处理方法的映射关系。
该注解有 6 个属性,一般在项目中比较常用的有三个属性:value、method 和 produces。
-
value 属性:指定请求的实际地址,如果注解中只有一个 value 属性时,value=可以省略不写
-
method 属性:指定请求的类型,主要有 GET、PUT、POST、DELETE,默认为 GET。如果没有对应请求方法的定义,则页面上报错 type=Method Not Allowed, status=405
-
produces 属性:指定返回内容类型,如 produces = “application/json; charset=UTF-8”
@RestController
@RequestMapping(value = "/test", produces = "application/json; charset=UTF-8")
public class TestController {
@RequestMapping(value = "/bbbb", method = RequestMethod.GET)
public String testGet() {
return "success";
}
}
针对 GET、POST、DELETE 和 PUT 四种不同的请求方式是有相应注解的,例如@GetMapping、@PostMappging等,使用对应请求的注解后则不用每次在@RequestMapping 注解中加 method 属性来指定,上面的 GET 方式请求可以直接使用@GetMapping(“/bbbb”)注解,效果一样。相应地 PUT 方式、POST 方式和 DELETE 方式对应注解分别为@PutMapping、@PostMapping 和 DeleteMapping。
@PathVariable
@PathVariable 注解主要是用来获取 url 参数,Spring Boot 支持 restfull 风格的 url,比如一个 GET 请求携带一个参数 id 过来 localhost:8080/user?id=123,可以将 id 作为参数接收,注解使用@RequestParam。如果使用路径参数则使用@PathVariable 注解。
@GetMapping("/user/{id}") 例如 http://localhost:8080/user/123 这里的{id}对应的就是 123 值。如果请求路
径为 localhost:8080/user/abc 则由于将 abc 无法转换为 Integer 类型所以报错
public String testPathVariable(@PathVariable Integer id) { 将路径参数中的 123 赋值给方法参数 id
System.out.println("获取到的 id 为:" + id);
return "success";
}
这里需要注意一个问题,如果想要 url 中占位符中的{id}值直接赋值到参数 id 中,需要保证 url 中的参数和方法接收参数一致,否则就无法接收。如果不一致的话,其实也可以解决,需要用@PathVariable 中的 value 属性来指定对应关系。
@RequestMapping("/user/{idd}")
public String testPathVariable(@PathVariable(value = "idd") Integer id) {
System.out.println("获取到的 id 为:" + id);
return "success";
}
对于访问的 url,占位符的位置可以在任何位置,不一定非要在最后,比如这样也行/xxx/{id}/user。另外 url也支持多个占位符,方法参数使用同样数量的参数来接收,原理和一个参数是一样的。
@RequestParam
@RequestParam 也是获取请求参数的,@PathValiable 注解也是获取请求参数的,
@RequestParam 和@PathVariable 有什么不同呢?主要区别在于:@PathValiable 是从 url 模板中获取参数值,即这种风格的 url 为 http://localhost:8080/user/{id};而@RequestParam 是从 request 里面获取参数值,即这种风格的 url 为 http://localhost:8080/user?id=1。可以使用该 url 带上参数 id 来测试代码:@GetMapping(“/user”) 例如请求路径为 localhost:8080/user?id=123,将数据 123 赋值给方法中的同名参数,如果参数不是 String 类型,则自动执行数据类型转换。
public String testRequestParam(@RequestParam Integer id) {
System.out.println("获取到的 id 为:" + id);
return "success";
}
可以正常从控制台打印出 id 信息。同样地 url 上面的参数和方法的参数需要一致,如果不一致,也需要使用 value属性来说明,比如 url 为 http://localhost:8080/user?idd=1。
@RequestMapping(“/user”) 例如请求路径为 localhost:8080/user?idd=123,如果没有@RequestParam 注解,则 id 为 null。
public String testRequestParam(@RequestParam(value = "idd", required = false) Integer id) {
System.out.println("获取到的 id 为:" + id);
return "success";
}
除了 value 属性外,还有个两个属性比较常用:required 属性:true 表示该参数必须要传,否则就会报 404 错误,false 表示可有可无,如果没有传递这个参数,则方法参数为 null。defaultValue 属性:默认值,表示如果请求中没有同名参数时的默认值。
从 url 中可以看出,@RequestParam 注解用于 GET 请求上时,接收拼接在 url 中的参数【URL 重写】。除此之外,该注解还可以用于 POST 请求,接收前端表单提交的参数,假如前端通过表单提交 username 和 password两个参数,那可以使用@RequestParam 来接收。
@PostMapping("/form1")
public String testForm(@RequestParam String username, @RequestParam String password) {
System.out.println("获取到的 username 为:" + username);
System.out.println("获取到的 password 为:" + password);
return "success";
}
具体测试种可以使用 postman 来模拟一下表单提交,测试一下接口。但是如果表单数据很多,不可能在后台方法中写上很多参数,每个参数还要@RequestParam 注解。针对这种情况,需要封装一个实体类来接收这些参数,实体中的属性名和表单中的参数名一致即可。
使用实体接收的话,不能在前面加@RequestParam 注解了,直接使用即可。
@PostMapping("/form2")
public String testForm(User user) {
System.out.println("获取到的 username 为:" + user.getUsername());
System.out.println("获取到的 password 为:" + user.getPassword());
return "success";
}
如果写成 public String testForm(User user,String username)则提交的请求参数 username 的值会赋值两个地址,一个 user 中的 username 属性,另外一个是方法的参数 username。可以使用 postman 再次测试一下表单提交,观察一下返回值和控制台打印出的日志即可。在实际项目中,一般都是封装一个实体类来接收表单数据,因为实际项目中表单数据一般都很多。
@RequestBody
@RequestBody 注解用于接收前端传来的实体,接收参数也是对应的实体,比如前端通过 json 提交传来两个参数 username 和 password,此时需要在后端封装一个实体来接收。在传递的参数比较多的情况下,使用@RequestBody 接收会非常方便。
@RequestBody 注解用于 POST 请求上,接收 json 实体参数。它和上面表单提交有点类似,只不过参数的格式不同,一个是 json 实体,一个是表单提交。在实际项目中根据具体场景和需要使用对应的注解即可。