Skip to content

SpringBoot整合框架

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

SpringBoot-web

WebMvcAutoConfiguration

java
//在这些自动配置之后,才会进行该配置
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class }) 
//如果是web应用就生效,类型SERVLET、REACTIVE(响应式web)
@ConditionalOnWebApplication(type = Type.SERVLET) 
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
//容器中没有这个Bean,才生效。默认就是没有
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 
//设置优先级
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration { 
}

1、添加了两个过滤器

HiddenHttpMethodFilter用于支持 HTML 表单中的 PUT、DELETE 和 PATCH 请求。由于 HTML 表单只支持 GET 和 POST 请求,所以 Spring 提供了这个过滤器来支持其他的 HTTP 方法。当我们在 HTML 表单中添加一个名为 _method 的隐藏字段,并且设置这个字段的值为 PUT、DELETE 或 PATCH 时,这个过滤器会将请求的方法修改为这个隐藏字段的值。这样,我们就可以在我们的控制器中处理 PUT、DELETE 和 PATCH 请求。

FormContentFilter: 这个过滤器用于处理 HTTP POST 请求的内容,它可以解析 application/x-www-form-urlencoded 和 multipart/form-data 两种类型的数据。

2、添加了WebMvcConfigurer组件

给容器中放了WebMvcConfigurer组件;给SpringMVC添加各种定制功能

  1. 所有的功能最终会和配置文件进行绑定
  2. WebMvcProperties: spring.mvc配置文件
  3. WebProperties: spring.web配置文件
java
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class) //额外导入了其他配置
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware{

}

//public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
//相当于SpringBoot 给容器中放 WebMvcConfigurationSupport 组件。
//我们如果自己放了 WebMvcConfigurationSupport 组件,Boot的WebMvcAutoConfiguration都会失效。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware 
{
    
}

WebMvcAutoConfigurationweb 场景的自动配置类

  1. 支持RESTful的filter:HiddenHttpMethodFilter
  2. 支持非POST请求,请求体携带数据:FormContentFilter
  3. 导入EnableWebMvcConfiguration
    • RequestMappingHandlerAdapter
    • WelcomePageHandlerMapping:欢迎页功能支持(模板引擎目录、静态资源目录放index.html),项目访问/ 就默认展示这个页面.
    • RequestMappingHandlerMapping:找每个请求由谁处理的映射关系
    • ExceptionHandlerExceptionResolver:默认的异常解析器
    • LocaleResolver:国际化解析器
    • ThemeResolver:主题解析器
    • FlashMapManager:临时数据共享
    • FormattingConversionService: 数据格式化 、类型转化
    • Validator: 数据校验JSR303提供的数据校验功能
    • WebBindingInitializer:请求参数的封装与绑定
    • ContentNegotiationManager:内容协商管理器
  4. WebMvcAutoConfigurationAdapter配置生效,它是一个WebMvcConfigurer,定义mvc底层组件
    • 定义好 WebMvcConfigurer 底层组件默认功能;所有功能详见列表
    • 视图解析器:InternalResourceViewResolver
    • 视图解析器:BeanNameViewResolver,视图名(controller方法的返回值字符串)就是组件名
    • 内容协商解析器:ContentNegotiatingViewResolver
    • 请求上下文过滤器:RequestContextFilter: 任意位置直接获取当前请求
    • 静态资源链规则
    • ProblemDetailsExceptionHandler:错误详情,SpringMVC内部场景异常被它捕获
  5. 定义了MVC默认的底层行为: WebMvcConfigurer

3、默认效果

  1. 包含了 ContentNegotiatingViewResolver 和 BeanNameViewResolver 组件,方便视图解析
  2. 默认的静态资源处理机制: 静态资源放在 static 文件夹下即可直接访问
  3. 自动注册了 Converter,GenericConverter,Formatter组件,适配常见数据类型转换和格式化需求
  4. 支持 HttpMessageConverters,可以方便返回json等数据类型
  5. 注册 MessageCodesResolver,方便国际化及错误消息处理
  6. 支持 静态 index.html
  7. 自动使用ConfigurableWebBindingInitializer,实现消息处理、数据绑定、类型转化、数据校验等功能

WebMvcConfigurer 功能

提供方法核心参数功能默认
addFormattersFormatterRegistry格式化器:支持属性上@NumberFormat和@DatetimeFormat的数据类型转换GenericConversionService
getValidator数据校验:校验 Controller 上使用@Valid标注的参数合法性。需要导入starter-validator
addInterceptorsInterceptorRegistry拦截器:拦截收到的所有请求
configureContentNegotiationContentNegotiationConfigurer内容协商:支持多种数据格式返回。需要配合支持这种类型的HttpMessageConverter支持 json
configureMessageConvertersList<HttpMessageConverter<?>>消息转换器:标注@ResponseBody的返回值会利用MessageConverter直接写出去8 个,支持byte,string,multipart,resource,json
addViewControllersViewControllerRegistry视图映射:直接将请求路径与物理视图映射。用于无 java 业务逻辑的直接视图页渲染mvc:view-controller
configureViewResolversViewResolverRegistry视图解析器:逻辑视图转为物理视图ViewResolverComposite
addResourceHandlersResourceHandlerRegistry静态资源处理:静态资源路径映射、缓存控制ResourceHandlerRegistry
configureDefaultServletHandlingDefaultServletHandlerConfigurer默认 Servlet:可以覆盖 Tomcat 的DefaultServlet。让DispatcherServlet拦截/
configurePathMatchPathMatchConfigurer路径匹配:自定义 URL 路径匹配。可以自动为所有路径加上指定前缀,比如 /api
configureAsyncSupportAsyncSupportConfigurer异步支持TaskExecutionAutoConfiguration
addCorsMappingsCorsRegistry跨域
addArgumentResolversList<HandlerMethodArgumentResolver>参数解析器mvc 默认提供
addReturnValueHandlersList<HandlerMethodReturnValueHandler>返回值解析器mvc 默认提供
configureHandlerExceptionResolversList<HandlerExceptionResolver>异常处理器默认 3 个 ExceptionHandlerExceptionResolver ResponseStatusExceptionResolver DefaultHandlerExceptionResolver
getMessageCodesResolver消息码解析器:国际化使用

使用配置

方式用法效果
全自动直接编写控制器逻辑全部使用自动配置默认效果
手自一体@Configuration + 配置WebMvcConfigurer+ 配置 WebMvcRegistrations不要标注 @EnableWebMvc保留自动配置效果 手动设置部分功能 定义MVC底层组件
全手动@Configuration + 配置WebMvcConfigurer标注 @EnableWebMvc禁用自动配置效果 全手动设置

@EnableWebMvc

@EnableWebMvc给容器中导入 DelegatingWebMvcConfiguration组件,他是WebMvcConfigurationSupport的子类。其可以禁用自动配置是因为WebMvcAutoConfiguration有一个核心的条件注解, @ConditionalOnMissingBean(WebMvcConfigurationSupport.class),容器中没有WebMvcConfigurationSupport,WebMvcAutoConfiguration才生效.

java
………………………………
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 
………………………………
public class WebMvcAutoConfiguration { 
}

静态资源

默认映射规则

静态资源映射

静态资源映射规则在 WebMvcAutoConfiguration 中进行了定义:

