SpringBoot

本文最后更新于:2024年6月21日 凌晨

开发流程

创建项目

maven项目

<!--    所有springboot项目都必须继承自 spring-boot-starter-parent -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.1</version>
    </parent>

导入场景

场景启动器

    <dependencies>
<!--        web开发场景-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

主程序

@SpringBootApplication // 这是一个SpringBoot应用
public class MainApplication {
	public static void main(String[] args) {
		SpringApplication.run(MainApplication.class, args);
	}
}

业务

@RestController // @RestController = @Controller + @ResponseBody
public class HelloController {
	
	@GetMapping("/hello")
	public String Hello(){
		return "Hello, Spring Boot3!";
	}
}

测试

默认访问:localhost:8080

打包

<!--    SpringBoot应用打包插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

mvn clean package 把项目打成可执行的 jar 包

image-20230629152729007

java -jar demo.jar 启动项目

核心技能

注解

组件注册

@Configuration:表面这是一个通用配置类

@SpringBootConfiguration:与@Configuration类似,表面是SpringBoot的配置类

@Bean:创建实例 @Scope:调整组件范围

@Import:导入组件,组件的名字默认是全类名

@Component @Controller @Service @Repository

步骤:

  1. 在@Configuration 编写一个配置类
  2. 在配置类中,自定义方法给容器中注册组件。配合@Bean
  3. 或使用@Import导入第三方组件

条件注解

如果注解指定的条件成立,则触发指定行为

@ConditionalOnXxx

@ConditionalOnClass:如果类路径中存在这个类,则触发指定行为

@ConditionalOnMissingClass:如果类路径中不存在这个类,则触发指定行为

@ConditionalOnBean:如果容器中存在这个Bean(组件),则触发指定行为

@ConditionalOnMissingBean:如果容器中不存在这个Bean(组件),则触发指定行为

属性绑定

**@ConfigurationProperties(prefix = “”)**:声明组件的属性和配置文件哪些前缀进行绑定

@EnableConfigurationProperties:快速注册注解

将容器中的任意组件(Bean)的属性值和配置文件的配置项的值进行绑定

  • 给容器中注册组件
  • 使用@ConfigurationProperties声明组件和配置文件中的哪些配置项进行绑定

Yaml语法

  • birthDay 推荐写成birth-day
  • 文本:
    • 单引号不会转义 【\n以普通字符串显示】
    • 双引号会转义 【\n显示为换行符
  • 大文本
    • | 开头,大文本写在下层,保留文本格式,换行符正确显示
    • > 开头,大文本写在下层,折叠换行符
  • 多文本合并
    • 使用--- 可以把多个yaml文档合并在一个文档,每个文档区依然是独立的,中间用---分割

application.yml中,分别配置属性、对象、数组、哈希Map

@Component
@ConfigurationProperties(prefix = "person")
@Data
public class Person {
	private String name;
	private Integer age;
	private Child child; // 嵌套对象
	private List<Dog> dogs; // 数组
	private Map<String, Cat> cats; // 表示Map
}
person:
  name: 张三
  age: 18
  child:
    name: 儿子
    age: 19
  dogs:
    - name: 小黑
      age: 3
    - name: 小狗
      age: 4
  cats:
    c1:
      name: 小猫
      age: 5
    c2: { name: 小花, age: 6 } # 对象也可以用{ }表示

xxxxxxxxxx redis 127.0.0.1:6379> CONFIG GET dirbash

规范:项目开发不要编写system.out.println(),应该使用日志记录信息

日志门面 日志实现
SLF4J Logback

日志是系统一启动就要使用xxxAutoConfigration是系统启动好了以后放好的组件,启动后使用

所以日志是利用监听器机制配置好的

日志格式

2023-07-01T23:27:53.062+08:00  INFO 12256 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-07-01T23:27:53.062+08:00  INFO 12256 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.10]

默认输出格式:

  • 时间和日期:毫秒级精度
  • 日志级别:ERROR, WARN, INFO, DBBUG, or TRACE
  • 进程ID
  • — :消息分隔符
  • 线程名:使用 [] 包含
  • Logger 名:通常是产生日志的类名
  • 消息:日志记录的消息

注意:logback没有FATAL级别,对应的是ERROR

默认值:参考:spring-bootadditonal-spring-configration-metadata.json文件

可修改为:%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger ===> %msg%n

