SpringBoot整合框架
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>SpringBoot-web
WebMvcAutoConfiguration
//在这些自动配置之后,才会进行该配置
@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添加各种定制功能
- 所有的功能最终会和配置文件进行绑定
- WebMvcProperties: spring.mvc配置文件
- WebProperties: spring.web配置文件
@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 场景的自动配置类
- 支持RESTful的filter:HiddenHttpMethodFilter
- 支持非POST请求,请求体携带数据:FormContentFilter
- 导入EnableWebMvcConfiguration:
- RequestMappingHandlerAdapter
- WelcomePageHandlerMapping:欢迎页功能支持(模板引擎目录、静态资源目录放index.html),项目访问/ 就默认展示这个页面.
- RequestMappingHandlerMapping:找每个请求由谁处理的映射关系
- ExceptionHandlerExceptionResolver:默认的异常解析器
- LocaleResolver:国际化解析器
- ThemeResolver:主题解析器
- FlashMapManager:临时数据共享
- FormattingConversionService: 数据格式化 、类型转化
- Validator: 数据校验JSR303提供的数据校验功能
- WebBindingInitializer:请求参数的封装与绑定
- ContentNegotiationManager:内容协商管理器
- WebMvcAutoConfigurationAdapter配置生效,它是一个WebMvcConfigurer,定义mvc底层组件
- 定义好 WebMvcConfigurer 底层组件默认功能;所有功能详见列表
- 视图解析器:InternalResourceViewResolver
- 视图解析器:BeanNameViewResolver,视图名(controller方法的返回值字符串)就是组件名
- 内容协商解析器:ContentNegotiatingViewResolver
- 请求上下文过滤器:RequestContextFilter: 任意位置直接获取当前请求
- 静态资源链规则
- ProblemDetailsExceptionHandler:错误详情,SpringMVC内部场景异常被它捕获
- 定义了MVC默认的底层行为: WebMvcConfigurer