规则一:访问: /webjars/路径就去 classpath:/META-INF/resources/webjars/下找资源. 规则二:访问: /路径就去 静态资源默认的四个位置找资源

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public/
静态资源缓存

静态资源默认都有缓存规则的设置

  • 所有缓存的设置,直接通过配置文件: spring.web
  • cachePeriod: 缓存周期; 多久不用找服务器要新的。 默认没有,以s为单位
  • cacheControl: HTTP缓存控制;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
  • useLastModified:是否使用最后一次修改。配合HTTP Cache规则

如果浏览器访问了一个静态资源 index.js,如果服务这个资源没有发生变化,下次访问的时候就可以直接让浏览器用自己缓存中的东西,而不用给服务器发请求。

properties
#开启静态资源映射规则
spring.web.resources.add-mappings=true

#设置缓存
#spring.web.resources.cache.period=3600
##缓存详细合并项控制,覆盖period配置:
## 浏览器第一次请求服务器,服务器告诉浏览器此资源缓存7200秒,7200秒以内的所有此资源访问不用发给服务器请求,7200秒以后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
#使用资源 last-modified 时间,来对比服务器和浏览器的资源是否相同没有变化。相同返回 304
spring.web.resources.cache.use-last-modified=true

自定义静态资源规则

properties
#1、spring.web:
# 1.配置国际化的区域信息
# 2.静态资源策略(开启、处理链、缓存)

#开启静态资源映射规则
spring.web.resources.add-mappings=true

#设置缓存
spring.web.resources.cache.period=3600
##缓存详细合并项控制,覆盖period配置:
## 浏览器第一次请求服务器,服务器告诉浏览器此资源缓存7200秒,7200秒以内的所有此资源访问不用发给服务器请求,7200秒以后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
## 共享缓存
spring.web.resources.cache.cachecontrol.cache-public=true
#使用资源 last-modified 时间,来对比服务器和浏览器的资源是否相同没有变化。相同返回 304
spring.web.resources.cache.use-last-modified=true

#自定义静态资源文件夹位置
spring.web.resources.static-locations=classpath:/a/,classpath:/b/,classpath:/static/