logging:
  pattern:
    console: "%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger ===> %msg%n"

使用方法

在类上添加@Slf4j

@Slf4j
@RestController
public class HelloController {
	@GetMapping("/h")
	public String hello() {
		log.info("哈哈,方法进来了");
		return "hello";
	}
}

日志级别

  • 由低到高:ALL,TRACE,DEBUG,INFO,WARN,ERROR,FATAL,OFF
    • 只会打印指定级别以上级别的日志
    • ALL:打印所有日志
    • TRACE:追踪框架详细流程日志,一般不使用
    • DEBUG:开发调式细节日志
    • INFO:关键、感兴趣信息日志
    • WARN:警告但不是错误的信息日志,比如:版本过时
    • ERROR:业务错误日志,比如出现各种异常
    • FATAL:致命错误日志,比如出现jvm系统崩溃
    • OFF:关闭所有日志记录
  • 不指定级别的所有类,都使用root指定的级别作为默认级别
  • SpringBoot日志默认级别是INFO
  1. 在application.yml中配置logging.level.<logger-anme>=<level/>指定日志级别
  2. level可取值范围:ALL,TRACE,DEBUG,INFO,WARN,ERROR,FATAL,OFF,定义在LogLevel类中
  3. root的logger-name叫root,可以配置logging.level.root=warn,代表所有未指定日志级别都是root的warn级别

日志分组

自定义一个abc分组

logging:
  group:
    abc: com.wjwang.controller, com.wjwang.service

然后可以使用logging.level.abc=xxx进行分组控制

日志文件保存

logging:
  file:
    name: D:\\log  # 指定日志文件的名称,可以写路径加名称

文档归档与滚动切割

归档:每天的日志单独存在一个文档中。

切割:每个文件10MB,超过大小切割成另外一个文件。

logging:
  file:
    name: spring3-log
  logback:
    rollingpolicy:
#      归档、切割
      file-name-pattern: "${LOG_FILE}.%d{yyyy-MM-dd}.%i.log"
      max-file-size: 1KB

Web开发

WebMvcAutoConfiguration

效果:

  1. 放了两个Filter:
    1. HiddenHttpMethodFilter:页面表单提交Rest请求(GET、POST、PUT、DELETE)
    2. FormContentFilter: 表单内容Filter,GET(数据放URL后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE 的请求体数据会被忽略
  2. 给容器中放了WebMvcConfigurer组件;给SpringMVC添加各种定制功能
    1. 所有的功能最终会和配置文件进行绑定
    2. WebMvcProperties: spring.mvc配置文件
    3. WebProperties: spring.web配置文件
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class) //额外导入了其他配置
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware{
       
   }

WebMvcConfigurer接口

提供了配置SpringMVC底层的所有组件入口

image-20230702163039001.png

