zoukankan      html  css  js  c++  java
  • SpringBoot 入门

    目录

    • 文件结构
    • pom.xml
    • 主程序
    • Controller
    • Service
    • 异常处理
    • 配置
    • 自定义注解以及 AOP
    • 拦截器
    • ApplicationRunner
    • 定时调度
    • logback-spring.xml 配置日志
    • Actuator
    • Prometheus

    文件结构



    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.4.0</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.example</groupId>
        <artifactId>demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>demo</name>
        <description>Demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
            <logstash.logback.version>5.2</logstash.logback.version>
            <prometheus.simple.client.version>0.8.0</prometheus.simple.client.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-tomcat</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-undertow</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
    
            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-registry-prometheus</artifactId>
            </dependency>
    
            <dependency>
                <groupId>io.prometheus</groupId>
                <artifactId>simpleclient</artifactId>
                <version>${prometheus.simple.client.version}</version>
            </dependency>
    
            <dependency>
                <groupId>net.logstash.logback</groupId>
                <artifactId>logstash-logback-encoder</artifactId>
                <version>${logstash.logback.version}</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    最简单的 spring boot 程序可以只引入 spring-boot-starter
    因为这个例子是一个 web 程序,所以改成引入 spring-boot-starter-web
    而 web 的默认服务器是 tomcat,可以通过 exclusion 把它去掉,然后引入 undertow 替换


    主程序

    package com.example.demo;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    

    通过 @SpringBootApplication 注解启动 spring boot 程序


    Controller

    package com.example.demo.controller;
    
    import com.example.demo.annotations.Audit;
    import com.example.demo.entity.User;
    import com.example.demo.service.DemoService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    @RestController
    @RequestMapping("/api/v1")
    public class DemoController {
        @Autowired
        private DemoService demoService;
    
        @Audit("get_all_user_id")
        @GetMapping("/users-id")
        public List<String> getId() {
            return demoService.getUsersId();
        }
    
        @Audit("get_users")
        @GetMapping("/users")
        public List<User> getUsers(@RequestParam(value = "gender", required = false) String gender) {
            return demoService.getUsers(gender);
        }
    
        @Audit("create_user")
        @PostMapping("/users/{id}")
        public void createUser(@PathVariable("id") String id,
                               @RequestBody User user) {
            demoService.createUser(id, user);
        }
    
        @Audit("get_user")
        @GetMapping("/user/{id}")
        public User getUser(@PathVariable("id") String id) {
            return demoService.getUser(id);
        }
    
        @Audit("update_user")
        @PostMapping("/user/{id}")
        public void updateUser(@PathVariable("id") String id,
                               @RequestBody User user) {
            demoService.updateUser(id, user);
        }
    }
    

    用于实现 Rest 接口,这个类的所有 URL 接口都以 "/api/v1" 开头
    @PathVariable 定义的是 URL 路径里的变量
    @RequestParam 定义的是 URL 路径的问号后带的变量
    @RequestBody 是消息体带的变量

    比如

    curl -X GET "http://localhost:9000/api/v1/user/1"
    curl -X GET "http://localhost:9000/api/v1/users?gender=male"
    curl -l -H "Content-type: application/json" -X POST -d '{"name":"han","gender":"male","age":35,"salary":20000}' "http://localhost:9000/api/v1/user/1"
    

    具体的业务交给了 DemoService 类实现
    注解 @Autowired 用于自动初始化类,并实现单例化

    Controller 会自动将请求的 body 携带的数据填到 User 类,User 类的变量名必须和 body 的名字一致

    package com.example.demo.entity;
    
    import java.io.Serializable;
    
    public class User implements Serializable {
        private String name;
        private String gender;
        private int age;
        private float salary;
    
        // 这个空的构造函数是必须的,不然 Controller 无法将 request 的 body 取出
        public User() {
        }
    
        public User(String name, String gender, int age, float salary) {
            this.name = name;
            this.gender = gender;
            this.age = age;
            this.salary = salary;
        }
    
        public void setName(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
        public int getAge() {
            return this.age;
        }
    
        public void setSalary(float salary) {
            this.salary = salary;
        }
        public float getSalary() {
            return this.salary;
        }
    
        public void setGender(String gender) {
            this.gender = gender;
        }
        public String getGender() {
            return this.gender;
        }
    }
    

    这几个接口实现了添加、查看、更改用户信息的功能


    Service

    package com.example.demo.service;
    
    import com.example.demo.entity.User;
    import com.example.demo.exception.DemoException;
    import com.example.demo.properties.UserProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.PostConstruct;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.stream.Collectors;
    
    @Service
    public class DemoService {
        @Autowired
        private UserProperties userProperties;
    
        private Map<String, User> users = new ConcurrentHashMap<>();
    
        @PostConstruct
        public void init() {
            users.put("1", new User("Lin", "male", 30, 20000));
            users.put("2", new User("Zhao", "female", 25, 10000));
        }
    
        public User getUser(String id) {
            if (! users.containsKey(id)) {
                throw new DemoException("Demo-40001", "User not exist");
            }
            return users.get(id);
        }
    
        public List<String> getUsersId() {
            return new ArrayList<>(users.keySet());
        }
    
        public List<User> getUsers(String gender) {
            if (gender != null) {
                if (!gender.equals("male") && !gender.equals("female") ) {
                    throw new DemoException("Demo-40005", "Invalid gender");
                }
    
                return users.values().stream()
                        .filter(user -> user.getGender().equals(gender))
                        .collect(Collectors.toList());
            } else {
                return new ArrayList<> (users.values());
            }
        }
    
        public void createUser(String id, User user) {
            if (users.containsKey(id)) {
                throw new DemoException("Demo-40002", "User already exist");
            } else if (userProperties.getSize() <= users.size()) {
                throw new DemoException("Demo-40003", "User db is full");
            } else if (userProperties.getNameLength() <= user.getName().length()) {
                throw new DemoException("Demo-40004", "User name must <= " + userProperties.getNameLength());
            }
    
            users.put(id, user);
        }
    
        public void updateUser(String id, User user) {
            if (! users.containsKey(id)) {
                throw new DemoException("Demo-40001", "User not exist");
            } else if (userProperties.getNameLength() <= user.getName().length()) {
                throw new DemoException("Demo-40004", "User name must <= " + userProperties.getNameLength());
            }
            users.put(id, user);
        }
    
    }
    

    @PostConstruct 表示在依赖注入完成后调用,正常的初始化顺序是 Construct -> Autowired -> PostConstruct,如果在构造函数使用了 @Autowired 的变量,会不起效果,因为是先执行构造函数再初始化 Autowired 变量,所以如果有这种需求就要用 @PostConstruct,上面这个例子可以在构造函数执行可以不用 @PostConstruct


    异常处理

    package com.example.demo.exception;
    
    public class DemoException extends RuntimeException {
        private final String code;
        private final String message;
    
        public DemoException(String code, String message) {
            super(message);
            this.code = code;
            this.message = message;
        }
    
        public String getCode() {
            return code;
        }
    
        @Override
        public String getMessage() {
            return message;
        }
    }
    
    package com.example.demo.exception;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
    
    import java.io.Serializable;
    
    @ControllerAdvice
    public class DemoExceptionHandler extends ResponseEntityExceptionHandler {
    
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        private class Result implements Serializable {
            private String code;
            private String message;
    
            public String getCode() {
                return code;
            }
            public String getMessage() {
                return message;
            }
    
            private Result(String code, String message) {
                this.code = code;
                this.message = message;
            }
        }
    
        @ExceptionHandler(DemoException.class)
        @ResponseBody
        public ResponseEntity<Result> handleDemoException(DemoException ex) {
            logger.error("Demo Exception", ex.getMessage(), ex);
            Result result = new Result(ex.getCode(), ex.getMessage());
            return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
        }
    
        @ExceptionHandler(Exception.class)
        @ResponseBody
        public ResponseEntity<Result> handleOtherError(Exception ex) {
            logger.error("Unknown Exception", ex.getMessage(), ex);
            Result result = new Result("Demo-50000", ex.getMessage());
            return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    

    通过 @ControllerAdvice 和 ResponseEntityExceptionHandler 可以统一捕获处理程序抛出的异常,@ExceptionHandler(DemoException.class) 和 @ExceptionHandler(Exception.class) 表示这两个函数分别处理抛出的 DemoException 和 Exception,并通过 ResponseEntity 返回给客户端

    DemoService 里抛出的 DemoException 异常或其他异常都会在这里统一处理

    比如如果添加一个已经存在的 user,HTTP 请求的返回内容是

    {
        "code": "Demo-40002",
        "message": "User already exist"
    }
    

    返回码则是 400 Bad Request


    配置

    # application.yaml
    
    # server:
    #   port: 9000
    
    logging:
      level:
        root: INFO
        com:
          example:
            demo: INFO
    
    user:
      size: 10
      name-length: 10
    
    # 暴露 Actuator 的所有接口,并使 health 接口展示所有信息
    # http://localhost:9000/actuator
    # http://localhost:9000/actuator/health
    # http://localhost:9000/actuator/metrics
    # http://localhost:9000/actuator/prometheus
    # 需要在 pom.xml 添加 actuator 包
    management:
      endpoints:
        web:
          exposure:
            include: "*"
      endpoint:
        health:
          show-details: always
    

    spring boot 默认 8080 端口,通过 server.port 可以指定为 9000

    DemoService 读取的 UserProperties 类就是用于获取 application.yaml 的配置项

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-validation</artifactId>
            </dependency>
    
    package com.example.demo.properties;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.stereotype.Component;
    import org.springframework.validation.annotation.Validated;
    
    import javax.validation.constraints.Max;
    import javax.validation.constraints.Min;
    import javax.validation.constraints.NotNull;
    
    @Component
    @EnableConfigurationProperties
    @ConfigurationProperties(prefix = "user")
    @Validated
    public class UserProperties {
        @NotNull
        private int size;
    
        @Min(5)
        @Max(20)
        private int nameLength = 10;
    
        public int getSize() {
            return size;
        }
        public void setSize(int size) {
            this.size = size;
        }
    
        public int getNameLength() {
            return nameLength;
        }
        public void setNameLength(int nameLength) {
            this.nameLength = nameLength;
        }
    }
    

    @EnableConfigurationProperties 表示读取配置文件
    @ConfigurationProperties(prefix = "user") 表示读取 user 配置项
    @Validated、@NotNull、@Min、@Max 用于验证配置的值
    变量名必须和配置文件的一致,有连接符 - 的就用驼峰表示法命名

    可以统一处理默认配置

    package com.example.demo.config;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.env.EnvironmentPostProcessor;
    import org.springframework.core.Ordered;
    import org.springframework.core.env.ConfigurableEnvironment;
    import org.springframework.core.env.MapPropertySource;
    import org.springframework.core.env.PropertySource;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class DemoEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
    
        @Override
        public void postProcessEnvironment(ConfigurableEnvironment environment,
                                           SpringApplication application) {
            Map<String, Object> defaultMap = new HashMap<>();
    
            defaultMap.put("server.port", 9000);
            defaultMap.put("user.size", 100);
            defaultMap.put("user.name-length", 20);
    
            PropertySource<?> propertySource = new MapPropertySource("defaultProp", defaultMap);
            environment.getPropertySources().addLast(propertySource);
        }
    
        @Override
        public int getOrder() {
            return 0;
        }
    }
    

    这样如果 application.yaml 没指定某个配置项,而 defaultMap 又有相应的配置项,那就使用 defaultMap 指定的值

    需要配置 resources/META-INF/spring.factories 文件

    org.springframework.boot.env.EnvironmentPostProcessor=com.example.demo.config.DemoEnvironmentPostProcessor
    

    这样这个类才起作用


    自定义注解以及 AOP

    package com.example.demo.annotations;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Audit {
        String value() default "";
    }
    

    自定义注解并加到 Controller 用于修饰 REST 接口

        @Audit("get_all_user_id")
        @GetMapping("/users-id")
        public List<String> getId() {
            return demoService.getUsersId();
        }
    

    然后要实现一个 AOP(Aspect Oriented Programming) 对这个注解进行拦截处理

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
    package com.example.demo.aop;
    
    import com.example.demo.annotations.Audit;
    import io.micrometer.core.instrument.Metrics;
    import net.logstash.logback.marker.LogstashMarker;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    import org.wildfly.common.annotation.NotNull;
    
    import javax.servlet.http.HttpServletRequest;
    
    import static net.logstash.logback.marker.Markers.append;
    
    @Aspect
    @Component
    @Order(1)
    public class AuditAspect {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Autowired
        private HttpServletRequest request;
    
        private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    
        @Pointcut("@annotation(com.example.demo.annotations.Audit)")
        public void pcAudit() {
        }
    
        @Before(value = "pcAudit()")
        public void beforeAudit(JoinPoint point) {
            threadLocal.set(System.currentTimeMillis());
            String uri = request.getRequestURI();
            String method = request.getMethod();
            String auditName = getAnnotationName(point);
            logger.info(getMarker(method), "receive " + method + " request on uri " + uri + " to " + auditName);
        }
    
        @AfterReturning(value = "pcAudit()")
        public void afterAuditReturning(JoinPoint point) {
            String auditName = getAnnotationName(point);
            Metrics.counter("request_success_counter", "demo", auditName).increment();
            String uri = request.getRequestURI();
            String method = request.getMethod();
            long interval = System.currentTimeMillis() - threadLocal.get();
            logger.info(getMarker(method),
                    "after " + method + " request on uri " + uri + " return, consume " + interval + "ms");
        }
    
        @AfterThrowing(value = "pcAudit()", throwing = "ex")
        public void afterAuditThrowing(JoinPoint point, Exception ex) {
            String auditName = getAnnotationName(point);
            Metrics.counter("request_fail_counter", "demo", auditName).increment();
            String uri = request.getRequestURI();
            String method = request.getMethod();
            long interval = System.currentTimeMillis() - threadLocal.get();
            logger.info(getMarker(method), "after " + method + " request on uri " + uri + ", consume "
                    + interval + "ms, throw " + ex.getMessage());
        }
    
        private String getAnnotationName(@NotNull JoinPoint point) {
            MethodSignature methodSignature = (MethodSignature) point.getSignature();
            Audit audit = methodSignature.getMethod().getAnnotation(Audit.class);
            return audit.value();
        }
    
        private LogstashMarker getMarker(String action) {
            // marker 字段只会在 logback-spring.xml 中使用 LogstashEncoder 的 appender 会使用到,会打出来
            // 在其他 appender 中也会打 log,但不会带上 marker 字段
            return append("type", "audit").and(append("action", action));
        }
    }
    

    @Aspect 表示这个类用于进行 AOP 处理
    @Order(1) 表示优先级,因为一个函数有可能被多个注解标记
    @Pointcut("@annotation(com.example.demo.annotations.Audit)") 表示拦截 Audit 标记的函数
    @Before(value = "pcAudit()") 表示在被标记的函数运行前执行
    @AfterReturning(value = "pcAudit()") 表示在被标记的函数运行后执行
    @AfterThrowing(value = "pcAudit()", throwing = "ex") 表示在被标记的函数抛异常后执行

    getAnnotationName 用于获取 Audit 注解的值,比如 "get_all_user_id"

    这里实现了在目标函数执行前后打印日志,计算函数执行时间,计算函数执行次数,等功能


    拦截器

    package com.example.demo.config;
    
    import com.example.demo.interceptor.UriInterceptor;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebMvcConfigurerImpl implements WebMvcConfigurer {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new UriInterceptor());
        }
    }
    

    通过继承 WebMvcConfigurer 添加了拦截器 UriInterceptor 用于拦截用户请求
    可以添加多个,按添加的顺序执行

    package com.example.demo.interceptor;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    public class UriInterceptor extends HandlerInterceptorAdapter {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
            String urlList[] = {
                    "^/api/v1/users-id$",
                    "^/api/v1/users(/[^//]*){0,1}$",
                    "^/api/v1/user/[^//]+$"
            };
    
            String uri = request.getRequestURI();
            for (String urlPattern : urlList) {
                if (uri.matches(urlPattern)) {
                    return true;
                }
            }
            response.setStatus(HttpStatus.NOT_FOUND.value());
            response.getWriter().write("<html><head><title>Error Page</title></head><body>Invalid Request</body></html>");
            return false;
        }
    }
    

    UriInterceptor 通过继承 HandlerInterceptorAdapter 并重载 preHandle 函数实现
    preHandle 函数在用户请求被执行之前运行

    这里收到请求后,先检查是不是合法的 URL,如果是就返回 true,表示执行下一个拦截器,或是执行 Controller,如果不是合法的 URL,就返回我们自定义的 404 NOT FOUND 页面(不用这个拦截器会返回默认的 404 页面)


    ApplicationRunner

    package com.example.demo.service;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.stereotype.Component;
    
    
    @Component
    public class InitService implements ApplicationRunner {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            // TODO: init database
             logger.info("InitService : init database");
        }
    }
    

    有时需要在程序启动后做一些操作,可以用 ApplicationRunner 实现


    定时调度

    package com.example.demo.service;
    
    import com.example.demo.entity.User;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.scheduling.TaskScheduler;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
    import org.springframework.stereotype.Component;
    
    import java.util.List;
    
    @EnableScheduling
    @Component
    public class ScheduleService {
        @Autowired
        private DemoService demoService;
    
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        //@Scheduled(cron = "0 0/2 * * * ?")
        @Scheduled(initialDelayString = "5000", fixedDelayString = "100000")
        public void saveUserToDB() {
            List<User> user = demoService.getUsers(null);
            // TODO: save user to database
            logger.info("schedule : save user list to database");
        }
    
        // 配置线程池
        // 不知道写在这里有没有用,可能写到一个专门初始化配置的类比较好
        @Bean
        public TaskScheduler configTaskScheduler() {
            ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
            scheduler.setPoolSize(10);
            return scheduler;
        }
    }
    

    可以周期性调用,也可以通过 cron 指定固定时间调用


    logback-spring.xml 配置日志

    <?xml version="1.0" encoding="UTF-8"?>
    <!-- scan: 配置文件如果发生改变,将会被重新加载,默认值为 true -->
    <!-- scanPeriod: 监测配置文件是否有修改的时间间隔,默认单位是毫秒,默认的时间间隔为 1 分钟 -->
    <!-- debug: 设置为 true 时,将打印出 logback 内部日志信息,实时查看 logback 运行状态,默认值为 false -->
    <configuration  scan="true" scanPeriod="10 seconds"  debug="true">
    
        <!-- 定义变量,后面可以通过 ${log.path} 引用 -->
        <property name="log.path" value="./log" />
    
        <!-- 输出到控制台,
             name 可以是任意名字,最后面要添加到 <root>,
             class 是打印日志的类,ConsoleAppender 是打到控制台 -->
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <!-- filter 指定用于过滤的类,可以是自定义的,这里是过滤大于等于 info level 的日志 -->
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <level>info</level>
            </filter>
    
            <encoder>
                <!-- 输出日志的格式 -->
                <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5level] [%logger{50}] - %msg%n</Pattern>
                <!-- 设置字符集 -->
                <charset>UTF-8</charset>
            </encoder>
        </appender>
    
        <!-- 输出到文件 -->
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 路径及文件名 -->
            <file>${log.path}/demo.log</file>
    
            <!-- 此日志文件只记录 info 级别的 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>info</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
    
            <!-- 输出日志的格式 -->
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5level] [%logger{50}] - %msg%n</pattern>
                <charset>UTF-8</charset>
            </encoder>
    
            <!-- 日志的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- 日志归档 -->
                <fileNamePattern>${log.path}/save/demo-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!-- 日志文件保留天数 -->
                <maxHistory>15</maxHistory>
            </rollingPolicy>
        </appender>
    
        <!-- 输出到文件,使用 LogstashEncoder 输出 json 格式的日志 -->
        <appender name="FILE-JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 路径及文件名 -->
            <file>${log.path}/demo-json.log</file>
    
            <!-- 自定义 filter 只输出 audit 的非 ERROR 日志 -->
            <Filter class="com.example.demo.filter.FileJsonLogAuditFilter" />
    
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
                <includeCallerData>true</includeCallerData>
                <customFields>{"group":"example", "service":"demo"}</customFields>
                <timestampPattern>yyyy-MM-dd HH:mm:ss.SSS'Z'</timestampPattern>
                <timeZone>UTC +0</timeZone>
                <fieldNames>
                    <timestamp>timestamp</timestamp>
                    <thread>thread</thread>
                    <logger>logger</logger>
                    <message>message</message>
                    <level>level</level>
                    <callerLine>line</callerLine>
                    <!-- 如果不设置为 ignore 的话会打出来 -->
                    <version>[ignore]</version>
                    <levelValue>[ignore]</levelValue>
                    <callerClass>[ignore]</callerClass>
                    <callerMethod>[ignore]</callerMethod>
                    <callerFile>[ignore]</callerFile>
                </fieldNames>
            </encoder>
    
            <!-- 日志的滚动策略,按日期,按大小记录 -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- 日志归档 -->
                <fileNamePattern>${log.path}/save/demo-json-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!-- 日志文件保留天数 -->
                <maxHistory>15</maxHistory>
            </rollingPolicy>
        </appender>
    
        <root level="info" additivity="true">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="FILE" />
            <appender-ref ref="FILE-JSON" />
        </root>
    
    </configuration>
    

    SpringBoot 会默认扫描 classpath 下面的 logback.xml、logback-spring.xml 文件

    这里可以定义多个 appender,每个 appender 定义日志输出到哪里,是到 console 还是文件,使用什么样的 filter 过滤,输出格式怎么样,等等

    这个例子中的 FILE-JSON appender 使用了 net.logstash.logback.encoder.LogstashEncoder 用于输出 JSON 格式的日志,并且使用了自定义的 filter

            <dependency>
                <groupId>net.logstash.logback</groupId>
                <artifactId>logstash-logback-encoder</artifactId>
                <version>${logstash.logback.version}</version>
            </dependency>
    
    package com.example.demo.filter;
    
    import ch.qos.logback.classic.Level;
    import ch.qos.logback.classic.spi.LoggingEvent;
    import ch.qos.logback.core.filter.Filter;
    import ch.qos.logback.core.spi.FilterReply;
    import org.slf4j.Marker;
    
    import static net.logstash.logback.marker.Markers.append;
    
    public class FileJsonLogAuditFilter extends Filter<Object> {
    
        @Override
        public FilterReply decide(Object eventObject) {
            LoggingEvent event = (LoggingEvent) eventObject;
            Level level = event.getLevel();
            Marker marker = event.getMarker();
    
            if (level != Level.ERROR) {
                if (marker != null && marker.contains(append("type", "audit"))) {
                    return FilterReply.ACCEPT;
                } else {
                    return FilterReply.DENY;
                }
            }
            return FilterReply.DENY;
        }
    }
    

    可以看到这个 Filter 只允许非 ERROR 并且有 {"type": "audit"} 这个 marker 的日志输出

    import net.logstash.logback.marker.LogstashMarker;
    
        private LogstashMarker getMarker(String action) {
            // marker 字段只会在 logback-spring.xml 中使用 LogstashEncoder 的 appender 会使用到,会打出来
            // 在其他 appender 中也会打 log,但不会带上 marker 字段
            return append("type", "audit").and(append("action", action));
        }
    
    logger.info(getMarker(method), "receive " + method + " request on uri " + uri + " to " + auditName);
    

    可以看到 Audit AOP 中就使用了 marker,主要用于标记 log,在正常的日志中不会打印 marker,但在 LogstashEncoder 的 appender 会打印出来

    FILE-JSON appender 的 log 看起是这样

    {"timestamp":"2020-11-24 13:13:43.259Z","message":"after GET request on uri /api/v1/users return, consume 3ms","logger":"com.example.demo.aop.AuditAspect","thread":"XNIO-1 task-1","level":"INFO","type":"audit","action":"GET","line":55,"group":"example","service":"demo"}
    

    CONSOLE appender 的 log 看起是这样

    2020-11-25 01:08:25.203 [main] [INFO ] [com.example.demo.service.InitService] - InitService : init database
    

    多个 appender 同时起作用


    Actuator
    Spring Boot Actuator 模块提供了生产级别的功能,比如健康检查,审计,指标收集,HTTP 跟踪等
    这些功能都可以通过 HTTP 和 JMX 访问

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
    

    application.yaml 需要配置

    # 暴露 Actuator 的所有接口,并使 health 接口展示所有信息
    # http://localhost:9000/actuator
    # http://localhost:9000/actuator/health
    # http://localhost:9000/actuator/metrics
    # http://localhost:9000/actuator/prometheus
    # 需要在 pom.xml 添加 actuator 包
    management:
      endpoints:
        web:
          exposure:
            include: "*"
      endpoint:
        health:
          show-details: always
    

    http://localhost:9000/actuator 可以查看有哪些 Actuator 可以用,比如 health,metrics,beans 等

    举一些例子
    http://localhost:9000/actuator/health 的返回如下

    {
        "status": "UP",
        "components": {
            "diskSpace": {
                "status": "UP",
                "details": {
                    "total": 175025696768,
                    "free": 33702227968,
                    "threshold": 10485760,
                    "exists": true
                }
            },
            "ping": {
                "status": "UP"
            }
        }
    }
    

    http://localhost:9000/actuator/metrics 的返回如下

    {
        "names": [
            "http.server.requests",
            "jvm.buffer.count",
            "jvm.buffer.memory.used",
            "jvm.buffer.total.capacity",
            "jvm.classes.loaded",
            "jvm.classes.unloaded",
            "jvm.gc.live.data.size",
            "jvm.gc.max.data.size",
            "jvm.gc.memory.allocated",
            "jvm.gc.memory.promoted",
            "jvm.memory.committed",
            "jvm.memory.max",
            "jvm.memory.used",
            "jvm.threads.daemon",
            "jvm.threads.live",
            "jvm.threads.peak",
            "jvm.threads.states",
            "logback.events",
            "process.cpu.usage",
            "process.start.time",
            "process.uptime",
            "request_success_counter",
            "system.cpu.count",
            "system.cpu.usage"
        ]
    }
    

    可以看到我们在 AOP 定义的 request_success_counter 这里可以看到
    进一步查看 http://localhost:9000/actuator/metrics/request_success_counter

    {
        "name": "request_success_counter",
        "description": null,
        "baseUnit": null,
        "measurements": [
            {
                "statistic": "COUNT",
                "value": 3.0
            }
        ],
        "availableTags": [
            {
                "tag": "demo",
                "values": [
                    "get_all_user_id",
                    "get_users"
                ]
            }
        ]
    }
    

    统计了访问次数


    Prometheus

            <dependency>
                <groupId>io.micrometer</groupId>
                <artifactId>micrometer-registry-prometheus</artifactId>
            </dependency>
    
            <dependency>
                <groupId>io.prometheus</groupId>
                <artifactId>simpleclient</artifactId>
                <version>${prometheus.simple.client.version}</version>
            </dependency>
    

    在 AuditAspect 类中,我们使用了 metrics 进行统计
    除了在 metrics actuator 可以看到,在 prometheus actuator 也可以看到

    String auditName = getAnnotationName(point);
    
    Metrics.counter("request_success_counter", "demo", auditName).increment();
    
    Metrics.counter("request_fail_counter", "demo", auditName).increment();
    

    http://localhost:9000/actuator/prometheus

    # HELP process_cpu_usage The "recent cpu usage" for the Java Virtual Machine process
    # TYPE process_cpu_usage gauge
    process_cpu_usage 0.0
    # HELP http_server_requests_seconds  
    # TYPE http_server_requests_seconds summary
    http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/api/v1/users",} 1.0
    http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/api/v1/users",} 0.155734128
    http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/metrics",} 2.0
    http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/metrics",} 0.011018067
    http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/health",} 1.0
    http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/health",} 0.074643576
    http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/api/v1/users-id",} 2.0
    http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/api/v1/users-id",} 0.01569854
    http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 1.0
    http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 0.153424829
    http_server_requests_seconds_count{exception="None",method="GET",outcome="CLIENT_ERROR",status="404",uri="/**",} 1.0
    http_server_requests_seconds_sum{exception="None",method="GET",outcome="CLIENT_ERROR",status="404",uri="/**",} 0.067295168
    http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/metrics/{requiredMetricName}",} 1.0
    http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/metrics/{requiredMetricName}",} 0.021771337
    http_server_requests_seconds_count{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/beans",} 1.0
    http_server_requests_seconds_sum{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/beans",} 0.097360332
    # HELP http_server_requests_seconds_max  
    # TYPE http_server_requests_seconds_max gauge
    http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/api/v1/users",} 0.0
    http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/metrics",} 0.0
    http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/health",} 0.0
    http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/api/v1/users-id",} 0.0
    http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator",} 0.0
    http_server_requests_seconds_max{exception="None",method="GET",outcome="CLIENT_ERROR",status="404",uri="/**",} 0.0
    http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/metrics/{requiredMetricName}",} 0.0
    http_server_requests_seconds_max{exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/beans",} 0.0
    # HELP jvm_memory_used_bytes The amount of used memory
    # TYPE jvm_memory_used_bytes gauge
    jvm_memory_used_bytes{area="heap",id="PS Survivor Space",} 0.0
    jvm_memory_used_bytes{area="heap",id="PS Old Gen",} 1.5820736E7
    jvm_memory_used_bytes{area="heap",id="PS Eden Space",} 1.73317984E8
    jvm_memory_used_bytes{area="nonheap",id="Metaspace",} 4.5230552E7
    jvm_memory_used_bytes{area="nonheap",id="Code Cache",} 1.296704E7
    jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space",} 6097952.0
    # HELP jvm_gc_live_data_size_bytes Size of long-lived heap memory pool after reclamation
    # TYPE jvm_gc_live_data_size_bytes gauge
    jvm_gc_live_data_size_bytes 0.0
    # HELP jvm_gc_memory_promoted_bytes_total Count of positive increases in the size of the old generation memory pool before GC to after GC
    # TYPE jvm_gc_memory_promoted_bytes_total counter
    jvm_gc_memory_promoted_bytes_total 0.0
    # HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution
    # TYPE jvm_classes_unloaded_classes_total counter
    jvm_classes_unloaded_classes_total 0.0
    # HELP jvm_threads_states_threads The current number of threads having NEW state
    # TYPE jvm_threads_states_threads gauge
    jvm_threads_states_threads{state="runnable",} 10.0
    jvm_threads_states_threads{state="blocked",} 0.0
    jvm_threads_states_threads{state="waiting",} 15.0
    jvm_threads_states_threads{state="timed-waiting",} 2.0
    jvm_threads_states_threads{state="new",} 0.0
    jvm_threads_states_threads{state="terminated",} 0.0
    # HELP jvm_buffer_memory_used_bytes An estimate of the memory that the Java virtual machine is using for this buffer pool
    # TYPE jvm_buffer_memory_used_bytes gauge
    jvm_buffer_memory_used_bytes{id="direct",} 118702.0
    jvm_buffer_memory_used_bytes{id="mapped",} 0.0
    # HELP jvm_buffer_count_buffers An estimate of the number of buffers in the pool
    # TYPE jvm_buffer_count_buffers gauge
    jvm_buffer_count_buffers{id="direct",} 10.0
    jvm_buffer_count_buffers{id="mapped",} 0.0
    # HELP jvm_gc_memory_allocated_bytes_total Incremented for an increase in the size of the (young) heap memory pool after one GC to before the next
    # TYPE jvm_gc_memory_allocated_bytes_total counter
    jvm_gc_memory_allocated_bytes_total 0.0
    # HELP jvm_buffer_total_capacity_bytes An estimate of the total capacity of the buffers in this pool
    # TYPE jvm_buffer_total_capacity_bytes gauge
    jvm_buffer_total_capacity_bytes{id="direct",} 118702.0
    jvm_buffer_total_capacity_bytes{id="mapped",} 0.0
    # HELP logback_events_total Number of error level events that made it to the logs
    # TYPE logback_events_total counter
    logback_events_total{level="warn",} 0.0
    logback_events_total{level="debug",} 0.0
    logback_events_total{level="error",} 0.0
    logback_events_total{level="trace",} 0.0
    logback_events_total{level="info",} 25.0
    # HELP request_success_counter_total  
    # TYPE request_success_counter_total counter
    request_success_counter_total{demo="get_all_user_id",} 2.0
    request_success_counter_total{demo="get_users",} 1.0
    # HELP system_cpu_count The number of processors available to the Java virtual machine
    # TYPE system_cpu_count gauge
    system_cpu_count 4.0
    # HELP jvm_threads_daemon_threads The current number of live daemon threads
    # TYPE jvm_threads_daemon_threads gauge
    jvm_threads_daemon_threads 13.0
    # HELP jvm_threads_peak_threads The peak live thread count since the Java virtual machine started or peak was reset
    # TYPE jvm_threads_peak_threads gauge
    jvm_threads_peak_threads 27.0
    # HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use
    # TYPE jvm_memory_committed_bytes gauge
    jvm_memory_committed_bytes{area="heap",id="PS Survivor Space",} 1.1534336E7
    jvm_memory_committed_bytes{area="heap",id="PS Old Gen",} 6.8681728E7
    jvm_memory_committed_bytes{area="heap",id="PS Eden Space",} 2.03948032E8
    jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 4.8324608E7
    jvm_memory_committed_bytes{area="nonheap",id="Code Cache",} 1.4352384E7
    jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 6684672.0
    # HELP process_uptime_seconds The uptime of the Java virtual machine
    # TYPE process_uptime_seconds gauge
    process_uptime_seconds 587.891
    # HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
    # TYPE jvm_memory_max_bytes gauge
    jvm_memory_max_bytes{area="heap",id="PS Survivor Space",} 1.1534336E7
    jvm_memory_max_bytes{area="heap",id="PS Old Gen",} 1.244659712E9
    jvm_memory_max_bytes{area="heap",id="PS Eden Space",} 5.95591168E8
    jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
    jvm_memory_max_bytes{area="nonheap",id="Code Cache",} 2.5165824E8
    jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
    # HELP jvm_classes_loaded_classes The number of classes that are currently loaded in the Java virtual machine
    # TYPE jvm_classes_loaded_classes gauge
    jvm_classes_loaded_classes 9090.0
    # HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads
    # TYPE jvm_threads_live_threads gauge
    jvm_threads_live_threads 27.0
    # HELP jvm_gc_max_data_size_bytes Max size of long-lived heap memory pool
    # TYPE jvm_gc_max_data_size_bytes gauge
    jvm_gc_max_data_size_bytes 1.244659712E9
    # HELP system_cpu_usage The "recent cpu usage" for the whole system
    # TYPE system_cpu_usage gauge
    system_cpu_usage 0.23906219894981506
    # HELP process_start_time_seconds Start time of the process since unix epoch.
    # TYPE process_start_time_seconds gauge
    process_start_time_seconds 1.60623930251E9
    

    可以看到有很多系统默认的统计,也有我们自定义的

    # HELP request_success_counter_total  
    # TYPE request_success_counter_total counter
    request_success_counter_total{demo="get_all_user_id",} 2.0
    request_success_counter_total{demo="get_users",} 1.0
    


  • 相关阅读:
    常见linux内核线程说明
    /proc/modules分析
    linux用户空间和内核空间(内核高端内存)_转
    二层交换机/三层交换机/路由器
    NAT--Network Address Translator
    curl命令使用
    (转)XML中必须进行转义的字符
    LFCP
    IPSP问题
    API和schema开发过程问题汇总
  • 原文地址:https://www.cnblogs.com/moonlight-lin/p/14054523.html
Copyright © 2011-2022 走看看