#2、 spring.mvc
## 2.1. 自定义webjars路径前缀
spring.mvc.webjars-path-pattern=/wj/**
## 2.2. 静态资源访问路径前缀
spring.mvc.static-path-pattern=/static/**

欢迎页

欢迎页规则在 WebMvcAutoConfiguration 中进行了定义:

  1. 静态资源目录下找 index.html
  2. 没有就在 templates下找index模板页

Favicon

在静态资源目录下找 favicon.ico

模式切换

AntPathMatcher 与 PathPatternParser

  • PathPatternParser 在 jmh 基准测试下,有 6~8 倍吞吐量提升,降低 30%~40%空间分配率
  • PathPatternParser 兼容 AntPathMatcher语法,并支持更多类型的路径模式
  • PathPatternParser "*" 多段匹配的支持仅允许在模式末尾使用*
properties
#使用默认的路径匹配规则,是由 PathPatternParser  提供的
# 改变路径匹配策略:
# ant_path_matcher 老版策略;
# path_pattern_parser 新版策略;
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

内容协商

一套系统适配多端数据返回

基于请求头内容协商:(默认开启)

  • 客户端向服务端发送请求,携带HTTP标准的Accept请求头。
    • Accept: application/json、text/xml、text/yaml。
    • 服务端根据客户端请求头期望的数据类型进行动态返回。

基于请求参数内容协商:(需要开启)

  • 发送请求 GET /projects/spring-boot?format=json
  • 匹配到 @GetMapping("/projects/spring-boot")
  • 根据参数协商,优先返回 json 类型数据【需要开启参数匹配设置】
  • 发送请求 GET /projects/spring-boot?format=xml,优先返回 xml 类型

写出为xml

导入依赖

xml
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

写出为xml

java
@JacksonXmlRootElement  // 可以写出为xml文档
@Data
public class Person {
    private Long id;
    private String userName;
    private String email;
    private Integer age;
}

开启基于请求参数的内容协商

properties
# 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format
spring.mvc.contentnegotiation.parameter-name=type

写出为yml

导入依赖

xml
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

写出为xml

java
@Bean
public WebMvcConfigurer webMvcConfigurer(){
    return new WebMvcConfigurer() {
        @Override //配置一个能把对象转为yaml的messageConverter
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            converters.add(new MyYamlHttpMessageConverter());
        }
    };
}


public class MyYamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    private ObjectMapper objectMapper = null; //把对象转成yaml

    public MyYamlHttpMessageConverter(){
        //告诉SpringBoot这个MessageConverter支持哪种媒体类型  //媒体类型
        super(new MediaType("text", "yaml", Charset.forName("UTF-8")));
        YAMLFactory factory = new YAMLFactory()
                .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
        this.objectMapper = new ObjectMapper(factory);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        //只要是对象类型,不是基本类型
        return true;
    }

    @Override  //@RequestBody
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override //@ResponseBody 把对象怎么写出去
    protected void writeInternal(Object methodReturnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        //try-with写法,自动关流
        try(OutputStream os = outputMessage.getBody()){
            this.objectMapper.writeValue(os,methodReturnValue);
        }

    }
}

开启基于请求参数的内容协商

properties
# 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format
spring.mvc.contentnegotiation.parameter-name=type

#自定义内容类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

增加其他类型

  • 配置媒体类型支持:

    • spring.mvc.contentnegotiation.media-types.yaml=text/yaml
  • 编写对应的HttpMessageConverter,要告诉Boot这个支持的媒体类型

  • 把MessageConverter组件加入到底层

    • 容器中放一个WebMvcConfigurer 组件,并配置底层的MessageConverter

自定义拦截器

java
@Component
public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor拦截器的preHandle方法执行....");
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor拦截器的postHandle方法执行....");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor拦截器的afterCompletion方法执行....");
    }
}

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private MyInterceptor myInterceptor ;

    /**
     * /**  拦截当前目录及子目录下的所有路径 /user/**   /user/findAll  /user/order/findAll
     * /*   拦截当前目录下的以及子路径   /user/*     /user/findAll
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor).addPathPatterns("/**");
    }
}

函数式web

Web请求处理的方式:

  1. @Controller + @RequestMapping耦合式路由业务耦合)
  2. 函数式Web:分离式(路由、业务分离)
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RouterFunctions.route;

//proxyBeanMethods为false时,Spring容器不会生成CGLIB子类
//当proxyBeanMethods为true(默认值)时,Spring容器会为该配置类生成一个CGLIB子类,用于处理@Bean方法的调用,确保每个配置类只实例化一次。
@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {
    // 定义一个请求谓词,用于匹配接受JSON的请求
    private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

    // 使用@Bean注解标记这是一个bean的定义方法,该方法返回一个RouterFunction
    @Bean
    public RouterFunction<ServerResponse> routerFunction(MyUserHandler userHandler) {
        // 使用route()方法创建一个RouterFunction的构建器
        return route()
                // 为GET /{user}路径定义一个路由,当请求满足ACCEPT_JSON谓词(即接受JSON)时,使用userHandler的getUser方法处理请求
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                // 为GET /{user}/customers路径定义一个路由,当请求满足ACCEPT_JSON谓词(即接受JSON)时,使用userHandler的getUserCustomers方法处理请求
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                // 为DELETE /{user}路径定义一个路由,当请求满足ACCEPT_JSON谓词(即接受JSON)时,使用userHandler的deleteUser方法处理请求
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                // 构建并返回RouterFunction
                .build();
    }
}
java
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

@Component
public class MyUserHandler {
    public ServerResponse getUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }
    public ServerResponse getUserCustomers(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse deleteUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }
}

SpringBoot-Thymeleaf

  1. 开启了 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration 自动配置

  2. 属性绑定在 ThymeleafProperties 中,对应配置文件 spring.thymeleaf 内容

  3. 默认效果

    1. 所有的模板页面在 classpath:/templates/下面找
    2. 找后缀名为.html的页面
xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

名称空间

xmlns:th="http://www.thymeleaf.org"`

thymeleaf所有的表达式都是通过属性的方式添加,th:表达式内容
位置:html标签的属性上   xmlns:th="http://www.thymeleaf.org"

修改标签体和属性值:${ }

  • 不经过服务器解析,直接用浏览器打开HTML文件,看到的是『标签体原始值』
  • 经过服务器解析,Thymeleaf引擎根据th:text属性指定的『标签体新值』去<span style="color:blue;font-weight:bold;">替换</span>『标签体原始值』y
修改标签的文本值(双标签)
    th:text="新值"
    案例:<h2 th:text="${msg}">放服务器传过来的msg数据</h2>

修改标签的属性值(单标签和双标签)
    th:属性名="新值"
    案例:<input type="text" value="这是原始值" th:value="${msg}" >

解析URL地址:@{/}

  • 用在base标签上,给当前页面所有的URL加上/所代表的路径:<base th:href="@{/}">

  • 作为请求的路径,当标签前面有/时,base标签会对该标签失效,可以携带域中的参数

    xml
    如果你的网页想使用thymeleaf表达式的话,必须经过Servlet然后在经过Thymeleaf进行渲染才可以
    项目内所有的网页都需要thymeleaf渲染(都需要过Servlet在过Thymeleaf模板引擎)
    <a th:href="@{/root(id=101,name='jack',age=20)}">RootServlet02</a>

获得域对象中的数据:

  • 应用域:ServletContext application ${appliaction.应用域中的key值}
  • 会话域:HttpSession session ${session.会话域中的key值}
  • 请求域:HttpServletRequest request ${请求域中的key值}

获得请求参数:

param.key访{param.key[0]}

内置对象

直接就可以使用的对象

  • html
    ${#request.getParameter('id')};
    ${#String.length(msg)};
  • 基本内置对象 #request 就是Servlet中的HttpServletRequest对象 #response 就是Servlet中的HttpServletResponse对象 #session 就是Servlet中的HttpSession对象 #servletContext 就是Servlet中的ServletContext对象

  • 公共内置对象 #strings 提供了很多对字符串操作的方法 ☆ #arrays 提供了操作数组的常用方法 #lists 提供了操作List集合的常用方法 ☆ #sets 提供了操作set集合的常用方法 #maps 提供了操作Map集合的常用方法

  • OGNL 对象-图 导航语言:将复杂的对象或者集合放在域对象内,Thymeleaf去获取到数据

    • 遇到对象就通过.属性名的方式去获取属性值(原理是调用get方法)
    • 遇到List集合或者数组就通过下标获得到元素
    • 遇到Map集合就通过.key值的方式获得value值
    java
    a. 简单对象
        Employee employee=new Employee(101,"法外狂徒张三",0,500000.0);
        request.setAttribute("emp",employee);
        ${emp}    整个对象
        ${emp.id}    拿到对象内getId方法的返回值
    b. 稍微复杂对象
         Employee02 employee02=new Employee02(102,"熊二",1,34567d,new Computer(1,"联想",5000d));
         request.setAttribute("emp02",employee02);
         <div th:text="${emp02.salary}"></div>    员工的工资
         <div th:text="${emp02.computer}"></div>        员工的电脑对象
         <div th:text="${emp02.computer.id}"></div>     员工的电脑的getId方法的返回值
    c. 简单集合
        List<String> names=new ArrayList<>();
        names.add("java");
        names.add("mysql");
        names.add("oracle");
        names.add("php");
        request.setAttribute("names",names);
        <div th:text="${names}"></div>
        <div th:text="${names[0]}"></div>
        <div th:text="${names[1]}"></div>
    d. 复杂集合
        List<Employee> emps=new ArrayList<>();
        emps.add(new Employee(101,"法外狂徒张三",0,500000.0));
        emps.add(new Employee(102,"法外狂徒李四",1,600000.0));
        emps.add(new Employee(103,"法外狂徒王五",0,700000.0));
        request.setAttribute("emps",emps);
        <p th:text="${emps}"></p>
        <p th:text="${emps[0]}"></p>
        <p th:text="${emps[0].id}"></p>
        <p th:text="${emps[0].name}"></p>
    e. Map集合
        Map<String,Employee> map=new HashMap<>();
        map.put("emp01",new Employee(101,"jack",0,500000.0));
        map.put("emp02",new Employee(102,"rose",1,500000.0));
        map.put("emp03",new Employee(103,"tom",0,500000.0));
        request.setAttribute("map",map);
        <p th:text="${map}"></p>
        <p th:text="${map.emp01}"></p>
        <p th:text="${map.emp01.id}"></p>
        <p th:text="${map.emp01.name}"></p>

分支和迭代

xml
a. 分支  控制元素的是否显示
    th:if=""        值如果是true,元素就显示,值如果是false,就不显示
        <p th:if="${#strings.length(msg)>5}">msg的数据长度大于5</p>
        <p th:if="${not (#strings.length(msg)>5)}">msg的数据长度不大于5(1)</p>
    th:unless=""    值如果是false,元素就显示,值如果是true,就不显示
        <p th:unless="${#strings.length(msg)>5}">msg的数据长度不大于5(3)</p>
    th:switch=""   看switch中的数据和哪个case相同,有相同的就显示哪个
        th:case=""
        <div th:switch="${#strings.length(msg)}">
            <p th:case="1">长度为1</p>
            <p th:case="2">长度为2</p>
            <p th:case="3">长度为3</p>
            <p th:case="4">长度为4</p>
            <p th:case="5">长度为5</p>
        </div>
        request.setAttribute("msg","是共享数据");

b. 迭代
    th:each="obj,status : 后台请求域中数据的key"
    简单数组迭代:
        List<String> names=new ArrayList<>();
        names.add("java");
        names.add("mysql");
        names.add("oracle");
        names.add("php");
         <ul>
            <li th:each="name,status : ${names}" th:text="${name}"></li>
        </ul>
    复杂数组迭代:
        List<Employee> emps=new ArrayList<>();
        emps.add(new Employee(101,"法外狂徒张三",0,500000.0));
        emps.add(new Employee(102,"法外狂徒李四",1,600000.0));
        emps.add(new Employee(103,"法外狂徒王五",0,700000.0));
        request.setAttribute("emps",emps);
        <table border="1" width="500px">
                <tr>
                    <th>序号</th>
                    <th>编号</th>
                    <th>姓名</th>
                    <th>性别</th>
                    <th>工资</th>
                </tr>
                <tr th:each="emp,status : ${emps}">
                    <td th:text="${status.count}"></td>
                    <td th:text="${emp.id}"></td>
                    <td th:text="${emp.name}"></td>
        				<!--<td th:if="${emp.gender==0}">男</td>-->
        				<!--<td th:if="${emp.gender==1}">女</td>-->
                    <td th:text="${emp.gender==0?'男':'女'}"></td>   支持三目运算符
                    <td th:text="${emp.salary}"></td>
                </tr>
            </table>

Thymeleaf包含其他模板文件

html
功能:网页内公共代码片段的提取
a. 给公共的代码片段起个名字
    <div th:fragment="abc" id="header">公共头部信息</div>
    
b. 在其他页面根据名字引用
    <div th:replace="base::abc" id="aheader"></div>   将引入标签整体替换目标标签
    <div th:insert="base::abc" id="aheader"></div>    将引入标签插入到目标标签内
    <div th:include="base::abc" id="aheader"></div>   将引入标签内容插入到目标标签内

SpringBoot-mybatis

自动配置

  • MyBatisAutoConfiguration:配置了MyBatis的整合流程

    • mybatis-spring-boot-starter导入 mybatis-spring-boot-autoconfigure(mybatis的自动配置包)
    • 默认加载两个自动配置类:
      • org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration
      • org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
        • 必须在数据源配置好之后才配置
        • 给容器中SqlSessionFactory组件。创建和数据库的一次会话
        • 给容器中SqlSessionTemplate组件。操作数据库
    • MyBatis的所有配置绑定在MybatisProperties
    • 每个Mapper接口代理对象是怎么创建放到容器中。详见@MapperScan原理:
      • 利用@Import(MapperScannerRegistrar.class)批量给容器中注册组件。解析指定的包路径里面的每一个类,为每一个Mapper接口类,创建Bean定义信息,注册到容器中。

创建工程

导入xml依赖

xml
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

编写配置文件

properties
#配置数据源
spring.datasource.url=jdbc:mysql://192.168.200.100:3306/demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.zaxxer.hikari.HikariDataSource

#配置Mybatis

#不配置也有默认的位置
#private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"};
             
#指定mapper映射文件位置
mybatis.mapper-locations=classpath:/mapper/*.xml
#自动将列名中的下划线命名转换为Java的驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true

#为pojo包下的类指定别名
mybatis.type-aliases-package: com.atguigu.pojo 

启动类添加@MapperScan

java
@SpringBootApplication
//指定要扫描的mapper包位置
@MapperScan(basePackages = "com.fjut.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

编写Mapper

java
//使用Mapper注解标识接口,用于Mybatis生成代理对象
@Mapper
public interface BookBorrowingInfoMapper extends BaseMapper<BookBorrowingInfo> {
    List<BorrowingMonthChartVo> getBorrowingMonthInfoByUserId(Long userId, Integer year);

    List<HashMap<String,Object>> getDyingBorrowingBooks();

    List<HashMap<String,Object>> getOvertimeBorrowingBooks();
}

在指定的mapper文件夹下创建xml

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fjut.library_management_system.mapper.BookInfoMapper">

</mapper>

整合其他数据源

  • 导入druid-starter
  • 写配置
properties
#数据源基本配置
spring.datasource.url=jdbc:mysql://192.168.200.100:3306/demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 配置StatFilter监控
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.db-type=mysql
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000
# 配置WallFilter防火墙
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=mysql
spring.datasource.druid.filter.wall.config.delete-allow=false
spring.datasource.druid.filter.wall.config.drop-table-allow=false
# 配置监控页,内置监控页面的首页是 /druid/index.html
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
spring.datasource.druid.stat-view-servlet.allow=*

# 其他 Filter 配置不再演示
# 目前为以下 Filter 提供了配置支持,请参考文档或者根据IDE提示(spring.datasource.druid.filter.*)进行配置。
# StatFilter
# WallFilter
# ConfigFilter
# EncodingConvertFilter
# Slf4jLogFilter
# Log4jFilter
# Log4j2Filter
# CommonsLogFilter

SpringBoot-Junit

导入依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

编写测试类

java
//@SpringBootTest:用于创建SpringBoot上下文,使得在测试类中可以直接注入Bean调用
//webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT:表示应用程序应在随机端口上启动,这对于避免端口冲突非常有用,特别是在并行测试的情况下
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class LibraryManagementSystemApplicationTests {
    @Test
    public void test01(){
        List<String> hello = List.of("hello", "world");
        hello.parallelStream()
                .flatMap(
                        word-> Stream.of(word).filter(w->w.contains("z"))
                )
                .forEach(System.out::println);
    }
}

常用注解

  • @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试

  • @DisplayName :为测试类或者测试方法设置展示名称

  • @BeforeEach :表示在每个单元测试之前执行

  • @AfterEach :表示在每个单元测试之后执行

  • @BeforeAll :表示在所有单元测试之前执行

  • @AfterAll :表示在所有单元测试之后执行

  • @Tag :表示单元测试类别,类似于JUnit4中的@Categories,可以用于排除或指定分组执行测试

  • @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore

  • @Timeout :表示测试方法运行如果超过了指定时间将会返回错误

  • @ExtendWith :为测试类或测试方法提供扩展类引用

    java
    //创建Spring环境中运行测试
    @ExtendWith({SpringExtension.class})
    public @interface SpringBootTest

@Nested嵌套注解

通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起,可以把它看作是一个文件夹,你可以在这个文件夹中放入一些相关的测试,这样你就可以更容易地找到和管理这些测试。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。

java
class TestingAStackDemo {
    @Nested
    @DisplayName("when new")
    class WhenNew {
        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }
        }
    }
}

参数化测试

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

@NullSource: 表示为参数化测试提供一个null的入参

@EnumSource: 表示为参数化测试提供一个枚举入参

@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参

@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

java
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
    System.out.println(string);
    Assertions.assertTrue(StringUtils.isNotBlank(string));
}


@ParameterizedTest
@MethodSource("method")    //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
    System.out.println(name);
    Assertions.assertNotNull(name);
}

static Stream<String> method() {
    return Stream.of("apple", "banana");
}

断言

使用Assertions类调用静态方法进行断言

assertEquals判断两个对象或两个原始类型是否相等
assertNotEquals判断两个对象或两个原始类型是否不相等
assertSame判断两个对象引用是否指向同一个对象
assertNotSame判断两个对象引用是否指向不同的对象
assertTrue判断给定的布尔值是否为 true
assertFalse判断给定的布尔值是否为 false
assertNull判断给定的对象引用是否为 null
assertNotNull判断给定的对象引用是否不为 null
assertArrayEquals数组断言
assertAll组合断言
assertThrows异常断言
assertTimeout超时断言
fail快速失败

SpringBoot-Logback

SpringBoot日志

1、每个starter场景,都会导入一个核心场景spring-boot-starter

2、核心场景引入了日志的所用功能spring-boot-starter-logging

3、默认使用了logback + slf4j 组合作为默认底层日志

4、日志是系统一启动就要用xxxAutoConfiguration是系统启动好了以后放好的组件,后来用的。

5、日志是利用监听器机制配置好的。ApplicationListener

6、日志所有的配置都可以通过修改配置文件实现。以logging开始的所有配置

自定义配置文件

日志系统自定义
Logbacklogback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
Log4j2log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging)logging.properties
xml
<!--切换默认的日志文件为log4j2-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <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-log4j2</artifactId>
</dependency>

Logback XML配置

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration  scan="true" scanPeriod="10 seconds">
    <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
    <!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
    <!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
    <!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->

    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <property name="log.path" value= "./logs" />

    <!-- 彩色日志 -->
    <!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
    <!-- magenta:洋红 -->
    <!-- boldMagenta:粗红-->
    <!-- cyan:青色 -->
    <!-- white:白色 -->
    <!-- magenta:洋红 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>


    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>


    <!--输出到文件-->

    <!-- 时间滚动输出 level为 INFO 日志 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_info.log</file>
        <!--日志文件输出格式-->
        <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}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 WARN 日志 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_warn.log</file>
        <!--日志文件输出格式-->
        <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}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>


    <!-- 时间滚动输出 level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_error.log</file>
        <!--日志文件输出格式-->
        <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}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>


    <!-- 时间滚动输出操作日志 -->
    <appender name="OPERATION_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_operation.log</file>
        <!--日志文件输出格式-->
        <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}/operation/log-operation-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
    </appender>


    <!-- 异步INFO日志 -->
    <appender name="ASYNC_INFO_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="INFO_FILE" />
        <queueSize>500</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <includeCallerData>true</includeCallerData>
    </appender>

    <!-- 异步WARN日志 -->
    <appender name="ASYNC_WARN_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="WARN_FILE" />
        <queueSize>500</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <includeCallerData>true</includeCallerData>
    </appender>

    <!-- 异步ERROR日志 -->
    <appender name="ASYNC_ERROR_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="ERROR_FILE" />
        <queueSize>500</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <includeCallerData>true</includeCallerData>
    </appender>

    <!-- 异步OPERATION日志 -->
    <appender name="ASYNC_OPERATION_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="OPERATION_FILE" />
        <queueSize>500</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <includeCallerData>true</includeCallerData>
    </appender>
    <!--
        <logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
        <logger>仅有一个name属性,
        一个可选的level和一个可选的addtivity属性。
        name:用来指定受此logger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
              如果未设置此属性,那么当前logger将会继承上级的级别。
    -->
    <!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
     -->
    <!--开发环境:打印控制台只在dev环境下生效-->
    <springProfile name="dev">
        <!--可以输出项目中的debug日志,包括mybatis的sql日志-->
        <!--选择要输出日志文件的包-->
        <logger name="com.fjut" level="INFO" />

        <!--
            root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
            level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
            可以包含零个或多个appender元素。
        -->
        <root level="INFO">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
        </root>
    </springProfile>


    <!--生产环境:输出到文件-->
    <springProfile name="prod">
        <!--选择要输出日志文件的包-->
        <logger name="com.fjut.operation" level="INFO" additivity="false">
            <appender-ref ref="ASYNC_OPERATION_FILE" />
        </logger>

        <logger name="com.fjut" level="INFO" additivity="false">
            <appender-ref ref="ASYNC_INFO_FILE" />
            <appender-ref ref="ASYNC_WARN_FILE" />
            <appender-ref ref="ASYNC_ERROR_FILE" />
        </logger>

        <root level="INFO">
            <appender-ref ref="CONSOLE" />
        </root>
    </springProfile>
</configuration>

记录日志

java
@Aspect
@Component
public class logRecords {
    /*
     <!--选择要输出日志文件的包-->
     <logger name="com.fjut.operation" level="INFO" additivity="false">
        <appender-ref ref="ASYNC_OPERATION_FILE" />
     </logger>
    */
    //将日志文件输出到一个单独的包
    Logger logger = LoggerFactory.getLogger("com.fjut.operation");

    //注意,表达式中创建的切点方法,绝对不能包括WebsocketController中的方法,否则会@ServerEndPoint注解的类注册失败
    //Controller层的update、delete、add、remove方法
    @Pointcut("execution(* com.fjut.library_management_system.controller.*.update*(..))")
    public void update() {
    }

    @Pointcut("execution(* com.fjut.library_management_system.controller.*.delete*(..))")
    public void delete() {
    }

    @Pointcut("execution(* com.fjut.library_management_system.controller.*.add*(..))")
    public void add() {
    }

    @Pointcut("execution(* com.fjut.library_management_system.controller.*.remove*(..))")
    public void remove() {
    }

    //使用前置通知,对update、delete、add、remove等危险操作方法进行记录
    @Before("update() || delete() || add()|| remove()")
    public void recordUpdateOperation(JoinPoint joinPoint) throws Throwable {
        //获取当前用户
        Long currentUsername = SecurityUtil.getCurrentUsername();
        //获取方法名
        String methodName = joinPoint.getSignature().getName();
        //获取参数
        String args = Arrays.deepToString(joinPoint.getArgs());
        logger.info("用户{}执行了{}方法,参数为{}", currentUsername, methodName, args);
        //2024-05-26 23:32:19.929 [http-nio-8080-exec-5] INFO  com.fjut.operation - 用户3221311414执行了updateBanUser方法,参数为[3000000000]
    }
}