静态资源规则源码

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    //1、
    addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(),
            "classpath:/META-INF/resources/webjars/");
    addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
        registration.addResourceLocations(this.resourceProperties.getStaticLocations());
        if (this.servletContext != null) {
            ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
            registration.addResourceLocations(resource);
        }
    });
}
  1. 规则一:访问: /webjars/**路径就去 classpath:/META-INF/resources/webjars/下找资源.
    1. maven 导入依赖
  2. 规则二:访问: /**路径就去 静态资源默认的四个位置找资源
    1. classpath:/META-INF/resources/
    2. classpath:/resources/
    3. classpath:/static/
    4. classpath:/public/
  3. 规则三:静态资源默认都有缓存规则的设置
    1. 所有缓存的设置,直接通过配置文件spring.web
    2. cachePeriod: 缓存周期; 多久不用找服务器要新的。 默认没有,以s为单位
    3. cacheControl: HTTP缓存控制;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
    4. useLastModified:是否使用最后一次修改。配合HTTP Cache规则

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

registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());

EnableWebMvcConfiguration

//SpringBoot 给容器中放 WebMvcConfigurationSupport 组件。
//我们如果自己放了 WebMvcConfigurationSupport 组件,Boot的WebMvcAutoConfiguration都会失效。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware 
{   
}
  1. HandlerMapping: 根据请求路径 /a 找那个handler能处理请求
    1. WelcomePageHandlerMapping
      1. 访问 /**路径下的所有请求,都在以前四个静态资源路径下找,欢迎页也一样
      2. index.html:只要静态资源的位置有一个 index.html页面,项目启动默认访问

Web场景

自动配置

  1. 整合web场景

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  2. 引入了autoconfigure功能

  3. @EnableAutoConfiguration注解使用@Import(AutoConfigurationImportSelector.class)批量导入组件

  4. 所有自动配置类如下:

org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
===以下是响应式web场景和现在的没关系===
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
==================================
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

默认效果

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

重要:

  • 如果想保持 boot mvc 的默认配置,并且自定义更多的 mvc 配置,如:interceptors,* formatters*,* view controllers** *等。可以使用@Configuration*注解添加一个 WebMvcConfigurer 类型的配置类,并不要标注 @EnableWebMvc
  • 如果想保持 boot mvc 的默认配置,但要自定义核心组件实例,比如:*RequestMappingHandlerMapping,* RequestMappingHandlerAdapter, 或ExceptionHandlerExceptionResolver,给容器中放一个 WebMvcRegistrations 组件即可
  • 如果想全面接管 Spring MVC,@Configuration 标注一个配置类,并加上 @EnableWebMvc**注解,实现 WebMvcConfigurer 接口

静态资源

默认规则

  1. 规则一:访问: /webjars/**路径就去 classpath:/META-INF/resources/webjars/下找资源.
    1. maven 导入依赖
  2. 规则二:访问: /**路径就去 静态资源默认的四个位置找资源
    1. classpath:/META-INF/resources/
    2. classpath:/resources/
    3. classpath:/static/
    4. classpath:/public/
  3. 规则三:静态资源默认都有缓存规则的设置
    1. 所有缓存的设置,直接通过配置文件spring.web
    2. cachePeriod: 缓存周期; 多久不用找服务器要新的。 默认没有,以s为单位
    3. cacheControl: HTTP缓存控制;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
    4. useLastModified:是否使用最后一次修改。配合HTTP Cache规则

自定义静态资源规则

自定义静态资源路径、自定义缓存规则

配置方式

spring.mvc: 静态资源访问前缀路径

spring.web

  • 静态资源目录
  • 静态资源缓存策略
#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/**
代码方式
  • 容器中只要有一个 WebMvcConfigurer 组件。配置的底层行为都会生效
  • @EnableWebMvc //禁用boot的默认配置
@Configuration //这是一个配置类
public class MyConfig implements WebMvcConfigurer {


    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //保留以前规则
        //自己写新的规则。
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/a/","classpath:/b/")
                .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
    }
}
@Configuration //这是一个配置类,给容器中放一个 WebMvcConfigurer 组件,就能自定义底层
public class MyConfig  /*implements WebMvcConfigurer*/ {


    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
                registry.addResourceHandler("/static/**")
                        .addResourceLocations("classpath:/a/", "classpath:/b/")
                        .setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
            }
        };
    }

}

路径匹配

Ant风格路径用法

Ant 风格的路径模式语法具有以下规则:

  • *:表示任意数量的字符。
  • ?:表示任意一个字符
  • :表示任意数量的目录**。
  • {}:表示一个命名的模式占位符
  • []:表示字符集合,例如[a-z]表示小写字母。

例如:

  • *.html 匹配任意名称,扩展名为.html的文件。
  • /folder1/*/*.java 匹配在folder1目录下的任意两级目录下的.java文件。
  • /folder2/**/*.jsp 匹配在folder2目录下任意目录深度的.jsp文件。
  • /{type}/{id}.html 匹配任意文件名为{id}.html,在任意命名的{type}目录下的文件。

注意:Ant 风格的路径模式语法中的特殊字符需要转义,如:

  • 要匹配文件路径中的星号,则需要转义为\\*
  • 要匹配文件路径中的问号,则需要转义为\\?

模式切换

AntPathMatcher 与 PathPatternParser

  • PathPatternParser 在 jmh 基准测试下,有 68 倍吞吐量提升,降低 30%40%空间分配率
  • PathPatternParser 兼容 AntPathMatcher语法,并支持更多类型的路径模式
  • PathPatternParser “*多段匹配的支持仅允许在模式末尾使用*
@GetMapping("/a*/b?/{p1:[a-f]+}")
public String hello(HttpServletRequest request, 
                    @PathVariable("p1") String path) {

    log.info("路径变量p1: {}", path);
    //获取请求路径
    String uri = request.getRequestURI();
    return uri;
}