3、默认效果
- 包含了 ContentNegotiatingViewResolver 和 BeanNameViewResolver 组件,方便视图解析
- 默认的静态资源处理机制: 静态资源放在 static 文件夹下即可直接访问
- 自动注册了 Converter,GenericConverter,Formatter组件,适配常见数据类型转换和格式化需求
- 支持 HttpMessageConverters,可以方便返回json等数据类型
- 注册 MessageCodesResolver,方便国际化及错误消息处理
- 支持 静态 index.html
- 自动使用ConfigurableWebBindingInitializer,实现消息处理、数据绑定、类型转化、数据校验等功能
WebMvcConfigurer 功能
| 提供方法 | 核心参数 | 功能 | 默认 |
|---|---|---|---|
| addFormatters | FormatterRegistry | 格式化器:支持属性上@NumberFormat和@DatetimeFormat的数据类型转换 | GenericConversionService |
| getValidator | 无 | 数据校验:校验 Controller 上使用@Valid标注的参数合法性。需要导入starter-validator | 无 |
| addInterceptors | InterceptorRegistry | 拦截器:拦截收到的所有请求 | 无 |
| configureContentNegotiation | ContentNegotiationConfigurer | 内容协商:支持多种数据格式返回。需要配合支持这种类型的HttpMessageConverter | 支持 json |
| configureMessageConverters | List<HttpMessageConverter<?>> | 消息转换器:标注@ResponseBody的返回值会利用MessageConverter直接写出去 | 8 个,支持byte,string,multipart,resource,json |
| addViewControllers | ViewControllerRegistry | 视图映射:直接将请求路径与物理视图映射。用于无 java 业务逻辑的直接视图页渲染 | 无 mvc:view-controller |
| configureViewResolvers | ViewResolverRegistry | 视图解析器:逻辑视图转为物理视图 | ViewResolverComposite |
| addResourceHandlers | ResourceHandlerRegistry | 静态资源处理:静态资源路径映射、缓存控制 | ResourceHandlerRegistry |
| configureDefaultServletHandling | DefaultServletHandlerConfigurer | 默认 Servlet:可以覆盖 Tomcat 的DefaultServlet。让DispatcherServlet拦截/ | 无 |
| configurePathMatch | PathMatchConfigurer | 路径匹配:自定义 URL 路径匹配。可以自动为所有路径加上指定前缀,比如 /api | 无 |
| configureAsyncSupport | AsyncSupportConfigurer | 异步支持: | TaskExecutionAutoConfiguration |
| addCorsMappings | CorsRegistry | 跨域: | 无 |
| addArgumentResolvers | List<HandlerMethodArgumentResolver> | 参数解析器: | mvc 默认提供 |
| addReturnValueHandlers | List<HandlerMethodReturnValueHandler> | 返回值解析器: | mvc 默认提供 |
| configureHandlerExceptionResolvers | List<HandlerExceptionResolver> | 异常处理器: | 默认 3 个 ExceptionHandlerExceptionResolver ResponseStatusExceptionResolver DefaultHandlerExceptionResolver |
| getMessageCodesResolver | 无 | 消息码解析器:国际化使用 | 无 |
使用配置
| 方式 | 用法 | 效果 | |
|---|---|---|---|
| 全自动 | 直接编写控制器逻辑 | 全部使用自动配置默认效果 | |
| 手自一体 | @Configuration + 配置WebMvcConfigurer+ 配置 WebMvcRegistrations | 不要标注 @EnableWebMvc | 保留自动配置效果 手动设置部分功能 定义MVC底层组件 |
| 全手动 | @Configuration + 配置WebMvcConfigurer | 标注 @EnableWebMvc | 禁用自动配置效果 全手动设置 |
@EnableWebMvc
@EnableWebMvc给容器中导入 DelegatingWebMvcConfiguration组件,他是WebMvcConfigurationSupport的子类。其可以禁用自动配置是因为WebMvcAutoConfiguration有一个核心的条件注解, @ConditionalOnMissingBean(WebMvcConfigurationSupport.class),容器中没有WebMvcConfigurationSupport,WebMvcAutoConfiguration才生效.
………………………………
@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,如果服务这个资源没有发生变化,下次访问的时候就可以直接让浏览器用自己缓存中的东西,而不用给服务器发请求。
#开启静态资源映射规则
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自定义静态资源规则
#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 中进行了定义:
- 在静态资源目录下找 index.html
- 没有就在 templates下找index模板页
Favicon
在静态资源目录下找 favicon.ico
模式切换
AntPathMatcher 与 PathPatternParser
- PathPatternParser 在 jmh 基准测试下,有 6~8 倍吞吐量提升,降低 30%~40%空间分配率
- PathPatternParser 兼容 AntPathMatcher语法,并支持更多类型的路径模式
- PathPatternParser "*" 多段匹配的支持仅允许在模式末尾使用*
#使用默认的路径匹配规则,是由 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
导入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>写出为xml
@JacksonXmlRootElement // 可以写出为xml文档
@Data
public class Person {
private Long id;
private String userName;
private String email;
private Integer age;
}开启基于请求参数的内容协商
# 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format
spring.mvc.contentnegotiation.parameter-name=type写出为yml
导入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>写出为xml
@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);
}
}
}开启基于请求参数的内容协商
# 开启基于请求参数的内容协商功能。 默认参数名: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
- 容器中放一个
自定义拦截器
@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请求处理的方式:
@Controller + @RequestMapping:耦合式 (路由、业务耦合)- 函数式Web:分离式(路由、业务分离)
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();
}
}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
开启了 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration 自动配置
属性绑定在 ThymeleafProperties 中,对应配置文件 spring.thymeleaf 内容
默认效果
- 所有的模板页面在
classpath:/templates/下面找 - 找后缀名为
.html的页面
- 所有的模板页面在
<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值}
获得请求参数:
内置对象
直接就可以使用的对象
- 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值
javaa. 简单对象 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>
分支和迭代
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包含其他模板文件
功能:网页内公共代码片段的提取
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原理:
- MyBatis的所有配置绑定在
- 利用
@Import(MapperScannerRegistrar.class)批量给容器中注册组件。解析指定的包路径里面的每一个类,为每一个Mapper接口类,创建Bean定义信息,注册到容器中。
- 利用
创建工程
导入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>编写配置文件
#配置数据源
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
@SpringBootApplication
//指定要扫描的mapper包位置
@MapperScan(basePackages = "com.fjut.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}编写Mapper
//使用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 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 - 写配置
#数据源基本配置
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
# CommonsLogFilterSpringBoot-Junit
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>编写测试类
//@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 注解,而且嵌套的层次没有限制。
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:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
@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开始的所有配置
自定义配置文件
| 日志系统 | 自定义 |
|---|---|
| Logback | logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy |
| Log4j2 | log4j2-spring.xml or log4j2.xml |
| JDK (Java Util Logging) | logging.properties |
<!--切换默认的日志文件为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 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>记录日志
@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
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>配置
#设置了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/**常用注解
| 注解 | 标注位置 | 作用 | 参数 |
|---|---|---|---|
| @Tag | controller 类 | 标识 controller 作用 | name:标签的名称。 description:标签的描述。 |
| @Parameter | 参数 | 标识参数作用 | name:参数的名称。 description:参数的描述。 required:参数是否必需。 schema:参数的模型,使用@Schema注解来描述。 |
| @Parameters | 参数 | 参数多重说明 | 用于描述多个API操作的参数,它是@Parameter注解的容器。 |
| @Schema | model 层的 JavaBean | 描述模型作用及每个属性 | name:模型的名称。 description:模型的描述。 implementation:模型的实现类。 example:模型的示例值。 |
| @Operation | 方法 | 描述方法作用 | summary:操作的简短描述。 description:操作的详细描述。 tags:操作的标签,使用@Tag注解来描述。 parameters:操作的参数,使用@Parameter注解来描述。 responses:操作的响应,使用@ApiResponse注解来描述。 |
| @ApiResponse | 方法 | 描述响应状态码等 | responseCode:响应的HTTP状态码。 description:响应的描述。 content:响应的内容,使用@Content注解来描述。 |
@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<>();
}
}分组
//创建一个分组,名为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();
}文档描述
//设置文档的整体描述
@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
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>编写配置类
配置SpringSecurity
@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
@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,重写方法
@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;
}
}基于方法的权限认证
@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的起步依赖
<!-- springBoot JPA的起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>在application.yml中配置数据库和jpa的相关属性
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使用的命名策略创建实体类
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>中已经存在了。
import com.atguigu.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserDao extends JpaRepository<User,Long> {
}编写service层
//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追踪等。
配置
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>配置参数
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.name或logging.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导入依赖
<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 Client向Spring Boot Admin Server注册 , Client会定时向Server发送数据, Server使用友好的界面展示数据。
服务端
创建项目:springboot-admin-server
起步依赖
<?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>配置文件
spring:
application:
name: admin-server
server:
port: 8769启动类
@SpringBootApplication
@EnableAdminServer// 开启管理服务
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}客户端
在SpringBoot Actuator的配置基础上
导入依赖
<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地址,即服务器端配置的端口号SpringBoot-AOT
AOT与JIT
- AOT:Ahead-of-Time(提前编译):程序执行前,全部被编译成机器码
- JIT:Just in Time(即时编译): 程序边编译,边运行
| JIT | AOT |
|---|---|
| 1.具备实时调整能力 2.生成最优机器指令 3.根据代码运行情况优化内存占用 | 1.速度快,优化了运行时编译时间和内存消耗 2.程序初期就能达最高性能 3.加快程序启动速度 |
| 1.运行期边编译速度慢 2.初始编译不能达到最高性能 | 1.程序第一次编译占用时间长 2.牺牲高级语言一些特性 |
Complier 与 Interpreter
- 编译型语言:编译器
- 解释型语言:解释器
| 对比项 | 编译器 | 解释器 |
|---|---|---|
| 机器执行速度 | 快,因为源代码只需被转换一次 | 慢,因为每行代码都需要被解释执行 |
| 开发效率 | 慢,因为需要耗费大量时间编译 | 快,无需花费时间生成目标代码,更快的开发和测试 |
| 调试 | 难以调试编译器生成的目标代码 | 容易调试源代码,因为解释器一行一行地执行 |
| 可移植性(跨平台) | 不同平台需要重新编译目标平台代码 | 同一份源码可以跨平台执行,因为每个平台会开发对应的解释器 |
| 学习难度 | 相对较高,需要了解源代码、编译器以及目标机器的知识 | 相对较低,无需了解机器的细节 |
| 错误检查 | 编译器可以在编译代码时检查错误 | 解释器只能在执行代码时检查错误 |
| 运行时增强 | 无 | 可以动态增强 |

java编译流程
总体概述

详细步骤


分层编译
Java 7开始引入了分层编译(Tiered Compiler)的概念,它结合了C1和C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将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)的本机可执行文件运行(本地镜像)。
