@ConfigurationProperties //可以将全局配置文件中的配置数据绑定到 JavaBean 中 @Value //绑定单个 上面是绑定全部的 @PropertySource(value = "classpath:person.properties")//指向对应的配置文件 @ImportResource //导入 Spring 配置文件 @Configuration //注解定义配置类,注解用于定义一个配置类,相当于 Spring 的配置文件 @SpringBootApplication //是一个组合元注解 @EnableAutoConfiguration //注解用于开启 Spring Boot 的自动配置功能
Thymeleaf 语法规则
就是在若依中常见的 :th
拥有自己的语法分为两类:标准表达式语法、th属性
支持多种表达式:
${...} 变量表达式 获取对象的属性和方法: ${person.lastName} 获取person对象的lastName属性 使用内置基本对象: #ctx:上下文对象 #vars:上下文变量 #locale:上下文语言环境 #request:HttpServletRequest对象(仅在web应用中可用) #response:HttpServletResponse对象(仅在web应用中可用) #session:HttpSession对象(仅在web应用中可用) #servletContext:ServletContext对象(仅在web对象中可用) 实例:${#session.getAttribute('map')} 相当于 ${session.map} 使用内置工具对象: strings:字符串工具对象 他的方法可以去百度搜索就不一一陈列了 numbers:数字工具对象 bools:布尔对象 arrays:数组工具对象 lists/sets:List/Set集合工具对象对象 maps:map集合工具对象 dates:日期工具对象 实例:${#strings.equals('张三',name)} 判断字符串与对象的属性是否相等 *{...} 选择表达式 选择变量表达式与变量表达式基本一致,就是在变量表达式基础上与th:object的配合使用,当使用th:object储存一个对象后获取该对象属性 * 代表该对象
<div th:object="${session.user}"> <p th:text="*{fisrName}}">fisrName</p> </div>
th:object 用于储存一个临时变量该变量只在该标签以及后代中有效
@{…} 链接表达式:不管是静态资源的引用还是form表单的请求,都可以用链接表达式
结构如下:
无参请求:@{/xxx}
有参请求:@{/xxx(k1=v1,k2=v2)}
链接表达式引入CSS
#{...} 国际化表达式 th:text=#{"msg"} ~{...} 片段引用表达式 用于在模板页面引用其他模板 ~{templatename::fragmentname} 说明:templatename是模板名,Thymeleaf会根据模板名解析完整路径名 fragmentname是片段名,Thymeleaf会通过th:fragment声明代码块, th:fragment="fragmentname" id:HTML 的id选择器使用时要在前面加上# 不支持class选择器 th 属性
th:id 替换HTML的id属性 <input id="html-id" th:id="thymeleaf-id"> th:text 文本替换转义特殊字符 <h1 th:text="hello,bianchengbang"> th:utext 文本替换,不转义特殊字符 <div th:utext="<h1>欢迎来到变成世界</h1>">欢迎你</div> th:object 在父标签选择对象,子标签使用*{...}选择表达式取值,即使选择了对象子标签仍然可以使用变量表达式 <div th:object="${session.user}}"> <p th:text="*{firstname}">firstname</p> </div> th:value 替换value属性 <imput th:value="${user.name}"> th:with 局部变量赋值运算 <div th:with="isEvens=${prodStat.count}%2=0" th:text="${isEvens}"></div> th:style 设置样式 <div th:style="'clolr:#FFFFFF;font-weight:bold'">宝宝宝宝</div> th:onclick 点击事件 <td th:onclick="'getInfo()'"></td> th:each 遍历,支持Iterable、Map、数组等 <table> <tr th:each="m:${session.map}"> <td th:text="${m.getKey()}"></td> <td th:text="${m.getValue()}"></td> </tr> </table> th:if 根据条件判断是否展示此标签 <a th:if="${userId=collect.userId}"> th:unless 跟th:if 相反 <div th:unless="${m.getKey()='name')}"></div> th:switch 与Java的switch case语句类似 与th:case配合使用 <div th:switch="${name}"> <a th:case="a">11</a> <a th:case="b">22</a> </div> th:fragment 模板布局,类似JSP的tag 用来定义一段呗引用或包含模板片段 <footer th:fragment="footer">插入内容</footer> th:insert 布局标签 将使用th:fragment属性指定模板片段插入到当前标签中 <div th:insert="commons/bar::footer"></div> th:replace 布局标签 将使用th:fragment 属性指定的模板片段(包含标签)替换当前标签 <div th:replace="commons/bar::footer"></div> th:selected select选择框选中 <select> <option>---</option> <option th:selected="${name='a'}">A</option> <option th:selected="${name='b'}">B</option> </select> th:src 替换HTML中的src属性 <img th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg"> th:inline 内联属性 该属性有text\none\javascript三种取值 在<script>中使用时js代码可以获取后台传递的页面对象 <script type="text/javascript" th:inline="javascript"> var name = /*[[#{name}]]*/ 'biannchengbang'; alert(name) </script> th:action 替换表单提交地址 <form th:action="@{/user/login}" th:method="post"></form>
公共页面抽取
在 Web 项目中,通常会存在一些公共页面片段(重复代码),例如头部导航栏、侧边菜单栏和公共的 js css 等。我们一般会把这些公共页面片段抽取出来,存放在一个独立的页面中,然后再由其他页面根据需要进行引用,这样可以消除代码重复,使页面更加简洁。
Thymeleaf 作为一种优雅且高度可维护的模板引擎,同样支持公共页面的抽取和引用。我们可以将公共页面片段抽取出来,存放到一个独立的页面中,并使用 Thymeleaf 提供的 th:fragment 属性为这些抽取出来的公共页面片段命名。
实例:将公共特面片段抽取出来,存放在commons.html中,
<div th:fragment="fragment-name" id="fragment-id"> <span>公共页面片段</span> </div>
引用公共页面
可以使用以下三个属性,将公共页面片段引入到当前页面中
th:insert:将代码片段整个插入到thinsert属性的HTML标签中
th:replace:将代码块片段整个替换使用了th:replace属性的HTML标签中
th:include:将代码块片段包含的内容插入到使用了th:include属性的HTML标签中
使用上3个属性引入页面片段都可以通过一下2中方式实现
~{remplatename::selector} :模板名::选择器
{remplatename::fragmentname}:模板名::片段名(通常{…}可以省略)
传递参数
Thymeleaf 在抽取和引入公共页面片段时,还可以进行参数传递,大致步骤如下:
1,传入参数
2种方式 模板名::选择器名或片段名(参数1=参数值1,参数2=参数值2) //用于参数多 模板名::选择器名或片段名(参数值1,参数值2) //用于参数少 <div th:insert="commons::fragment-name(varl='insert-name',var2='insert-name2')"></div> <div th:insert="commons::#fragment-id(varl='insert-id',var2='insert-id2')"></div> <!--th:insert 片段名引入--> <div th:insert="commons::fragment-name(var1='insert-name',var2='insert-name2')"></div> <!--th:insert id 选择器引入--> <div th:insert="commons::#fragment-id(var1='insert-id',var2='insert-id2')"></div> ------------------------------------------------ <!--th:replace 片段名引入--> <div th:replace="commons::fragment-name(var1='replace-name',var2='replace-name2')"></div> <!--th:replace id 选择器引入--> <div th:replace="commons::#fragment-id(var1='replace-id',var2='replace-id2')"></div> ------------------------------------------------ <!--th:include 片段名引入--> <div th:include="commons::fragment-name(var1='include-name',var2='include-name2')"></div> <!--th:include id 选择器引入--> <div th:include="commons::#fragment-id(var1='include-id',var2='include-id2')"></d
,使用参数
<!--使用 var1 和 var2 声明传入的参数,并在该片段中直接使用这些参数 --> <div th:fragment="fragment-name(var1,var2)" id="fragment-id"> <p th:text="'参数1:'+${var1} + '-------------------参数2:' + ${var2}">...</p> </div>
示例:
创建一个名为 hello.html 的页面,并将该页面放在项目类路径(resources)下的 templates 目录中,hello.html 代码如下。
<!DOCTYPE html> <!--导入thymeleaf的名称空间--> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!--th:text 为 Thymeleaf 属性,用于获取指定属性的值--> <h1 th:text="'欢迎来到'+${name}"></h1> </body> </html>
新建一个控制类 HelloController,并通过参数 map 传递数据到前台页面中,代码如下。
package net.biancheng.www.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import java.util.Map; @Controller public class HelloController { @RequestMapping("/hello") public String hello(Map<String, Object> map) { //通过 map 向前台页面传递数据 map.put("name", "编程帮(www.biancheng.net)"); return "hello"; } }
就可以访问到了
拦截器
主要应用于登陆校验、权限验证、乱码解决、性能监控和异常处理等功能上
componet包中创建一个名为 LoginInterceptor 的拦截器类,对登陆进行拦截
package com.example.demo.componet; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j public class LoginInterceptor implements HandlerInterceptor { // 目标方法执行前 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null){ //未登录返回登录页 request.setAttribute("msg","您还没有此权限,请先登录"); request.getRequestDispatcher("index.html").forward(request,response); return false; }else { return true; } } // 目标方法执行后 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("postHandle执行",modelAndView); } // 页面渲染后 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("afterCompletion执行异常",ex); } }
注册拦截器
创建一个实现了 WebMvcConfigurer 接口的配置类(使用了 @Configuration 注解的类),重写 addInterceptors() 方法,并在该方法中调用 registry.addInterceptor() 方法将自定义的拦截器注册到容器中。
@Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override // InterceptorRegistry public void addInterceptors( InterceptorRegistry registry){ registry.addInterceptor(new LoginInterceptor()); } }
指定拦截规则
修改 MyMvcConfig 配置类中 addInterceptors() 方法的代码
@Slf4j @Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors( InterceptorRegistry registry){ log.info("注册拦截器"); registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**")//拦截所有请求,包括静态资源文件 .excludePathPatterns("/", "/login", "/index.html", "/user/login", "/css/**", "/images/**", "/js/**", "/fonts/**");//放行登录页,登陆套作,静态资源 } }
制定拦截器规则用了两个方法
addInterceptor:拦截方法,参数为要拦截的路径
excludePathPatterns:排除拦截路径
关于错误页面的优先级问题
自定义动态错误页面(精确匹配)>自定义静态错误页面(精确匹配)>自定义动态错误页面(模糊匹配)>自定义静态错误页面(模糊匹配)>自定义 error.html
自定义异常
package com.example.demo.exception; public class UserNotExistException extends RuntimeException{ public UserNotExistException(){ super("用户名不存在"); } }
异常处理类 在controller包内
package com.example.demo.controller; import com.example.demo.exception.UserNotExistException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(UserNotExistException.class) public String handleException(Exception e, HttpServletRequest request){ Map<String,Object> map = new HashMap<>(); //向 request 对象传入错误状态码 request.setAttribute("javax.servlet.error.status_code",500); //根据当前处理的异常,自定义的错误数据 map.put("code","user.notexist"); map.put("message",e.getMessage()); //将自定的错误数据传入 request 域中 request.setAttribute("ext",map); return "forward:/error"; } }
自定义一个:错误属性处理工具类 MyErrorAttributes(继承 DefaultErrorAttributes ),通过该类我们便可以添加自定义的错误数据,代码如下:在componet包内
package com.example.demo.componet; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; import org.springframework.stereotype.Component; import org.springframework.web.context.request.WebRequest; import java.util.Map; //向容器中添加自定义的储物属性处理工具 @Component public class MyErrorAttributes extends DefaultErrorAttributes { @Override public Map<String,Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options){ Map<String,Object> errorAttributes = super.getErrorAttributes(webRequest, options); //添加自定义的错误数据 errorAttributes.put("company","com.example.demo"); //获取 MyExceptionHandler 传入 request 域中的错误数据 Map ext = (Map) webRequest.getAttribute("ext",0); errorAttributes.put("ext",ext); return errorAttributes; } }
注册web原生组件
通过组件扫描注册
@WebServlet:用于声明一个 Servlet;
package com.example.demo.servlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; //使用 @WebServlet 注解声明一个 Servlet @WebServlet(name = "MyServlet ",urlPatterns ="/MyServlet") public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException, IOException { doPost(req,resp); } @Override protected void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{ resp.setContentType("text/html;charset=UTF-8"); PrintWriter writer = resp.getWriter(); writer.write("Spring Boot Servlet"); writer.close(); } }
@WebFilter:用于声明一个 Filter;
package com.example.demo.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter(urlPatterns = ("/myServlet")) public class MyFiler implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException{ System.out.println("MyFiler初始化"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("MyFiler doFilter"); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { Filter.super.destroy(); System.out.println("MyFiler 销毁"); } }
@WebListener:用于声明一个 Listener。
package com.example.demo.Listener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; //使用 @WebListener 注解声明一个自定义的 Listener @WebListener public class Listener implements ServletContextListener{ @Override public void contextInitialized(ServletContextEvent sce){ System.out.println("MyListener 监听到 ServletContext 初始化"); } @Override public void contextDestroyed(ServletContextEvent src){ System.out.println("MyListener 监听到 ServletContext 销毁"); } }
在启动类上使用 @ServletComponentScan 注解,扫描以上刚刚声明的 Servlet、Filter 和 Listener,并将它们注册到容器中使用
使用 RegistrationBean 注册
Spring Boot JDBC访问数据库
想要在 Spring Boot 中使用 JDBC 进行数据访问,第一步就是要在 pom.xml 中导入 JDBC 场景启动器:spring-boot-starter-data-jdbc
<!--导入JDBC的场景启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency>
导入 MySQL 的数据库驱动:mysql-connector-java (数据库驱动的版本必须与数据库的版本相对应)
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
配置数据源
#数据源连接信息 spring: datasource: username: root password: root url: jdbc:mysql://127.0.0.1:3306/bianchengbang_jdbc driver-class-name: com.mysql.cj.jdbc.Driver
package com.example.demo; import org.testng.annotations.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; import java.sql.SQLException; @SpringBootTest class SpringBootJdbcApplicationTests { //数据源组件 @Autowired DataSource dataSource; //用于访问数据库的组件 @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads() throws SQLException { System.out.println("默认数据源"+dataSource.getClass()); System.out.println("数据库连接实例"+dataSource.getConnection()); //访问数据库 Integer i = jdbcTemplate.queryForObject("SELECT count(*) from `user`",Integer.class); System.out.println("表中有" + i + "条数据"); } }
Spring Boot数据源配置原理
DataSourceAutoConfiguration(自动配置类) 中共包括以下 5 个内部静态类:
EmbeddedDatabaseCondition(限制条件类)
PooledDataSourceAvailableCondition(限制条件类)
PooledDataSourceCondition (限制条件类)
PooledDataSourceConfiguration(池化数据源自动配置类)
只有当容器中存在池化数据源时, 才可以被实例化,才可以向容器中添加池化数据源
通过 @Import 注解引入了 Hikari、Tomcat、Dbcp2、OracleUcp 和 Generic 五个数据源配置类,它们都是 DataSourceConfiguration 的内部类,且它们的功能类似,都是向容器中添加指定的数据源。
在 Hikari 类中,主要使用以下注解:
@Configuration:表示当前类是一个配置类;
@ConditionalOnMissingBean({DataSource.class}):表示容器中没有用户自定义的数据源时,该配置类才会被实例化;
@ConditionalOnClass({HikariDataSource.class}) :表示必须在类路径中存在 HikariDataSource 类时,Hikari 才会实例化。而 HikariDataSource 类是由 spring- boot-starter-jdbc 默认将其引入的,因此只要我们在 pom.xml 中引入了该 starter, Hikari 就会被实例化(这也是 Spring Boot 2.x 默认使用 HikariCP 作为其数据源的原因)。;
@ConditionalOnProperty( name = {“spring.datasource.type”},havingValue = “com.zaxxer.hikari.HikariDataSource”,matchIfMissing = true): 表示当 Spring Boot 配置文件中,配置了 spring.datasource.type = com.zaxxer.hikari.HikariDataSource(明确指定使用 Hikari 数据源)或者不配置 spring.datasource.type(即默认情况)时,Hikari 才会被实例化。
EmbeddedDatabaseConfiguration(内嵌数据源自动配置类)
该类中并没有任何的方法实现,它的主要功能都是通过 @Import 注解引入 EmbeddedDataSourceConfiguration 类来实现的。
@Import({EmbeddedDataSourceConfiguration.class})
Spring Boot整合Druid数据源
观察数据库连接池和 SQL 的运行情况
Druid 不是 Spring Boot 内部提供的技术,它属于第三方技术,我们可以通过以下两种方式进行整合:
自定义整合 Druid
通过 starter 整合 Druid
引入 JDBC 场景启动器、MySql 数据库驱动 和 Druid 依赖。
<!--导入 JDBC 场景启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <!--导入数据库驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--采用自定义方式整合 druid 数据源--> <!--自定义整合需要编写一个与之相关的配置类--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency>
################################################## JDBC 通用配置 ########################################## spring: datasource: username: root #数据库登陆用户名 password: root #数据库登陆密码 url: jdbc:mysql://127.0.0.1:3306/bianchengbang_jdbc #数据库url driver-class-name: com.mysql.cj.jdbc.Driver #数据库驱动 ################################################## Druid连接池的配置 ########################################## spring: datasource: druid: initial-size: 5 #初始化连接大小 min-idle: 5 #最小连接池数量 max-active: 20 #最大连接池数量 max-wait: 60000 #获取连接时最大等待时间,单位毫秒 time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒 validation-query: SELECT 1 FROM DUAL #测试连接 test-while-idle: true #申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性 test-on-borrow: false #获取连接时执行检测,建议关闭,影响性能 test-on-return: false #归还连接时执行检测,建议关闭,影响性能 pool-prepared-statements: false #是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭 max-pool-prepared-statement-per-connection-size: 20 #开启poolPreparedStatements后生效 filters: stat,wall #配置扩展插件,常用的插件有=>stat:监控统计 wall:防御sql注入 connection-properties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' #通过connectProperties属性来打开mergeSql功能;慢SQL记录 ###################################################### Druid 监控配置信息 ########################################## spring: datasource: druid: # StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置 stat-view-servlet: enabled: true #是否开启内置监控页面,默认值为 false url-pattern: '/druid/*' #StatViewServlet 的映射路径,即内置监控页面的访问地址 reset-enable: true #是否启用重置按钮 login-username: admin #内置监控页面的登录页用户名 username login-password: admin #内置监控页面的登录页密码 password # WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter web-stat-filter: enabled: true #是否开启内置监控中的 Web-jdbc 关联监控的数据 url-pattern: '/*' #匹配路径 exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' #排除路径 session-stat-enable: true #是否监控session # Spring监控配置,说明请参考Druid Github Wiki,配置_Druid和Spring关联监控配置 aop-patterns: net.biancheng.www.* #Spring监控AOP切入点,如x.y.z.abc.*,配置多个英文逗号分隔 # ####################################################### Druid 监控配置信息 ########################################## spring: datasource: druid: # 对配置已开启的 filters 即 stat(sql 监控) wall(防火墙) filter: #配置StatFilter (SQL监控配置) stat: enabled: true #开启 SQL 监控 slow-sql-millis: 1000 #慢查询 log-slow-sql: true #记录慢查询 SQL #配置WallFilter (防火墙配置) wall: enabled: true #开启防火墙 config: update-allow: true #允许更新操作 drop-table-allow: false #禁止删表操作 insert-allow: true #允许插入操作 delete-allow: true #删除数据操作
Spring Boot整合MyBatis
数据库查出的数据映射到 POJO 实体类上,而实体到数据库的映射则需要我们自己编写 SQL 语句实现
引入依赖
<!--引入 mybatis-spring-boot-starter 的依赖--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency>
配置 MyBatis
###################################### MyBatis 配置###################################### mybatis: # 指定 mapper.xml 的位置 mapper-locations: classpath:mybatis/mapper/*.xml #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名 type-aliases-package: net.biancheng.www.bean configuration: #默认开启驼峰命名法,可以不用设置该属性 map-underscore-to-camel-case: true
创建实体类(根据对应的库名来创建相应的实体类,名字对应上)
创建 Mapper 接口 创建一个 UserMapper 接口,并在该类上使用 @Mapper 注解
创建 Mapper 映射文件 通过 mybatis.mapper-locations 指定的位置中创建 UserMapper.xml
Spring Boot自定义starter
自定义 starter 可以分为以下 7 步:
创建工程
添加 POM 依赖
定义 propertie 类
定义 Service 类
定义配置类
创建 spring.factories文件
构建 starter