总结:

  • 使用默认的路径匹配规则,是由 PathPatternParser 提供的
  • 如果路径中间需要有 **,替换成ant风格路径
# 改变路径匹配策略:
# ant_path_matcher 老版策略;
# path_pattern_parser 新版策略;
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

内容协商

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

image

多端内容适配

默认规则
  1. SpringBoot 多端内容适配
    1. 基于请求头内容协商:(默认开启)
    2. ​ 客户端向服务端发送请求,携带HTTP标准的Accept请求头
    3. ​ Accept: application/jsontext/xmltext/yaml
    4. ​ 服务端根据客户端请求头期望的数据类型进行动态返回
    5. 基于请求参数内容协商:(需要开启)
    6. ​ 发送请求 GET /projects/spring-boot?format=json
    7. ​ 匹配到 @GetMapping("/projects/spring-boot")
    8. ​ 根据参数协商,优先返回 json 类型数据【需要开启参数匹配设置
    9. ​ 发送请求 GET /projects/spring-boot?format=xml,优先返回 xml 类型数据
效果演示

请求同一个接口,可以返回json和xml不同格式数据

  1. 引入支持写出xml内容依赖

    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
  2. 标注注解

    @JacksonXmlRootElement  // 可以写出为xml文档
    @Data
    public class Person {
        private Long id;
        private String userName;
        private String email;
        private Integer age;
    }
  3. 开启基于请求参数的内容协商

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

image-20230702215957810

image-20230702215935364

自定义内容返回

增加yaml返回支持

导入依赖

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

把对象写出成YAML

public static void main(String[] args) throws JsonProcessingException {
    Person person = new Person();
    person.setId(1L);
    person.setUserName("张三");
    person.setEmail("[email protected]");
    person.setAge(18);

    YAMLFactory factory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
    ObjectMapper mapper = new ObjectMapper(factory);

    String s = mapper.writeValueAsString(person);
    System.out.println(s);
}

编写配置

#新增一种媒体类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

增加HttpMessageConverter组件,专门负责把对象写出为yaml格式

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

模板引擎

image-20230703091109556

模板引擎页面默认放在 src/main/resources/templates

SpringBoot 包含以下模板引擎的自动配置

  • FreeMarker
  • Groovy
  • Thymeleaf
  • Mustache

Thymeleaf官网https://www.thymeleaf.org

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"`>
<head>
	<title>Good Thymes Virtual Grocery</title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/gtvg.css}" />
</head>
<body>
	<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
</body
</html>

Thymeleaf整合

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

自动配置原理

  1. 开启了org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration自动配置
  2. 属性绑定在 ThymeleafProperties 中,对应配置文件 spring.thymeleaf 内容
  3. 所有的模板页面默认在 classpath:/templates文件夹下
  4. 默认效果
    1. 所有的模板页面在 classpath:/templates/下面找
    2. 找后缀名为.html的页面

基本语法

th:xxx动态渲染指定的 html 标签属性值、或者th指令(遍历、判断等)

  • th:text:标签体内文本值渲染

    • th:utext:不会转义,显示为html原本的样子。
  • th:属性:标签指定属性渲染

  • th:attr:标签任意属性渲染

  • th:if``th:each``...:其他th指令

  • 例如:

<p th:text="${content}">原内容</p>
<a th:href="${url}">登录</a>
<img src="../../images/gtvglogo.png" 
	th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

表达式用来动态取值

  • ${}:变量取值;使用model共享给页面的值都直接用${}
  • @{}:url路径;
  • #{}:国际化消息
  • ~{}:片段引用
  • *{}:变量选择:需要配合th:object绑定对象

系统工具&内置对象:详细文档

  • param:请求参数对象
  • session:session对象
  • application:application对象
  • #execInfo:模板执行信息
  • #messages:国际化消息
  • #uris:uri/url工具
  • #conversions:类型转换工具
  • #dates:日期工具,是java.util.Date对象的工具类
  • #calendars:类似#dates,只不过是java.util.Calendar对象的工具类
  • #temporals: JDK8+ **java.time** API 工具类
  • #numbers:数字操作工具
  • #strings:字符串操作
  • #objects:对象操作
  • #bools:bool操作
  • #arrays:array工具
  • #lists:list工具
  • #sets:set工具
  • #maps:map工具
  • #aggregates:集合聚合工具(sum、avg)
  • #ids:id生成工具