SpringBoot-swagger

Swagger 可以快速生成实时接口文档,方便前后开发人员进行协调沟通。遵循 OpenAPI 规范。

导入xml

xml
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.1.0</version>
</dependency>

配置

properties
#设置了OpenAPI文档的路径。默认情况下,OpenAPI文档的路径是/v3/api-docs
springdoc.api-docs.path=/api-docs
#设置了Swagger UI的路径,默认情况下,Swagger UI的路径是/swagger-ui.html
springdoc.swagger-ui.path=/swagger-ui.html

#设置了是否在OpenAPI文档中显示Spring Boot Actuator的端点。(如健康检查、度量指标等)
springdoc.show-actuator=true

#如果只有一组可以按照下方式分组
#设置了Springdoc OpenAPI应该扫描哪些包来生成API文档
springdoc.packagesToScan=package1, package2
#设置了哪些路径应该被包含在API文档中
springdoc.pathsToMatch=/v1, /api/balance/**

常用注解

注解标注位置作用参数
@Tagcontroller 类标识 controller 作用name:标签的名称。 description:标签的描述。
@Parameter参数标识参数作用name:参数的名称。 description:参数的描述。 required:参数是否必需。 schema:参数的模型,使用@Schema注解来描述。
@Parameters参数参数多重说明用于描述多个API操作的参数,它是@Parameter注解的容器。
@Schemamodel 层的 JavaBean描述模型作用及每个属性name:模型的名称。 description:模型的描述。 implementation:模型的实现类。 example:模型的示例值。
@Operation方法描述方法作用summary:操作的简短描述。 description:操作的详细描述。 tags:操作的标签,使用@Tag注解来描述。 parameters:操作的参数,使用@Parameter注解来描述。 responses:操作的响应,使用@ApiResponse注解来描述。
@ApiResponse方法描述响应状态码等responseCode:响应的HTTP状态码。 description:响应的描述。 content:响应的内容,使用@Content注解来描述。
java
@Tag(name = "User Controller", description = "用户相关的API操作")
@RestController
public class UserController {

    @Operation(summary = "获取用户信息", description = "根据用户ID获取用户信息")
    @ApiResponse(responseCode = "200", description = "成功获取用户信息")
    @ApiResponse(responseCode = "404", description = "找不到用户")
    @GetMapping("/users/{id}")
    public User getUser(
            @Parameter(description = "用户ID", required = true)
            @PathVariable String id) {
        // 获取用户信息的代码
        return new User();
    }

    @Schema(name = "User", description = "用户模型")
    public class User {
        @Schema(description = "用户ID", example = "U001")
        private String id;
        @Schema(description = "用户姓名", example = "张三")
        private String name;
        // 省略其他属性和方法
    }
    
    @Operation(summary = "搜索用户")
    @Parameters({
        @Parameter(name = "name", description = "用户姓名", required = false),
        @Parameter(name = "age", description = "用户年龄", required = false)
    })
    @GetMapping("/users/search")
    public List<User> searchUsers(
            @RequestParam(required = false) String name,
            @RequestParam(required = false) Integer age) {
        // 搜索用户的代码
        return new ArrayList<>();
    }
}

分组

java
//创建一个分组,名为springshop-public,包含路径为/public/**的请求
@Bean
public GroupedOpenApi publicApi() {
    return GroupedOpenApi.builder()
        .group("springshop-public")
        .pathsToMatch("/public/**")
        .build();
}

//创建一个分组,名为springshop-admin,包含路径为/admin/**的请求
@Bean
public GroupedOpenApi adminApi() {
    return GroupedOpenApi.builder()
        .group("springshop-admin")
        .pathsToMatch("/admin/**")
        //生成API文档时,它会使用这个过滤器来决定哪些方法应该包含在API文档中。只有那些过滤器返回true的方法才会被包含在API文档中。
        .addMethodFilter(method -> method.isAnnotationPresent(Admin.class))
        .build();
}

文档描述

java
//设置文档的整体描述
@Bean
public OpenAPI springShopOpenAPI() {
    return new OpenAPI()
        .info(new Info().title("SpringShop API")
              .description("Spring shop sample application")
              .version("v0.0.1")
              .license(new License().name("Apache 2.0").url("http://springdoc.org")))
        .externalDocs(new ExternalDocumentation()
                      .description("SpringShop Wiki Documentation")
                      .url("https://springshop.wiki.github.org/docs"));
}

SpringBoot-Security

导入依赖

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

编写配置类

配置SpringSecurity

java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
    @Autowired
    private UserService userService;

    //注入token验证的过滤器
    @Autowired
    private TokenAuthenticationFilterConfig tokenAuthenticationFilterConfig;

    @Autowired
    private RedisTemplate redisTemplate;

    //时间戳验证过滤器
    @Autowired
    private TimestampAuthenticationFilterConfig timestampAuthenticationFilterConfig;

    //密码加密为BCryptPasswordEncoder
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //从配置文件中读取允许跨域的域名
    @Value("${websecurity.allow.origin}")
    private String origin;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        //anyRequest():对所有请求开启授权保护
        //authenticated():已认证请求会自动被授权
        http
                //authorizeRequests():开启授权保护
                .authorizeRequests(authorize -> authorize
                        //放行请求
                        .requestMatchers("/login","/user/isLogin","websocket/hasMessage").permitAll())
                .formLogin(withDefaults())//表单授权方式
                .httpBasic(withDefaults());//基本授权方式

        http
                //跨域配置
                .cors(cors -> cors.configurationSource(request -> {
                    CorsConfiguration configuration = new CorsConfiguration();
                    configuration.setAllowedOrigins(List.of(origin)); //允许跨域的源
                    //configuration.setAllowedOriginPatterns(List.of("*"));
                    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的方法
                    configuration.setAllowedHeaders(List.of("*")); // 允许的头
                    configuration.setExposedHeaders(Arrays.asList("Access-Control-Allow-Origin", "Access-Control-Allow-Credentials")); // 允许暴露的头
                    configuration.setAllowCredentials(true); // 允许携带凭证
                    return configuration;
                }));
        // ...

        //前后端分离,禁用csrf(跨站请求伪造)
        http.csrf(AbstractHttpConfigurer::disable);

        //登录配置
        http.formLogin(
                formLogin -> formLogin
                        .loginPage("/login").permitAll() //登录页面无需授权即可访问
                        //登录成功处理
                        .successHandler(
                                (request, response, authentication) -> {
                                    //查询用户是否绑定手机
                                    CompletableFuture<Boolean> res = VirtualThreadUtil
                                            .executorAsync(()->userService.queryUserHadPhone(Long.valueOf(authentication.getName())));

                                    String name = authentication.getName();

                                    //向Redis中插入数据
                                    VirtualThreadUtil
                                            .executorAsync(() -> redisTemplate.opsForValue().set("login:" + name, JSONObject.toJSONString(authentication.getAuthorities()), 3600, TimeUnit.SECONDS));

                                    //返回json数据
                                    response.setContentType("application/json;charset=utf-8");
                                    //返回是否绑定手机号,以及验证账号的Token
                                    response.getWriter().write(JSON.toJSONString(Result.ok().message("登录成功").data("hasPhone",res.join()).data("token",JwtUtil.createToken(name))));
                                }
                        )

                        //登录失败处理
                        .failureHandler((request, response, exception) -> {
                            //返回json数据
                            response.setContentType("application/json;charset=utf-8");
                            response.getWriter().write(JSON.toJSONString(Result.error().message("登录失败:"+exception.getMessage())));
                        })
        );

        //用户登出成功处理
        http.logout(logout -> {
            logout.logoutSuccessHandler((request, response, authentication) -> {
                //移出Redis中的Token
                String token = request.getHeader("Token");
                VirtualThreadUtil
                        .executorAsync(()->redisTemplate.delete("login:"+JwtUtil.getUsername(token)));

                //返回json数据
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(JSON.toJSONString(Result.ok().message("退出成功")));
            });
        });

        //请求未认证的接口
        http.exceptionHandling(exception  -> {
            exception.authenticationEntryPoint((request, response, authException) -> {
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(JSON.toJSONString(Result.error().message("未认证")));
            });
        });

        //会话管理,使用无状态的会话token
        http
                .sessionManagement(sessionManagement -> sessionManagement
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        //添加自定义过滤器
        http
                 //添加时间戳验证过滤器
                .addFilterBefore(timestampAuthenticationFilterConfig, UsernamePasswordAuthenticationFilter.class)
                //添加Token验证过滤器
                .addFilterBefore(tokenAuthenticationFilterConfig, UsernamePasswordAuthenticationFilter.class);

        //返回http配置对象
        return http.build();
    }
}

添加自定义过滤器

继承OncePerRequestFilter,重写doFilterInternal

java
@Component
public class TokenAuthenticationFilterConfig extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate redisTemplate;

    //每次请求都会执行这个方法
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        String token = request.getHeader("Token");

        //token为空
        if (token == null || token.isEmpty()) {
            // 如果是需要放行的URL,直接放行,不进行后续的过滤器处理(websocket心跳链接不携带token,所以直接放心)
            if ("/login".equals(requestURI)||"/websocket/hasMessage".equals(requestURI)) {
                // 如果是需要放行的URL,直接放行,不进行后续的过滤器处理
                filterChain.doFilter(request, response);
            }else{
                //需要验证才能访问的URL,返回登录状态已过期,请重新登录
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(JSON.toJSONString(Result.error().message("登录状态已过期,请重新登录").data("isLogin", false).data("reason", "token为空")));
            }
            return;
        }

        //token无效
        boolean tokenValid = JwtUtil.isTokenValid(token);
        if (!tokenValid) {
            // 如果是需要放行的URL,直接放行,不进行后续的过滤器处理
            if ("/login".equals(requestURI)) {
                filterChain.doFilter(request, response);
            }else{
                //需要验证才能访问的URL,返回登录状态已过期,请重新登录
                response.setContentType("application/json;charset=utf-8");
                response.getWriter().write(JSON.toJSONString(Result.error().message("登录状态已过期,请重新登录").data("isLogin", false).data("reason", "token无效")));
            }
            return;
        }

        // 获取userid 从redis中获取用户信息,key为login:userId,value为权限列表
        String userId = JwtUtil.getUsername(token);
        String redisKey = "login:" + userId;
        Object o = redisTemplate.opsForValue().get(redisKey);

        //redis中没有用户信息,返回登录状态已过期,请重新登录
        if (Objects.isNull(o)) {
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write(JSON.toJSONString(Result.error().message("登录状态已过期,请重新登录").data("isLogin", false)));
            return;
        }

        //将权限信息存入到SecurityContextHolder
        List<GrantedAuthority> grantedAuthorities = JSON.parseArray(o.toString(), GrantedAuthority.class);
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userId, null, grantedAuthorities);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        //放行
        filterChain.doFilter(request, response);
    }
}

编写登录等规则

实现UserDetailsManager, UserDetailsPasswordService,重写方法

java
@Component
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserPermissionMapper userPermissionMapper;

    @Override
    public void createUser(UserDetails user) {

    }

    @Override
    public void updateUser(UserDetails user) {

    }

    @Override
    public void deleteUser(String username) {

    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {

    }

    @Override
    public boolean userExists(String username) {
        return false;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户
        QueryWrapper<User> queryWrapper = new QueryWrapper<User>()
                .eq("user_id", username)
                .eq("is_deleted", 0);

        User user = VirtualThreadUtil
                .executor(() -> userMapper.selectOne(queryWrapper));

        //用户不存在
        if (user == null) {
            throw new UsernameNotFoundException(username);
        } else {
            //用户存在,获取用户权限,封装到GrantedAuthority,返回UserDetails
            Collection<GrantedAuthority> authorities = new ArrayList<>();

            //获取权限列表
            List<String> permissionList = VirtualThreadUtil
                    .executor(() -> userPermissionMapper.getPermissionList(user.getUserId()));

            //封装权限
            for (String permission : permissionList) {
                authorities.add((GrantedAuthority) () -> permission);
            }

            //返回UserDetails
            return new org.springframework.security.core.userdetails.User(
                    user.getUserId().toString(),
                    user.getPassword(),
                    true,
                    true, //用户账号是否过期
                    !user.getExpire(), //用户凭证是否过期
                    !user.getBan(), //用户是否未被锁定
                    authorities); //权限列表
        }
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        return null;
    }
}

基于方法的权限认证

java
@PreAuthorize("#queryBorrowingVo.userId==T(com.fjut.library_management_system.util.SecurityUtil).getCurrentUsername() or hasAuthority('queryBorrowingInfo')")
@GetMapping("/getBorrowingInfo")
public Result getBorrowingInfo(QueryBorrowingVo queryBorrowingVo) {
    //得到借阅信息总数,用于分页
    CompletableFuture<Long> total = VirtualThreadUtil
        .executorAsync(() -> borrowingInfoService.getCount(queryBorrowingVo));

    //得到分页借阅信息
    CompletableFuture<List<BorrowingVo>> borrowingInfo =VirtualThreadUtil
        .executorAsync(() -> borrowingInfoService.getAllBorrowingInfo(queryBorrowingVo));

    return Result.ok().data("total", total.join()).data("borrowingInfo", borrowingInfo.join());
}

SpringBoot-JPA

导入SpringDate JPA的起步依赖

xml
<!-- springBoot JPA的起步依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

在application.yml中配置数据库和jpa的相关属性

yaml
spring:
  datasource: #配置数据库连接信息
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/数据库?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa: 
    database: mysql # 指定使用的数据库类型是MySQL
    show-sql: true # 决定Hibernate是否打印出它执行的SQL语句
    generate-ddl: true # 决定Spring Boot是否在启动时自动创建、更新或验证数据库表结构
    hibernate: 
      ddl-auto: update # 决定Hibernate在启动时如何自动处理数据库表结构,表示Hibernate会根据你的实体类自动创建或更新数据库表结构
      naming_strategy: org.hibernate.cfg.ImprovedNamingStrategy # 指定Hibernate使用的命名策略

创建实体类

java
import javax.persistence.*;

@Entity
@Table(name = "user")
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
    @Column(name = "username")
    private String username;
    @Column(name = "password")
    private String password;
    @Column(name = "name")
    private String name;
    //此处省略setter和getter方法......
}

编写Dao层,继承JpaRepository<User,Long>接口,不需要写方法,JpaRepository<User,Long>中已经存在了。

java
import com.atguigu.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserDao extends JpaRepository<User,Long> {
}

编写service层

java
//service接口
public interface UserService {
    List<User> findUsers();

    User findUserById(Long id);

    void saveUser(User user);

    void updateUser(User user);

    void deleteUserById(Long id);
}


//service实现类
import com.atguigu.dao.UserDao;
import com.atguigu.domain.User;
import com.atguigu.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public List<User> findUsers() {
        return userDao.findAll();
    }

    @Override
    public User findUserById(Integer id) {
        return userDao.findById(id).get();
    }

    @Override
    public void saveUser(User user) {
        userDao.save(user);
    }

    @Override
    public void updateUser(User user) {
        userDao.save(user);
    }

    @Override
    public void deleteUserById(Integer id) {
        userDao.deleteById(id);
    }
}

注意:

  • 自动生成的表的存储引擎是MyISAM,此引擎不支持事务,需要该为InnoDB,创建hibernate.properties配置文件添加以下配置。

hibernate.dialect.storage_engine=innodb

  • update与save调用的是同一个函数,如果插入的对象有id就是更新,没有则是save。

SpringBoot-Actuator

Spring Boot Actuator是SpringBoot自带的一个组件 , 可以帮助我们监控和管理Spring Boot应用,比如健康检查、审计、统计和HTTP追踪等。

配置

引入依赖

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

配置参数

yaml
management:
  endpoints:
    enabled-by-default: true #暴露所有端点信息
    web:
      exposure:
        include: '*'  #以web方式暴露, 默认是/health和/info
      #base-path: /monitor # 默认是actuator
  endpoint:
    health:
      show-details: ALWAYS	# 显示所有健康状态

获取信息

发送:-http://localhost:9999/actuator/health请求即可获得health信息,其他以此类推
HTTP 方法路径描述
GET/autoconfig提供了一份自动配置报告,记录哪些自动配置条件通过了,哪些没通过
GET/configprops描述配置属性(包含默认值)如何注入Bean
GET/beans描述应用程序上下文里全部的Bean,以及它们的关系
GET/dump获取线程活动的快照
GET/env获取全部环境属性
GET/env/{name}根据名称获取特定的环境属性值
GET/health报告应用程序的健康指标,这些值由HealthIndicator的实现类提供
GET/info获取应用程序的定制信息,这些信息由info打头的属性提供
GET/mappings描述全部的URI路径,以及它们和控制器(包含Actuator端点)的映射关系
GET/metrics报告各种应用程序度量信息,比如内存用量和HTTP请求计数
GET/metrics/{name}报告指定名称的应用程序度量值
POST/shutdown关闭应用程序,要求endpoints.shutdown.enabled设置为true
GET/trace提供基本的HTTP请求跟踪信息(时间戳、HTTP头等)

常用端点

ID描述
auditevents暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans显示应用程序中所有Spring Bean的完整列表。
caches暴露可用的缓存。
conditions显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops显示所有@ConfigurationProperties
env暴露Spring的属性ConfigurableEnvironment
flyway显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health显示应用程序运行状况信息。
httptrace显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info显示应用程序信息。
integrationgraph显示Spring integrationgraph 。需要依赖spring-integration-core
loggers显示和修改应用程序中日志的配置。
liquibase显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics显示当前应用程序的“指标”信息。
mappings显示所有@RequestMapping路径列表。
scheduledtasks显示应用程序中的计划任务。
sessions允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown使应用程序正常关闭。默认禁用。
startup显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump执行线程转储。
heapdump返回hprof堆转储文件。
jolokia通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

Prometheus + Grafana

#安装prometheus:时序数据库
docker run -p 9090:9090 -d \
-v pc:/etc/prometheus \
prom/prometheus

#安装grafana;默认账号密码 admin:admin
docker run -d --name=grafana -p 3000:3000 grafana/grafana

导入依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <version>1.10.6</version>
</dependency>

启动java

nohup java -jar boot3-14-actuator-0.0.1-SNAPSHOT.jar > output.log 2>&1 &

配置 Prometheus 拉取数据

## 修改 prometheus.yml 配置文件
scrape_configs:
  - job_name: 'spring-boot-actuator-exporter'
    metrics_path: '/actuator/prometheus' #指定抓取的路径
    static_configs:
      - targets: ['192.168.200.1:8001']
        labels:
          nodename: 'app-demo'

配置 Grafana 监控面板

  • 添加数据源(Prometheus)
  • 添加面板。可去 dashboard 市场找一个自己喜欢的面板

SpringBoot Admin组件

Spring Boot Actuator , 可以通过http协议获取系统状态信息 , 但是返回的是JSON格式数据, SpringBoot Admin 应用程序作为Spring Boot Admin ClientSpring Boot Admin Server注册 , Client会定时向Server发送数据, Server使用友好的界面展示数据

服务端

创建项目:springboot-admin-server

起步依赖

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 http://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.2.2.RELEASE</version>
    </parent>

    <groupId>com.atguigu</groupId>
    <artifactId>springboot-admin-server</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-server</artifactId>
            <version>2.2.0</version>
        </dependency>
    </dependencies>
    
</project>

配置文件

yaml
spring:
  application:
    name: admin-server
server:
  port: 8769

启动类

java
@SpringBootApplication
@EnableAdminServer// 开启管理服务
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

客户端

在SpringBoot Actuator的配置基础上

导入依赖

xml
<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>2.2.0</version>
</dependency>

增加配置文件

spring:
  application:
    name: admin-client
  boot:
    admin:
      client:
        url: http://localhost:8769   # 指定注册地址 , Spring Boot Admin Server地址,即服务器端配置的端口号

访问:http://localhost:8769

SpringBoot-AOT

AOT与JIT

  • AOT:Ahead-of-Time(提前编译):程序执行前,全部被编译成机器码
  • JIT:Just in Time(即时编译): 程序边编译,边运行
JITAOT
1.具备实时调整能力 2.生成最优机器指令 3.根据代码运行情况优化内存占用1.速度快,优化了运行时编译时间和内存消耗 2.程序初期就能达最高性能 3.加快程序启动速度
1.运行期边编译速度慢 2.初始编译不能达到最高性能1.程序第一次编译占用时间长 2.牺牲高级语言一些特性

Complier 与 Interpreter

  • 编译型语言:编译器
  • 解释型语言:解释器
对比项编译器解释器
机器执行速度,因为源代码只需被转换一次,因为每行代码都需要被解释执行
开发效率,因为需要耗费大量时间编译,无需花费时间生成目标代码,更快的开发和测试
调试难以调试编译器生成的目标代码容易调试源代码,因为解释器一行一行地执行
可移植性(跨平台)不同平台需要重新编译目标平台代码同一份源码可以跨平台执行,因为每个平台会开发对应的解释器
学习难度相对较高,需要了解源代码、编译器以及目标机器的知识相对较低,无需了解机器的细节
错误检查编译器可以在编译代码时检查错误解释器只能在执行代码时检查错误
运行时增强可以动态增强

java编译流程

总体概述

详细步骤

分层编译

Java 7开始引入了分层编译(Tiered Compiler)的概念,它结合了C1C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次。五个层级分别是:

  • 解释执行。
  • 执行不带profiling的C1代码。
  • 执行仅带方法调用次数以及循环回边执行次数profiling的C1代码。
  • 执行带所有profiling的C1代码。
  • 执行C2代码。

profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。

  • 图中第①条路径,代表编译的一般情况,热点方法从解释执行到被3层的C1编译,最后被4层的C2编译。
  • 如果方法比较小(比如Java服务中常见的getter/setter方法),3层的profiling没有收集到有价值的数据,JVM就会断定该方法对于C1代码和C2代码的执行效率相同,就会执行图中第②条路径。在这种情况下,JVM会在3层编译之后,放弃进入C2编译,直接选择用1层的C1编译运行
  • C1忙碌的情况下,执行图中第③条路径,在解释执行过程中对程序进行profiling ,根据信息直接由第4层的C2编译
  • 前文提到C1中的执行效率是1层>2层>3层第3层一般要比第2层慢35%以上,所以在C2忙碌的情况下,执行图中第④条路径。这时方法会被2层的C1编译,然后再被3层的C1编译,以减少方法在3层的执行时间。
  • 如果编译器做了一些比较激进的优化,比如分支预测,在实际运行时发现预测出错,这时就会进行反优化,重新进入解释执行,图中第⑤条执行路径代表的就是反优化

总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。

云原生

存在的问题

  • java应用如果用jar,解释执行,热点代码才编译成机器码;初始启动速度慢,初始处理请求数量少。
  • 大型云平台,要求每一种应用都必须秒级启动。每个应用都要求效率高。

希望的效果

  • java应用也能提前被编译成机器码,随时急速启动,一启动就急速运行,最高性能

  • 编译成机器码的好处:

    • 另外的服务器还需要安装Java环境
    • 编译成机器码的,可以在这个平台 Windows X64 直接运行

原生镜像

  • 把应用打包成能适配本机平台 的可执行文件(机器码、本地镜像)

GraalVM

GraalVM是一个高性能的JDK,旨在加速用Java和其他JVM语言编写的应用程序执行,同时还提供JavaScript、Python和许多其他流行语言的运行时。

GraalVM提供了两种运行Java应用程序的方式:

  • 在HotSpot JVM上使用Graal即时(JIT)编译器
  • 作为预先编译(AOT)的本机可执行文件运行(本地镜像)。