属性设置

  1. th:href=”@{/product/list}”

  2. th:attr=”class=${active}”

  3. th:attr=”src=@{/images/gtvglogo.png},title=${logo},alt=#{logo}”

  4. th:checked=”${user.active}”

<p th:text="${content}">原内容</p>
<a th:href="${url}">登录</a>
<img src="../../images/gtvglogo.png" 
     th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

遍历

语法: th:each="元素名,迭代状态 : ${集合}"

<tr th:each="prod : ${prods}">
  <td th:text="${prod.name}">Onions</td>
  <td th:text="${prod.price}">2.41</td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
  <td th:text="${prod.name}">Onions</td>
  <td th:text="${prod.price}">2.41</td>
  <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

iterStat 有以下属性:

  • index:当前遍历元素的索引,从0开始
  • count:当前遍历元素的索引,从1开始
  • size:需要遍历元素的总数量
  • current:当前正在遍历的元素对象
  • even/odd:是否偶数/奇数行
  • first:是否第一个元素
  • last:是否最后一个元素

数据访问

SpringBoot 整合 SpringSpringMVCMyBatis 进行数据访问场景开发

创建SSM整合项目

<!-- 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

安装MyBatisX 插件,帮我们生成Mapper接口的xml文件即可

配置MyBatis

#指定mapper映射文件位置
mybatis.mapper-locations=classpath:/mapper/*.xml
#参数项调整
mybatis.configuration.map-underscore-to-camel-case=true

CRUD编写

  • 编写Bean
  • 编写Mapper
  • 使用mybatisx插件,快速生成MapperXML
  • 测试CRUD

自动配置原理

SSM整合总结:

  1. 导入 mybatis-spring-boot-starter

  2. 配置数据源信息

  3. 配置mybatis的mapper接口扫描xml映射文件扫描

  4. 编写bean,mapper,生成xml,编写sql 进行crud。事务等操作依然和Spring中用法一样

  5. 效果:

    1. 所有sql写在xml中
    2. 所有mybatis配置写在application.properties下面
  • jdbc场景的自动配置

    • mybatis-spring-boot-starter导入 spring-boot-starter-jdbc,jdbc是操作数据库的场景
    • Jdbc场景的几个自动配置
      • org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
        • 数据源的自动配置
        • 所有和数据源有关的配置都绑定在DataSourceProperties
        • 默认使用 HikariDataSource
      • org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
        • 给容器中放了JdbcTemplate操作数据库
      • org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration
      • org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration
        • 基于XA二阶提交协议的分布式事务数据源
      • org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
        • 支持事务
    • 具有的底层能力:数据源、JdbcTemplate事务
  • 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定义信息,注册到容器中。

如何分析哪个场景导入以后,开启了哪些自动配置类。

找:classpath:/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中配置的所有值,就是要开启的自动配置类,但是每个类可能有条件注解,基于条件注解判断哪个自动配置类生效了。

快速定位生效的配置

#开启调试模式,详细打印开启了哪些自动配置
debug=true
# Positive(生效的自动配置)  Negative(不生效的自动配置)

基础特性

SpringApplication

自定义 banner

  1. 类路径添加banner.txt或设置spring.banner.location就可以定制 banner
  2. 推荐网站:Spring Boot banner 在线生成工具

自定义 SpringApplication

import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(MyApplication.class);
        application.setBannerMode(Banner.Mode.OFF);
        application.run(args);
    }
}

FluentBuilder API

new SpringApplicationBuilder()
    .sources(Parent.class)
    .child(Application.class)
    .bannerMode(Banner.Mode.OFF)
    .run(args);

Profiles

环境隔离能力;快速切换开发、测试、生产环境

步骤:

  1. 标识环境:指定哪些组件、配置在哪个环境生效
  2. 切换环境:这个环境对应的所有组件和配置就应该生效

使用

指定环境

  • Spring Profiles 提供一种隔离配置的方式,使其仅在特定环境生效;
  • 任何@Component, @Configuration 或 @ConfigurationProperties 可以使用 @Profile 标记,来指定何时被加载。【容器中的组件都可以被 @Profile标记】

环境激活

  1. 配置激活指定环境; 配置文件
spring.profiles.active=production,hsqldb
  1. 也可以使用命令行激活。–spring.profiles.active=dev,hsqldb

  2. 还可以配置默认环境; 不标注@Profile 的组件永远都存在。

    1. 以前默认环境叫default
    2. spring.profiles.default=test
  3. 推荐使用激活方式激活指定环境

环境包含

注意:

  1. spring.profiles.active 和spring.profiles.default 只能用到 无 profile 的文件中,如果在application-dev.yaml中编写就是无效的
  2. 也可以额外添加生效文件,而不是激活替换。比如:
spring.profiles.include[0]=common
spring.profiles.include[1]=local

最佳实战:

  • 生效的环境 = 激活的环境/默认环境 + 包含的环境

  • 项目里面这么用

    • 基础的配置mybatislogxxx:写到包含环境中
    • 需要动态切换变化的 dbredis:写到激活的环境中

Profile分组

创建prod组,指定包含db和mq配置

spring.profiles.group.prod[0]=db
spring.profiles.group.prod[1]=mq

使用–spring.profiles.active=prod ,就会激活prod,db,mq配置文件

Profile 配置文件

  • application-{profile}.properties可以作为指定环境的配置文件

  • 激活这个环境,配置就会生效。最终生效的所有配置

    • application.properties:主配置文件,任意时候都生效
    • application-{profile}.properties:指定环境配置文件,激活指定环境生效

profile优先级 > application

外部化配置

场景:线上应用如何快速修改配置,并应用最新配置

  • SpringBoot 使用 配置优先级 + 外部配置 简化配置更新、简化运维。
  • 只需要给jar应用所在的文件夹放一个application.properties最新配置文件,重启项目就能自动应用最新配置

单元测试-JUnit5

SpringBoot 提供一系列测试工具集及注解方便我们进行测试。
spring-boot-test提供核心测试能力,spring-boot-test-autoconfigure 提供测试的一些自动配置。
我们只需要导入spring-boot-starter-test 即可整合测试

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

spring-boot-starter-test 默认提供了以下库供我们测试使用

核心原理

事件和监听器

自动配置原理

自定义starter

场景整合

Docker安装

sudo yum install -y yum-utils

sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo systemctl enable docker --now

#测试工作
docker ps
#  批量安装所有软件
docker compose  

创建 /prod 文件夹,准备以下文件

prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'redis'
    static_configs:
      - targets: ['redis:6379']

  - job_name: 'kafka'
    static_configs:
      - targets: ['kafka:9092']

docker-compose.yml

version: '3.9'

services:
  redis:
    image: redis:latest
    container_name: redis
    restart: always
    ports:
      - "6379:6379"
    networks:
      - backend

  zookeeper:
    image: bitnami/zookeeper:latest
    container_name: zookeeper
    restart: always
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    networks:
      - backend

  kafka:
    image: bitnami/kafka:3.4.0
    container_name: kafka
    restart: always
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      ALLOW_PLAINTEXT_LISTENER: yes
      KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    networks:
      - backend
  
  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    container_name:  kafka-ui
    restart: always
    depends_on:
      - kafka
    ports:
      - "8080:8080"
    environment:
      KAFKA_CLUSTERS_0_NAME: dev
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
    networks:
      - backend

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: always
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    networks:
      - backend

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: always
    depends_on:
      - prometheus
    ports:
      - "3000:3000"
    networks:
      - backend

networks:
  backend:
    name: backend

启动环境

docker compose -f docker-compose.yml up -d

NoSQL

Redis

依赖导入

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

配置

spring.data.redis.host=192.168.200.100
spring.data.redis.port=6379

测试

@Autowired
StringRedisTemplate redisTemplate;

@Test
void redisTest(){
    redisTemplate.opsForValue().set("a","1234");
    Assertions.assertEquals("1234",redisTemplate.opsForValue().get("a"));
}

自动配置原理

  1. META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中导入了RedisAutoConfiguration、RedisReactiveAutoConfiguration和RedisRepositoriesAutoConfiguration。所有属性绑定在RedisProperties`中

  2. RedisReactiveAutoConfiguration属于响应式编程,不用管。RedisRepositoriesAutoConfiguration属于 JPA 操作,也不用管

  3. RedisAutoConfiguration 配置了以下组件

    1. LettuceConnectionConfiguration: 给容器中注入了连接工厂LettuceConnectionFactory,和操作 redis 的客户端DefaultClientResources。
    2. RedisTemplate<Object, Object>: 可给 redis 中存储任意对象,会使用 jdk 默认序列化方式。
      1. StringRedisTemplate: 给 redis 中存储字符串,如果要存对象,需要开发人员自己进行序列化。key-value都是字符串进行操作··

定制化

序列化机制

@Configuration
public class AppRedisConfiguration {


    /**
     * 允许Object类型的key-value,都可以被转为json进行存储。
     * @param redisConnectionFactory 自动配置好了连接工厂
     * @return
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //把对象转为json字符串的序列化工具
        template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

redis客户端

RedisTemplate、StringRedisTemplate: 操作redis的工具类

  • 要从redis的连接工厂获取链接才能操作redis

  • Redis客户端

    • Lettuce: 默认
    • Jedis:可以使用以下切换
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!--        切换 jedis 作为操作redis的底层客户端-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

参考配置

spring.data.redis.host=8.130.74.183
spring.data.redis.port=6379
#spring.data.redis.client-type=lettuce

#设置lettuce的底层参数
#spring.data.redis.lettuce.pool.enabled=true
#spring.data.redis.lettuce.pool.max-active=8

spring.data.redis.client-type=jedis
spring.data.redis.jedis.pool.enabled=true
spring.data.redis.jedis.pool.max-active=8

远程调用

RPC(Remote Procedure Call):远程过程调用

本地过程调用: a(); b(); a() { b();}: 不同方法都在同一个JVM运行

远程过程调用

  • 服务提供者:
  • 服务消费者:
  • 通过连接对方服务器进行请求\响应交互,来实现调用效果

API/SDK的区别是什么?

  • api:接口(Application Programming Interface)

    • 远程提供功能;
  • sdk:工具包(Software Development Kit)

    • 导入jar包,直接调用功能即可

***开发过程中***,我们经常需要调用别人写的功能

  • 如果是**内部*微服务,可以通过依赖cloud**注册中心、openfeign***等进行调用
  • 如果是**外部*暴露的,可以发送 http 请求、或遵循外部协议***进行调用

SpringBoot 整合提供了很多方式进行远程调用

  • *轻量级客户端方式*

    • ***RestTemplate***: 普通开发
    • ***WebClient***: 响应式编程开发
    • ***Http Interface***: 声明式编程
  • ***Spring Cloud分布式***解决方案方式

    • Spring Cloud OpenFeign
  • *第三方框架*

    • Dubbo
    • gRPC

WebClient

非阻塞、响应式HTTP客户端

创建于配置

发请求:

  • 请求方式: GET\POST\DELETE\xxxx
  • 请求路径: /xxx
  • 请求参数:aa=bb&cc=dd&xxx
  • 请求头: aa=bb,cc=ddd
  • 请求体:

创建 WebClient 非常简单:

  • WebClient.create()
  • WebClient.create(String baseUrl)

还可以使用 WebClient.builder() 配置更多参数项:

  • uriBuilderFactory: 自定义UriBuilderFactory ,定义 baseurl.
  • defaultUriVariables: 默认 uri 变量.
  • defaultHeader: 每个请求默认头.
  • defaultCookie: 每个请求默认 cookie.
  • defaultRequest: Consumer 自定义每个请求.
  • filter: 过滤 client 发送的每个请求
  • exchangeStrategies: HTTP 消息 reader/writer 自定义.
  • clientConnector: HTTP client 库设置.
//获取响应完整信息
WebClient client = WebClient.create("https://example.org");

获取响应

retrieve()方法用来声明如何提取响应数据。比如

//获取响应完整信息
WebClient client = WebClient.create("https://example.org");

Mono<ResponseEntity<Person>> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .toEntity(Person.class);

//只获取body
WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(Person.class);

//stream数据
Flux<Quote> result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlux(Quote.class);

//定义错误处理
Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError, response -> ...)
        .onStatus(HttpStatus::is5xxServerError, response -> ...)
        .bodyToMono(Person.class);

天气调用结果图

image-20230712095436425

HTTP Interface

Spring 允许我们通过定义接口的方式,给任意位置发送 http 请求,实现远程调用,可以用来简化 HTTP 远程访问。需要webflux场景才可

导入依赖

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

定义接口

public interface BingService {

    @GetExchange(url = "/search")
    String search(@RequestParam("q") String keyword);
}

创建代理&测试

@SpringBootTest
class Boot05TaskApplicationTests {

    @Test
    void contextLoads() throws InterruptedException {
        //1、创建客户端
        WebClient client = WebClient.builder()
                .baseUrl("https://cn.bing.com")
                .codecs(clientCodecConfigurer -> {
                    clientCodecConfigurer
                            .defaultCodecs()
                            .maxInMemorySize(256*1024*1024);
                            //响应数据量太大有可能会超出BufferSize,所以这里设置的大一点
                })
                .build();
        //2、创建工厂
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
                .builder(WebClientAdapter.forClient(client)).build();
        //3、获取代理对象
        BingService bingService = factory.createClient(BingService.class);


        //4、测试调用
        Mono<String> search = bingService.search("尚硅谷");
        System.out.println("==========");
        search.subscribe(str -> System.out.println(str));

        Thread.sleep(100000);
    }
}

消息服务

Web安全

  • Apache Shiro
  • Spring Security
  • 自研:Filter

Spring Security

认证:Authentication

who are you?
登录系统,用户系统

授权:Authorization

what are you allowed to do?
权限管理,用户授权

攻击防护

● XSS(Cross-site scripting)
● CSRF(Cross-site request forgery)
● CORS(Cross-Origin Resource Sharing)
● SQL注入
● …

扩展:权限模型

  1. RBAC(Role Based Access Controll)

    ● 用户(t_user)
    ○ id,username,password,xxx
    ○ 1,zhangsan
    ○ 2,lisi
    ● 用户_角色(t_user_role)【N对N关系需要中间表】
    ○ zhangsan, admin
    ○ zhangsan,common_user
    ○ lisi, hr
    ○ lisi, common_user
    ● 角色(t_role)
    ○ id,role_name
    ○ admin
    ○ hr
    ○ common_user
    ● 角色_权限(t_role_perm)
    ○ admin, 文件r
    ○ admin, 文件w
    ○ admin, 文件执行
    ○ admin, 订单query,create,xxx
    ○ hr, 文件r
    ● 权限(t_permission)
    ○ id,perm_id
    ○ 文件 r,w,x
    ○ 订单 query,create,xxx

  2. ACL(Access Controll List)

    直接用户和权限挂钩
    ● 用户(t_user)
    ○ zhangsan
    ○ lisi
    ● 用户_权限(t_user_perm)
    ○ zhangsan,文件 r
    ○ zhangsan,文件 x
    ○ zhangsan,订单 query
    ● 权限(t_permission)
    ○ id,perm_id
    ○ 文件 r,w,x
    ○ 订单 query,create,xxx

@Secured("文件 r")
public void readFile(){
    //读文件
}

Spring Security 原理

过滤器链架构

Spring Security利用 FilterChainProxy 封装一系列拦截器链,实现各种安全拦截功能

Servlet三大组件:Servlet、Filter、Listener

image-20230704091507829

FilterChainProxy

SecurityFilterChain

使用

HttpSecurity

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/match1/**")
      .authorizeRequests()
        .antMatchers("/match1/user").hasRole("USER")
        .antMatchers("/match1/spam").hasRole("SPAM")
        .anyRequest().isAuthenticated();
  }
}

MethodSecurity

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

@Service
public class MyService {

  @Secured("ROLE_USER")
  public String secure() {
    return "Hello Security";
  }
}

核心

  • WebSecurityConfigurerAdapter

  • @EnableGlobalMethodSecurity: 开启全局方法安全配置

    • @Secured
    • @PreAuthorize
    • @PostAuthorize
  • UserDetailService: 去数据库查询用户详细信息的service(用户基本信息、用户角色、用户权限)

实战

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
    <!-- Temporary explicit version to fix Thymeleaf bug -->
    <version>3.1.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

页面

首页

<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>

Hello页

<h1>Hello</h1>

登录页

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
  <head>
    <title>Spring Security Example</title>
  </head>
  <body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
      <div>
        <label> User Name : <input type="text" name="username" /> </label>
      </div>
      <div>
        <label> Password: <input type="password" name="password" /> </label>
      </div>
      <div><input type="submit" value="Sign In" /></div>
    </form>
  </body>
</html>

SpringBoot
https://junyyds.top/2023/06/29/SpringBoot/
作者
Phils
发布于
2023年6月29日
更新于
2024年6月21日
许可协议