Zero、 注意事项

1.1 yml生效问题

yml文件修改后需要JRebel自动重新打包或使用devtools的F9重新编译即可生效

1.2 静态资源html更新问题

修改后自动更新

一、 直用功能

1. 统一结果返回

@Data
public class Result<T> {
//返回码
private Integer code;

//返回消息
private String message;

//返回数据
private T data;

public Result() {
}

// 返回数据
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<T>();
if (data != null)
result.setData(data);
return result;
}

public static <T> Result<T> build(T body, Integer code, String message) {
Result<T> result = build(body);
result.setCode(code);
result.setMessage(message);
return result;
}

public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
Result<T> result = build(body);
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}

public static <T> Result<T> ok() {
return Result.ok(null);
}

/**
* 操作成功
*
* @param data baseCategory1List
* @param <T>
* @return
*/
public static <T> Result<T> ok(T data) {
Result<T> result = build(data);
return build(data, ResultCodeEnum.SUCCESS);
}

public static <T> Result<T> fail() {
return Result.fail(null);
}

/**
* 操作失败
*
* @param data
* @param <T>
* @return
*/
public static <T> Result<T> fail(T data) {
Result<T> result = build(data);
return build(data, ResultCodeEnum.FAIL);
}

public Result<T> message(String msg) {
this.setMessage(msg);
return this;
}

public Result<T> code(Integer code) {
this.setCode(code);
return this;
}
}
@Getter
public enum ResultCodeEnum {
SUCCESS(200,"成功"),
FAIL(201, "失败"),
SERVICE_ERROR(2012, "服务异常"),
DATA_ERROR(204, "数据异常"),
ILLEGAL_REQUEST(205, "非法请求"),
REPEAT_SUBMIT(206, "重复提交"),
ARGUMENT_VALID_ERROR(210, "参数校验异常"),

LOGIN_AUTH(208, "未登陆"),
PERMISSION(209, "没有权限"),
ACCOUNT_ERROR(214, "账号不正确"),
PASSWORD_ERROR(215, "密码不正确"),
LOGIN_MOBLE_ERROR( 216, "账号不正确"),
ACCOUNT_STOP( 217, "账号已停用"),
NODE_ERROR( 218, "该节点下有子节点,不可以删除")
;

private Integer code;

private String message;

private ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}

2. MD5加密

public final class MD5 {
public static String encrypt(String strSrc) {
try {
char hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f'};
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错!!+" + e);
}
}
}

3. JWT生成

public class JwtHelper {
private static long tokenExpiration = 365L * 24 * 60 * 60 * 1000;
private static String tokenSignKey = "123456";

public static String createToken(String userId, String username) {
String token = Jwts.builder()
.setSubject("AUTH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId)
.claim("username", username)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}

public static String getUserId(String token) {
try {
if ("".equals(token)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String) claims.get("userId");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static String getUsername(String token) {
try {
if ("".equals(token)) return "";

Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return (String) claims.get("username");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static void removeToken(String token) {
//jwttoken无需删除,客户端扔掉即可。
}

public static void main(String[] args) {
String token = JwtHelper.createToken("1", "admin");
System.out.println(token);
System.out.println(JwtHelper.getUserId(token));
System.out.println(JwtHelper.getUsername(token));
}
}
eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ
.
H4sIAAAAAAAAAKtWKi5NUrJSCjAK0A0Ndg1S0lFKrShQsjI0MzY2sDQ3MTbQUSotTi3yTFGyMjKEsP0Sc1OBWp6unfB0f7NSLQDxzD8_QwAAAA
.
2eCJdsJXOYaWFmPTJc8gl1YHTRl9DAeEJprKZn4IgJP9Fzo5fLddOQn1Iv2C25qMpwHQkPIGukTQtskWsNrnhQ

1.3.1 实战使用

① pojo

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;

/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;

}

② JWTUtil

public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);

// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);

return builder.compact();
}

/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}

}

③ BaseContext

public class BaseContext {

public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

public static void setCurrentId(Long id) {
threadLocal.set(id);
}

public static Long getCurrentId() {
return threadLocal.get();
}

public static void removeCurrentId() {
threadLocal.remove();
}

}

④ JwtTokenUserInterceptor

@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("当前线程的ID是:"+Thread.currentThread().getId());
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}

//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());

//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long empId = Long.valueOf(claims.get("userId").toString());
log.info("当前用户的id:", empId);
// 将用户Id存入当前线程中
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}

⑤ UserController

@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {

@Autowired
private UserService userService;

@Autowired
private JwtProperties jwtProperties;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信用户登录:{}", userLoginDTO.getCode());

// 微信登录
User user = userService.wxLogin(userLoginDTO);

// 为微信用户生成JWT令牌
HashMap<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);

UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
}

二、 配置相关

2.1 yml配置文件读取

2.1.1 @Value

配合sqEL读取单个数据

取yml配置文件中的数据

java

@Value("${users[1].name}")
private String name
// 取法${"variable"} ${"variable.var"} ${"variable[index]"}

application.yml

users:
-
name: zhangsan
age: 18
-
name: lisi
age: 17

yml数组第二种写法:users:[{name:zhangsan,age:18},{name:lisi,age:17}]

2.1.2 ${value}

yml引用数据

baseDir: c:\windows
# 使用${属性名}引用数据
tempDir: ${baseDir}\temp

2.1.3 Environment封装

yml所有配置数据

java

// 自动装配
@Autowired
private Enviroment env;
// 使用
env.getProperty("server.port");

2.1.4 @ConfigurationProperties

添加依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

封装yml数据到java-pojo中

application.yml

datasource:
driver: com...
url: ...
username: ...
password: ...

java

// 定义为spring管控的bean
@Component
// 指定加载的数据
@ConfigurationProperties(prefix = "datasource")
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
class MyDataSource{
private String driver;
private String url;
...
}

使用

@Autowired
private MyDataSource myDataSource:

三、 Web相关

3.1 静态资源访问

以下四个目录可以直接放置静态文件,.jpg啥的

  • /static
  • /public
  • /resources
  • /META-INF/resources

springboot项目中动态资源放非static目录下,静态资源存放在static目录下,便于未来管理。

3.1.1 默认静态文件路径修改

可以通过以下yml配置实现其他的静态资源路径存放

  • 当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
spring:
web:
resources:
# 修改静态资源路径,这个配置修改的是springboot项目真实路径
static-locations: [classpath:/bamboo/]

注:修改后需要重启服务,否则不生效

3.1.2 webjar支持

自动映射 /webjars/**

默认实现是在WebMvcAutoConfiguration.java类中

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
addResourceHandler(registry, "/webjars/**", "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);
}
});
}

官网查找依赖:https://www.webjars.org/

① pom依赖

<!--jquery webjars-->
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>jquery</artifactId>
<version>3.7.0</version>
</dependency>

② 网页访问:http://localhost/res/webjars/jquery/3.7.0/dist/jquery.js

  • 因为配置了静态资源目录访问前缀,所有中间含有res路径

3.1.3 欢迎页支持

  • 静态资源路径下index.html

    • 可以配置静态资源路径
    • 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
  • spring:
    #  mvc:
    #    static-path-pattern: /res/**   这个会导致welcome page功能失效
    
      resources:
        static-locations: [classpath:/haha/]
    

    ### 3.1.4 自定义favicon

    avicon.ico 放在静态资源目录下即可。

    ```yml
    spring:
    # mvc:
    # static-path-pattern: /res/** 这个会导致 Favicon 功能失效

3.1.5 默认访问静态路径修改

【URL地址访问的静态资源】

spring:
mvc:
# 修改后生效的是http路径,最后一定要/**来匹配,否则不生效
static-path-pattern: /res/**

3.1.6 禁用静态资源配置

spring:
web:
resources:
add-mappings: false 禁用所有静态资源规则

3.2 请求参数处理

  • @xxxMapping;

  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作

    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user *GET-*获取用户 *DELETE-*删除用户 *PUT-*修改用户 *POST-*保存用户
    • 核心Filter;HiddenHttpMethodFilter
      • 用法: 表单method=post,隐藏域 _method=put
      • SpringBoot中手动开启
    • 扩展:如何把_method 这个名字换成我们自己喜欢的。
  • 注:SpringBoot默认不支持form表单提交的rest请求,它只能使用get和post请求来发送

3.2.1 手动开启Rest功能【表单提交】

① yml配置

spring:
mvc:
# 开启Rest功能
hiddenmethod:
filter:
enabled: true

form表单

<form action="/user" method="post">
<!--提交方式必须是post-->
<input name="_method" type="hidden" value="PUT"/>
<input type="submit" value="submit"/>
</form>

controller

@Controller
@ResponseBody
public class IndexController {

@RequestMapping(value = "/test", method = RequestMethod.PUT)
// 直接返回数据需要标注ResponseBody
public String putRest() {
System.out.println("in put method");
return "successPutMmethod";
}
}

② 自定义HiddenHttpMethodFilter

@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}

3.2.2 参数注解

① GET下的参数注解

1)Controller

@RestController
public class ParameterController {

// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
public Map<String, Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
// 将所有的pathVariable数据封装成Map
@PathVariable Map<String, String> pv,
// 获取页面指定的请求头
@RequestHeader("User-Agent") String userAgent,
// 当类型为 Map<String, String>, MultiValueMap<String, String>, or HttpHeaders时获取所有请求头
@RequestHeader Map<String, String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String, String> params,
@CookieValue("COOKIE_SESSION") String COOKIE_SESSION,
@CookieValue("Pycharm-e4eb2b56") Cookie cookie
) {


Map<String, Object> map = new HashMap<>();

map.put("id",id);
map.put("name",name);
map.put("pv",pv);
map.put("userAgent",userAgent);
map.put("headers",header);
map.put("age", age);
map.put("inters", inters);
map.put("params", params);
map.put("COOKIE_SESSION", COOKIE_SESSION);
System.out.println(cookie.getName() + "===>" + cookie.getValue());
return map;
}
}

2)前端提交

<a href="car/2/owner/zhangsan?age=18&inters=basketball&inters=game">请求参数测试</a>

3)获取结果

// http://localhost/car/2/owner/zhangsan?age=18&inters=basketball&inters=game

{
"headers": {
"host": "localhost",
"connection": "keep-alive",
"sec-ch-ua": "\"Chromium\";v=\"116\", \"Not)A;Brand\";v=\"24\", \"Google Chrome\";v=\"116\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"sec-fetch-site": "same-origin",
"sec-fetch-mode": "navigate",
"sec-fetch-user": "?1",
"sec-fetch-dest": "document",
"referer": "http://localhost/",
"accept-encoding": "gzip, deflate, br",
"accept-language": "zh-CN,zh;q=0.9",
"cookie": "Pycharm-e4eb2b56=488f0949-c871-4ec8-a81e-d9c94f731095; sug=3; sugstore=0; baikeVisitId=9d12be53-33e7-44d7-8780-dd8837823b59; COOKIE_SESSION=11569_0_2_2_2_13_1_0_2_2_30_7_0_0_42_0_1688356349_0_1688356307%7C2%230_0_1688356307%7C1"
},
"pv": {
"id": "2",
"username": "zhangsan"
},
"inters": [
"basketball",
"game"
],
"COOKIE_SESSION": "11569_0_2_2_2_13_1_0_2_2_30_7_0_0_42_0_1688356349_0_1688356307|2#0_0_1688356307|1",
"name": "zhangsan",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
"id": 2,
"params": {
"age": "18",
"inters": "basketball"
},
"age": 18
}

4)解析

  • @PathVariable:获取请求路径的值
    • 参数是单个值,获取指定类型的值
    • 无参且类型是Map<String, String>时,将所有的请求路径参数进行做一个封装
  • @RequestHeader:获取请求头信息
    • 参数是单个值,获取指定请求头信息
    • 无参且类型是Map<String, String>, MultiValueMap<String, String>, or HttpHeaders时获取所有请求头
  • @RequestParam:获取GET参数信息
    • 参数是单个值,获取指定类型参数,一参多值时可采用List接收
    • 无参且类型是 Map<String, String> or MultiValueMap<String, String>时,获取所有GET参数的集合
  • @CookieValue:获取Cookie信息
    • 参数是单个值时,获取对应String类型的cookie值
    • 如果类型替换为Cookie,可获取对应Cookie

② POST下的参数注解

1)Controller

@RestController
public class ParameterController {

@PostMapping("/save")
public Map postMethod(@RequestBody String content) {
Map<String, Object> map = new HashMap<>();
map.put("content", content);
return map;
}
}

2)html请求

<h2>POST参数测试</h2>
<form action="/save" method="post">
用户名:<input type="text" name="username" id="username">
邮箱&nbsp;&nbsp;<input type="text" name="email" id="email">
<input type="submit" value="submit">
</form>

3)获取结果

// http://localhost/save

{
"content": "username=bamboo&email=YT000000X%40163.com"
}

③ 请求处理 @RequestAttribute

1)Controller

@Controller
public class RequestController {

@GetMapping("/goto")
public String goToPage(HttpServletRequest request) {
request.setAttribute("msg", "success...");
request.setAttribute("code", 200);
return "forward:/success";
}

@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute("msg") String msg,
@RequestAttribute("code") Integer code,
HttpServletRequest request) {
Object msg1 = request.getAttribute("msg");
Map<String, Object> map = new HashMap<>();

map.put("reqMethod_msg", msg1);
map.put("annotation_msg", msg);
return map;
}
}

2)响应

// http://localhost/goto

{
"reqMethod_msg": "success...",
"annotation_msg": "success..."
}

3)解析

  • 使用HttpServletRequest请求可以将属性放入request请求域中
  • @RequestAttribute与HttpServletRequest.getAttribute功能类似,都可以获取request域中保存的值

④ 矩阵变量@MatrixVariable

1)解除矩阵变量限制

  1. 使用implements WebMvcConfigurer的方式

    @Configuration(proxyBeanMethods = false)
    public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
    UrlPathHelper urlPathHelper = new UrlPathHelper();
    // 移除对矩阵变量的禁用,即——使/cars/sell;low=34;brand=byd,audi,yd生效
    urlPathHelper.setRemoveSemicolonContent(false);
    configurer.setUrlPathHelper(urlPathHelper);
    }
    }
  2. 使用@Bean的方式

    @Configuration(proxyBeanMethods = false)
    public class WebConfig {

    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
    return new WebMvcConfigurer() {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
    UrlPathHelper urlPathHelper = new UrlPathHelper();
    // 移除对矩阵变量的禁用,即——使/cars/sell;low=34;brand=byd,audi,yd生效
    urlPathHelper.setRemoveSemicolonContent(false);
    configurer.setUrlPathHelper(urlPathHelper);
    }
    };
    }
    }

2)Controller

@RestController
public class ParameterController {

// /cars/sell;low=34;brand=byd,audi,yd
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path) {

Map<String, Object> map = new HashMap<>();
map.put("low", low);
map.put("brand", brand);
map.put("path", path);
return map;
}

}

3)html请求

<a href="/cars/sell;low=34;brand=byd,audi,yd">矩阵变量</a>

4)结果

// http://localhost/cars/sell;low=34;brand=byd,audi,yd

{
"path": "sell",
"low": 34,
"brand": [
"byd",
"audi",
"yd"
]
}

5)特殊情况

// /boss/1;age=20/2;age=10

@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();

map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;

}

3.2.3 Servlet API

WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId

ServletRequestMethodArgumentResolver 以上的部分参数

@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}

3.2.4 复杂类型

MapModel(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder

Map<String,Object> map,  Model model, HttpServletRequest request 都是可以给request域中放数据,
request.getAttribute();

Map、Model类型的参数,会返回 mavContainer.getModel();—> BindingAwareModelMap 是Model 也是Map

3.2.5 自定义对象参数

可以自动类型转换与格式化,可以级联封装。

/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {

private String userName;
private Integer age;
private Date birth;
private Pet pet;

}

@Data
public class Pet {

private String name;
private String age;

}

result

3.3 自定义配置

3.3.1 自定义 addFormatters 格式化器

案例:页面提交

<input type="text" name="pet" value="阿猫,6" />

代码【也可采用实现的方式重写方法】:

@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigurer */ {

// WebMvcConfigurer定制化springMVC功能
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
// 重写Converter
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
// 阿猫,3
if (!StringUtils.isEmpty(source)) {
Pet pet = new Pet();
String[] split = source.split(",");
if (split.length > 1) {
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
} else {
pet.setName(split[0]);
pet.setAge(null);
}
return pet;
}
return null;
}
});
}
};
}
}

返回结果

// 20230822124453
// http://localhost/saveuser

{
"username": "zhangsan",
"age": "18",
"birth": "2022-11-10T16:00:00.000+00:00",
"pet": {
"name": "阿猫",
"age": null
}
}

3.3.2 自定义 ResourceHandlers 静态资源路径

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

/**
* 扩展SpringMVC消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
// 创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 消息转换器设置一个对象转换器,对象转换器将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
// 将消息转换器加入到容器中,index为0,优先使用此消息转换
converters.add(0,converter);
}
}

3.3.3 自定义 MessageConverters 内容协商实现JSON转换

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

/**
* 扩展SpringMVC消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
// 创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 消息转换器设置一个对象转换器,对象转换器将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
// 将消息转换器加入到容器中,index为0,优先使用此消息转换
converters.add(0,converter);
}
}

3.3.4 自定义 Interceptors 拦截器

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;

/**
* 注册自定义拦截器
*
* @param registry
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
}

3.4 内容协商

根据客户端接收能力不同,返回不同媒体类型的数据。

例如:JSON XML

引入依赖

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

通过post测试

  • 默认情况下返回JSON格式数据
  • 标注Accept请求头为application/xml时,返回xml格式数据

3.4.1 参数内容协商

因:浏览器中Accept请求头text/html,application/xhtml+xml,application/xml;q=0.9权重过大,默认显示为xml格式,所以开启参数内容协商以访问JSON格式数据

开启方式:

spring:
mvc:
contentnegotiation:
# 开启参数方式内容协商
favor-parameter: true

调用方式:http://localhost/person?format=json

3.4.2 自定义MessageConverter

实现多协议数据兼容。json、xml、x-bamboo

① 编写HttpMessageConverter

此Converter只实现了写功能

public class BambooMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}

@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// 判断是否是Person对象
return clazz.isAssignableFrom(Person.class);
}

@Override
public List<MediaType> getSupportedMediaTypes() {
// 返回支持的媒体类型
return MediaType.parseMediaTypes("application/x-bamboo");
}

@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}

@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String data = person.getUsername() + ";" + person.getAge() + ";" + person.getPet();
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}

② 编写webMvcConfigurer

@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigurer */ {

// WebMvcConfigurer定制化springMVC功能
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 添加自定义的内容协商,此方法是extend,只做扩展不进行覆盖
converters.add(new BambooMessageConverter());
}
};
}
}

③ 添加配置实现自定义参数内容协商

@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigurer */ {
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}

// WebMvcConfigurer定制化springMVC功能
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {

@Override
// 自定义参数内容协商
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json", MediaType.APPLICATION_JSON);
mediaTypes.put("xml", MediaType.APPLICATION_XML);
mediaTypes.put("bamboo", MediaType.parseMediaType("application/x-bamboo"));
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
// 单独配置参数内容协商会使得请求头Accept失效,同时需要配置以下策略
HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();

configurer.strategies(Arrays.asList(parameterStrategy,headerStrategy));
}

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new BambooMessageConverter());
}
};
}

}

3.5 模板引擎-Thymeleaf

3.5.1 thymeleaf 简介

Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.

现代化、服务端Java模板引擎

使用

1)引入Starter

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

2)页面开发

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<a href="www.atguigu.com" th:href="${link}">去百度</a> <br/>
<a href="www.atguigu.com" th:href="@{link}">去百度2</a>
</h2>
</body>
</html>

3.5.2 基本语法

1)表达式

表达式名字 语法 用途
变量取值 ${…} 获取请求域、session域、对象等值
选择变量 *{…} 获取上下文对象值
消息 #{…} 获取国际化等值
链接 @{…} 生成链接
片段表达式 ~{…} jsp:include 作用,引入公共页面片段

2)字面量

文本值: ‘one text’ , ‘Another one!’ ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false

空值: null

变量: one,two,…. 变量不能有空格

3)文本操作

字符串拼接: +

变量替换: |The name is ${name}|

4)数学运算

运算符: + , - , * , / , %

5)布尔运算

运算符: and , or

一元运算: ! , not

6)比较运算

比较: > , <** **,** **>= , <= ( gt , lt , ge , le **)**等式: == , != ( eq , ne )

7)条件运算

If-then: (if) ? (then)

If-then-else: (if) ? (then) : (else)

Default: (value) ?: (defaultvalue)

8)特殊操作

无操作: _

3.5.3 设置属性值-th:attr

设置单个值

<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>

设置多个值

<img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

以上两个的代替写法 th:xxxx

<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">

所有h5兼容的标签写法

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes

3.5.4 迭代

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

3.5.5 条件运算

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>

3.6 拦截器

使用案例:拦截登录请求

1)编写拦截器

@Slf4j
public class LoginIntercepter implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("拦截的路径是:{}",requestURI);

HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null) {
return true;
}
// 拦截后跳转到登录页
// 设置返回提示信息
request.setAttribute("msg", "请先登录");
// 页面跳转
request.getRequestDispatcher("/").forward(request,response);
return false;
}
}

2)编写WebMvcConfigurer

@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercepter())
// 拦截所有路径
.addPathPatterns("/**")
// 放行以下路径,需要放行登录页和静态资源页面,也可以通过设置
.excludePathPatterns("/", "/login", "/css/**","/js/**");
}
}

3)静态资源路径过多时可配置yml来实现路径统一存放

  • 存放路径:文件夹static下
spring:
web:
resources:
# 静态资源路径
static-locations: [classpath:/bamboo/,classpath:/static/]
# 静态资源开启
add-mappings: true
mvc:
static-path-pattern: /static/**
  • 修改放行代码
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginIntercepter())
// 拦截所有路径
.addPathPatterns("/**")
// 放行以下路径,需要放行登录页和静态资源页面,也可以通过设置
.excludePathPatterns("/", "/login", "/static/**");
}
}

3.7 文件上传

  • 参数调用:@RequestPart(“photos”) MultipartFile[] photos
  • yml配置文件上传大小

1)Controller

@Slf4j
@Controller
public class FormController {

@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email, username, headerImg.getOriginalFilename(), photos.length);
if (!headerImg.isEmpty()) {
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("D:\\cache\\" + originalFilename));
}
if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("D:\\cache\\"+originalFilename));
}
}
}
return "index";
}
}

2)html

<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail">
</div>
<div class="form-group">
<label for="exampleInputName">名字</label>
<input type="text" name="username" class="form-control" id="exampleInputName">
</div>
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImg" class="form-control" id="exampleInputFile">
</div>
<div class="form-group">
<label for="exampleInputFiles">生活照</label>
<input type="file" multiple name="photos" class="form-control" id="exampleInputFiles">
</div>
<div class="check-box">
<label>
<input type="checkbox" name="" id="">Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>

3)修改上传大小

spring:
servlet:
multipart:
# 单个文件上传大小限制
max-file-size: 30MB
# 一次请求文件上传大小限制
max-request-size: 1024MB

3.8 异常处理

默认规则

  • 默认情况下,Spring Boot提供/error处理所有错误的映射
  • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据

定制错误处理逻辑

  • 自定义错误页

    • error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
    • 在templates文件下的error文件,其中的4xx,5xx页面会被自动解析
  • @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的

  • @ResponseStatus+自定义异常 ;底层是 ResponseStatusExceptionResolver ,把responsestatus注解的信息底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error

  • Spring底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。

    • response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());

3.8.1 自定义@ControllerAdvice

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handleArithException(Exception e) {
log.error("异常是:{}", e);
return "index";
}
}

3.8.2 ResponseStatus+自定义异常

需要结合error目录下的4xx.html,5xx.html异常页面

@ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException{
public UserTooManyException() {
}

public UserTooManyException(String message) {
super(message);
}
}

在错误页用以下参数名获取

<h1 th:text="${message}">reason的信息</h1>
<h2 th:text="${trace}">异常代码的追踪【很多】</h2>
<h2 th:text="${status}">状态码</h2>

3.8.3 自定义异常处理规则

此项配置后,所有报错都会根据这个内容进行修改,即此项为全局默认错误规则

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
public class CustomHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
response.sendError(511,"自定义规则错误");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}

3.9 原生组件注入

3.9.1 使用Servlet API进行注入

① 开启组件注入

  • SpringBoot主程序添加注解@ServletComponentScan(basePackages = "com.bamboo.boot.servlet")
@ServletComponentScan(basePackages = "com.bamboo.boot.servlet")
@SpringBootApplication
public class MainApplication {

public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}

}

② @WebServlet

@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("测试我的Servlet");
}
}

③ @WebFilter

@Slf4j
@WebFilter(urlPatterns = {"/css/*","/images/*","/my"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}

@Override
public void destroy() {
log.info("MyFilter销毁");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("MyFilter工作");
filterChain.doFilter(servletRequest, servletResponse);
}
}

④ @WebListener

@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}

3.9.2 RegistrationBean进行注入

ServletRegistrationBeanFilterRegistrationBeanServletListenerRegistrationBean

去掉3.9.2中的对应注解,使用另一种方式注入,也无需添加@ServletComponentScan注解

@Configuration
public class MyRegistConfig {

@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();

return new ServletRegistrationBean(myServlet,"/my","/my02");
}


@Bean
public FilterRegistrationBean myFilter(){

MyFilter myFilter = new MyFilter();
// 以下方式直接对上述mySerlvet中的映射进行了拦截
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}

@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}

3.10 CORS跨域

​ 跨源资源共享(CORS)是万维网联盟(W3C)的一项规范,由大多数浏览器实施,可让您以灵活的方式指定授权的跨域请求类型,而不是使用 IFRAME 或 JSONP 等安全性较低、功能较弱的方法。

​ 从 4.2 版开始,Spring MVC 支持 CORS。在 Spring Boot 应用程序中使用带有 @CrossOrigin 注解的控制器方法 CORS 配置不需要任何特定配置。全局 CORS 配置可通过使用自定义 addCorsMappings(CorsRegistry) 方法注册 WebMvcConfigurer Bean 来定义,如下例所示:

@Configuration(proxyBeanMethods = false)
public class MyCorsConfiguration {

@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**");
}

};
}

}

3.11 嵌入式Servlet容器

SpringBoot默认启动的Web容器是Tomcat

3.11.1 切换web容器

屏蔽pom中的Tomcat启动器

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

添加其他web容器依赖

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

3.12 定制化原理

3.12.1 定制化的常见方式

  • 修改配置文件【yml】;
  • xxxxxCustomizer;
  • 编写自定义的配置类 xxxConfiguration;+ @Bean替换、增加容器中默认组件;视图解析器
  • Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{}
  • @EnableWebMvc + WebMvcConfigurer —— @Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能

    • 原理
    • 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…..
    • 2、一旦使用 @EnableWebMvc 、。会 @Import(DelegatingWebMvcConfiguration.class)
    • 3、DelegatingWebMvcConfiguration 的 作用,只保证SpringMVC最基本的使用
      • 把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
      • 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
      • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
    • 4、WebMvcAutoConfiguration 里面的配置要能生效 必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    • 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。
  • … …

3.12.2 原理分析套路

场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项

四、整合第三方技术

4.1 JUnit

springboot创建时默认添加test测试功能

@SpringBootTest => class

@Test => method

一、创建测试对象

// 1.创建接口
public interface BookDao {
void say();
}

// 2.创建实现并标注为Bean
@Repository
public class BookDaoImpl implements BookDao {
@Override
public void say() {
System.out.println("bookdao run...");
}
}

二、**@SpringBootTest测试**

@SpringBootTest
class Module0615ApplicationTests {
// 1.注入测试的对象
@Autowired
private BookDao bookDao;

@Test
void contextLoads() {
// 2.执行测试对象的具体方法
System.out.println("test...");
bookDao.say();
}
}

*注意事项

如将SpringBoot测试类放到SpringBootApplication的父包及以上,则需要设置引导类

@SpringBootTest(classes = SpringBootApplication.class)

@SpringBootTest
@ContextConfiguration(classes = SpringBootApplication.class)

4.2 Mybatis

创建SpringBoot时勾选MybatisFramework和MySQLDriver

yml配置数据库

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/fruitdb?serverTimezone=UTC
username: root
password: root

创建pojo

public class Fruit {
private Integer id;
private String name;
private Double price;
private Integer count;
private String remark;
get set toString...
}

创建Dao

// 标注mapper的Bean
@Mapper
public interface FruitDao {
@Select("select fid as id,fname as name,price,fcount as count,remark from t_fruit where fid = #{id}")
public Fruit getById(Integer id);
}

测试

@SpringBootTest
class Module0615MybatisApplicationTests {
// 注入Dao
@Autowired
private FruitDao fruitDao;

@Test
void contextLoads() {
System.out.println(fruitDao.getById(1));
}
}

*注意事项

  • java-connection-j8.0+时区问题:添加serverTimezone=UTC

4.3 Mybatis-Plus

简称mp

创建SpringBoot项目时勾选MySQLDriver

Bean注入注意事项

在继承BaseMapper时会有两种Bean注入,即@Mapper和@Repository,这两种使用时具有重大不同

  • @Mapper注入:此注入直接使用即可,如无法直接使用在启动器增加@MapperScan注解
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
  • @Repository注入:此注入需要配合@MapperScan注解使用,否则会报错
@Repository
public interface UserMapper extends BaseMapper<User> {
}

@SpringBootApplication
@MapperScan("com.bamboo.warehouseerp.mapper")
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}

依赖添加

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>

yml配置数据库连接

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/fruitdb?serverTimezone=UTC
username: root
password: root

创建pojo

创建Dao

// 指定此接口为Bean,被SpringBoot扫描,如果扫描不到说明位置不对,可以在启动器指定@MapperScan("com.example.dao")进行扫描
@Mapper
public interface FruitDao extends BaseMapper<Fruit> {
}

创建xml(自定义方法或映射路径不同使用)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dao.FruitDao">
<resultMap id="fruitResultMap" type="com.example.domain.Fruit">
<id property="id" column="fid"/>
<result property="name" column="fname"/>
<result property="price" column="price"/>
<result property="count" column="fcount"/>
<result property="remark" column="remark"/>
</resultMap>
<select id="selectById" resultMap="fruitResultMap">
SELECT *
FROM t_fruit
WHERE fid=#{id}
</select>
</mapper>

调用测试

@SpringBootTest
class Module0615MybatisPlusApplicationTests {
@Autowired
private FruitDao fruitDao;

@Test
void contextLoads() {
Fruit fruit = fruitDao.selectById(1);
System.out.println(fruit);
}
}

4.4 Druid

spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/fruitdb?serverTimezone=UTC
username: root
password: root

4.5 Redis

1. redis的java客户端

  • Jedis
  • Lettuce
  • Spring Data Redis 【spring项目中使用】

2. Spring Data Redis使用

  1. 导入spring data redis的maven坐标

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 配置redis数据源

    spring:
    redis:
    host: localhost
    port: 6379
    password: 123456
    database: 10
  3. 编写配置类,创建RedisTemplate对象

    @Configuration
    @Slf4j
    public class RedisConfiguration {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    log.info("开始创建redis模板对象...");
    RedisTemplate redisTemplate = new RedisTemplate();
    // 设置redis的连接工厂对象
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    // 设置redis key的序列化器
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    return redisTemplate;
    }
    }
  4. 通过RedisTemplate对象操作Redis

    // Junit测试
    package com.sky.test;

    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.connection.DataType;
    import org.springframework.data.redis.core.*;

    import java.util.List;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;

    /**
    * @version v1.0
    * @auther Bamboo
    * @create 2023/7/13 21:18
    */
    @SpringBootTest
    public class SpringDataRedisTest {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testRedisTemplate() {
    System.out.println(redisTemplate);
    // string
    ValueOperations valueOperations = redisTemplate.opsForValue();
    // hash
    HashOperations hashOperations = redisTemplate.opsForHash();
    // list
    ListOperations listOperations = redisTemplate.opsForList();
    // set
    SetOperations setOperations = redisTemplate.opsForSet();
    // zset
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    }

    /**
    * 操作字符串数据
    */
    @Test
    public void testString() {
    ValueOperations valueOperations = redisTemplate.opsForValue();
    // set get setex setnx
    valueOperations.set("city", "北京");
    String city = (String) valueOperations.get("city");
    valueOperations.set("code", "1234", 3, TimeUnit.MINUTES);
    valueOperations.setIfAbsent("lock", 1);
    valueOperations.setIfAbsent("lock", 2);
    }

    /**
    * 操作哈希数据
    */
    @Test
    public void testHash() {
    //hset hget hdel hkeys hvals
    HashOperations hashOperations = redisTemplate.opsForHash();

    hashOperations.put("100", "name", "tom");
    hashOperations.put("100", "age", "20");

    String name = (String) hashOperations.get("100", "name");
    System.out.println(name);

    Set keys = hashOperations.keys("100");
    System.out.println(keys);

    List values = hashOperations.values("100");
    System.out.println(values);

    hashOperations.delete("100", "age");
    }

    /**
    * 操作列表数据
    */
    @Test
    public void testList() {
    //lpush lrange rpop llen
    ListOperations listOperations = redisTemplate.opsForList();

    listOperations.leftPushAll("mylist", "a", "b", "c");
    listOperations.leftPush("mylist", "d");

    List mylist = listOperations.range("mylist", 0, -1);
    System.out.println(mylist);

    Object popValue = listOperations.rightPop("mylist");
    System.out.println(popValue);

    Long size = listOperations.size("mylist");
    System.out.println(size);
    }

    /**
    * 操作集合数据
    */
    @Test
    public void testSet() {
    // sadd smemebers scard sinter sunion srem
    SetOperations setOperations = redisTemplate.opsForSet();

    setOperations.add("set1", "a", "b", "c", "d");
    setOperations.add("set2", "a", "b", "x", "y");

    Set members = setOperations.members("set1");
    System.out.println(members);

    Long size = setOperations.size("set1");
    System.out.println(size);

    Set intersect = setOperations.intersect("set1", "set2");
    System.out.println(intersect);

    Set union = setOperations.union("set1", "set2");
    System.out.println(union);

    setOperations.remove("set1", "a", "b");
    }

    /**
    * 操作有序集合类型的数据
    */
    @Test
    public void testZset() {
    // zadd zrange zincrby zrem
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    zSetOperations.add("zset1", "a", 10);
    zSetOperations.add("zset1", "b", 12);
    zSetOperations.add("zset1", "c", 9);

    Set zset1 = zSetOperations.range("zset1", 0, -1);
    System.out.println(zset1);

    zSetOperations.incrementScore("zset1", "c", 10);

    zSetOperations.remove("zset1", "a", "b");
    }

    /**
    * 通用命令操作
    */
    @Test
    public void testCommon() {
    // keys exists type del
    Set keys = redisTemplate.keys("*");
    System.out.println(keys);

    Boolean name = redisTemplate.hasKey("name");
    Boolean set1 = redisTemplate.hasKey("set1");

    for (Object key : keys) {
    DataType type = redisTemplate.type(key);
    System.out.println(type.name());
    }

    redisTemplate.delete("mylist");
    }
    }

4.6 Lombok

该功能直接引入依赖即可,内部集成了Slf4j日志

@Builder使用.build()进行构建Pojo

@Accessors(chain = true)构建链式表达

五、数据访问

5.1 SQL

5.1.1 数据源的自动配置-HikariDataSource

① 导入JDBC场景

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

注:官方并不自动导入数据库驱动,需手动导入

默认版本:<mysql.version>8.0.22</mysql.version>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- <version>5.1.49</version>-->
</dependency>
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.49</mysql.version>
</properties>

② 自动配置的类

  • DataSourceAutoConfiguration : 数据源的自动配置

    • 修改数据源相关的配置:spring.datasource
    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
    • 底层配置好的连接池是:HikariDataSource
  • DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置

  • JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud

    • 可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate
    • @Bean@Primary JdbcTemplate;容器中有这个组件
  • JndiDataSourceAutoConfiguration: jndi的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

③ 修改配置项

spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver

④ 测试

@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {

@Autowired
JdbcTemplate jdbcTemplate;


@Test
void contextLoads() {

// jdbcTemplate.queryForObject("select * from account_tbl")
// jdbcTemplate.queryForList("select * from account_tbl",)
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}",aLong);
}

}

5.1.2 使用Druid数据源

① druid官方github地址

https://github.com/alibaba/druid

整合第三方技术的两种方式

  • 自定义
  • 找starter

② 自定义方式

1)创建数据源

配置文件方式【已不推荐】

        <dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />

推荐方式

  1. 导入druid依赖

    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.17</version>
    </dependency>
  2. 编写配置类

    @Configuration
    public class MyDataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    return dataSource;
    }
    }
  3. 测试

    @Slf4j
    @SpringBootTest
    class MainApplicationTests {

    @Autowired
    private DataSource dataSource;

    @Test
    void contextLoads() {
    log.info("datasource类型是:"+dataSource.getClass());
    // datasource类型是:class com.alibaba.druid.pool.DruidDataSource
    }

    }
2)StatViewServlet

StatViewServlet的用途包括:

  • 提供监控信息展示的html页面
  • 提供监控信息的JSON API
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>

@Configuration
public class MyDataSourceConfig {

@Bean
public ServletRegistrationBean statViewServlet() {
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
registrationBean.addInitParameter("loginUsername","bamboo");
registrationBean.addInitParameter("loginPassword","druid");
return registrationBean;
}
}
3)StatFilter【打开上面的监控功能】

用于统计监控信息;如SQL监控、URI监控

@Configuration
public class MyDataSourceConfig {

@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
// 开启监控功能
dataSource.setFilters("stat,slf4j,wall");
return dataSource;
}

@Bean
public ServletRegistrationBean statViewServlet() {
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
return registrationBean;
}
}

需要给数据源中配置如下属性;可以允许多个filter,多个用,分割;如:

<property name="filters" value="stat,slf4j" />

系统中所有filter:

别名 Filter类名
default com.alibaba.druid.filter.stat.StatFilter
stat com.alibaba.druid.filter.stat.StatFilter
mergeStat com.alibaba.druid.filter.stat.MergeStatFilter
encoding com.alibaba.druid.filter.encoding.EncodingConvertFilter
log4j com.alibaba.druid.filter.logging.Log4jFilter
log4j2 com.alibaba.druid.filter.logging.Log4j2Filter
slf4j com.alibaba.druid.filter.logging.Slf4jLogFilter
commonlogging com.alibaba.druid.filter.logging.CommonsLogFilter

慢SQL记录配置

<bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
<property name="slowSqlMillis" value="10000" />
<property name="logSlowSql" value="true" />
</bean>

使用 slowSqlMillis 定义慢SQL的时长
4)WebStatFilter

采集web-jdbc关联监控的数据。

@Configuration
public class MyDataSourceConfig {

@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setFilters("stat,slf4j,wall");
return dataSource;
}

@Bean
public ServletRegistrationBean statViewServlet() {
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");
return registrationBean;
}

/**
* webStatFilter 用于采集web-jdbc关联监控的数据
*/
@Bean
public FilterRegistrationBean webStatFilter() {
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
// 排除监控的内容
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}

③ 使用官方starter方式

1)引入druid-starter

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>

2)分析自动配置

  • 扩展配置项 spring.datasource.druid
  • DruidSpringAopConfiguration.class, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
  • DruidStatViewServletConfiguration.class, 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
  • DruidWebStatFilterConfiguration.class, web监控配置;spring.datasource.druid.web-stat-filter;默认开启
  • DruidFilterConfiguration.class}) 所有Druid自己filter的配置
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";

3)配置示例

spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver

druid:
aop-patterns: com.atguigu.admin.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)

stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin
login-password: admin
resetEnable: false

web-stat-filter: # 监控web
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'


filter:
stat: # 对上面filters里面的stat的详细配置
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false

SpringBoot配置示例

https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

配置项列表https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8

DruidDataSource配置兼容DBCP,但个别配置的语意有所区别。

配置 缺省值 说明
name 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:”DataSource-“ + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。详情-点此处
url 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 连接数据库的用户名
password 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里
driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive 8 最大连接池数量
maxIdle 8 已经不再使用,配置了也没效果
minIdle 最小连接池数量
maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxPoolPreparedStatementPerConnectionSize -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery 用来检测连接是否有效的sql,要求是一个查询语句,常用select ‘x’。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validationQueryTimeout 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
keepAlive false (1.0.28) 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
timeBetweenEvictionRunsMillis 1分钟(1.0.14) 有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun 30分钟(1.0.14) 不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis 连接保持空闲而不被驱逐的最小时间
connectionInitSqls 物理连接初始化的时候执行的sql
exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall
proxyFilters 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系

5.1.3 整合MyBatis操作

https://github.com/mybatis

Starter

  • SpringBoot官方的Starter:spring-boot-starter-*
  • 第三方的: *-spring-boot-starter
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>

① 配置模式

  • 全局配置文件
  • SqlSessionFactory: 自动配置好了
  • SqlSession:自动配置了 SqlSessionTemplate 组合了SqlSession
  • @Import(AutoConfiguredMapperScannerRegistrar.class);
  • Mapper: 只要我们写的操作MyBatis的接口标准了 @Mapper 就会被自动扫描进来
@EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{}

@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties{}

可以修改配置文件中 mybatis 开始的所有;

# 配置mybatis规则
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置

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.atguigu.admin.mapper.AccountMapper">
<!-- public Account getAcct(Long id); -->
<select id="getAcct" resultType="com.atguigu.admin.bean.Account">
select * from account_tbl where id=#{id}
</select>
</mapper>

配置 private Configuration configuration; mybatis.configuration下面的所有,就是相当于改mybatis全局配置文件中的值

# 配置mybatis规则
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true

可以不写全局;配置文件,所有全局配置文件的配置都放在configuration配置项中即可
  • 导入mybatis官方starter
  • 编写mapper接口。标准@Mapper注解
  • 编写sql映射文件并绑定mapper接口
  • 在application.yaml中指定Mapper配置文件的位置,以及指定全局配置文件的信息 (建议;配置在mybatis.configuration

② 注解模式

@Mapper
public interface CityMapper {

@Select("select * from city where id=#{id}")
public City getById(Long id);

public void insert(City city);

}

③ 混合模式

@Mapper
public interface CityMapper {

@Select("select * from city where id=#{id}")
public City getById(Long id);

public void insert(City city);

}

④ 使用技巧

  • 引入mybatis-starter
  • 配置application.yaml中,指定mapper-location位置即可
  • 编写Mapper接口并标注@Mapper注解
  • 简单方法直接注解方式
  • 复杂方法编写mapper.xml进行绑定映射
  • @MapperScan(“com.atguigu.admin.mapper”) 简化,其他的接口就可以不用标注@Mapper注解

5.1.4 整合 MyBatis-Plus

① 什么是MyBatis-Plus

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

mybatis plus 官网

建议安装 MybatisX 插件

② 整合MyBatis-Plus

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>

自动配置

  • MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。mybatis-plus:xxx 就是对****mybatis-plus的定制
  • SqlSessionFactory 自动配置好。底层是容器中默认的数据源
  • mapperLocations 自动配置好的。有默认值。***classpath*:/mapper/*/*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下
  • 容器中也自动配置好了 SqlSessionTemplate
  • @Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan(“com.atguigu.admin.mapper”) 批量扫描就行

优点:

  • 只需要我们的Mapper继承 BaseMapper 就可以拥有crud能力

③ CRUD功能

    @GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1")Integer pn,
RedirectAttributes ra){

userService.removeById(id);

ra.addAttribute("pn",pn);
return "redirect:/dynamic_table";
}


@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
//表格内容的遍历
// response.sendError
// List<User> users = Arrays.asList(new User("zhangsan", "123456"),
// new User("lisi", "123444"),
// new User("haha", "aaaaa"),
// new User("hehe ", "aaddd"));
// model.addAttribute("users",users);
//
// if(users.size()>3){
// throw new UserTooManyException();
// }
//从数据库中查出user表中的用户进行展示

//构造分页参数
Page<User> page = new Page<>(pn, 2);
//调用page进行分页
Page<User> userPage = userService.page(page, null);


// userPage.getRecords()
// userPage.getCurrent()
// userPage.getPages()


model.addAttribute("users",userPage);

return "table/dynamic_table";
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
}

@Repository
public interface UserService extends IService<User> {
}

④ 分页写法

  1. 分页插件

    @EnableTransactionManagement
    @Configuration
    @MapperScan("com.bamboo.warehouseerp.mapper")
    public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor addPaginationInnerInterceptor(){
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    //向Mybatis过滤器链中添加分页拦截器
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
    }
    }
  2. Mapper.java

    @Mapper
    @Repository
    public interface CustomerMapper extends BaseMapper<Customer> {
    IPage<Customer> selectPage(Page<Customer> pageParam,@Param("vo") CustomerVo customerVo);
    }
  3. Mapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.bamboo.warehouseerp.mapper.CustomerMapper">

    <resultMap id="CustomerMap" type="com.bamboo.warehouseerp.pojo.Customer" autoMapping="true"/>

    <sql id="columns">
    id,name,receipt_number,commodity_information,bill_date,operator,total_amount,state,create_time,update_time,is_deleted
    </sql>
    <select id="selectPage" resultMap="CustomerMap">
    select
    <include refid="columns"/>
    from customer
    <where>
    <if test="vo.receiptNumber != null and vo.receiptNumber != ''">
    and receipt_number like CONCAT('%',#{vo.receiptNumber},'%')
    </if>
    <if test="vo.commodityInformation != null and vo.commodityInformation != ''">
    and commodity_information like CONCAT('%',#{vo.commodityInformation},'%')
    </if>
    <if test="vo.oldDate != null and vo.oldDate != ''">
    and bill_date &gt;= #{vo.oldDate}
    </if>
    <if test="vo.newDate != null and vo.newDate != ''">
    and bill_date &lt;= #{vo.newDate}
    </if>
    and is_deleted = 0
    </where>
    order by id desc
    </select>
    </mapper>
  4. Service.java

    public interface CustomerService extends IService<Customer> {
    IPage<Customer> selectPage(Page<Customer> pageParam, CustomerVo customerVo);
    }
  5. ServiceImpl.java

    @Service
    public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements CustomerService {
    @Autowired
    private CustomerMapper customerMapper;
    @Override
    public IPage<Customer> selectPage(Page<Customer> pageParam, CustomerVo customerVo) {
    return customerMapper.selectPage(pageParam, customerVo);
    }
    }
  6. Controller

    @RestController
    @RequestMapping("/api/customer")
    public class CustomerController {
    @Autowired
    private CustomerService customerService;

    @GetMapping("{page}/{limit}")
    public Result getPageList(
    @PathVariable("page") Long page,
    @PathVariable("limit") Long limit,
    CustomerVo customerVo
    ) {
    Page<Customer> pageParam = new Page<>(page, limit);
    IPage<Customer> pageModel = customerService.selectPage(pageParam, customerVo);
    return Result.ok(pageModel);
    }

    }

5.2 NoSQL

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication)LUA脚本(Lua scripting)LRU驱动事件(LRU eviction)事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

5.2.1 Redis自动配置

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

自动配置:

  • RedisAutoConfiguration 自动配置类。RedisProperties 属性类 –> spring.redis.xxx是对redis的配置
  • 连接工厂是准备好的。LettuceConnectionConfiguration、JedisConnectionConfiguration
  • 自动注入了RedisTemplate<Object, Object> : xxxTemplate;
  • 自动注入了StringRedisTemplate;k:v都是String
  • key:value
  • 底层只要我们使用 StringRedisTemplate、RedisTemplate就可以操作redis

redis环境搭建

1、阿里云按量付费redis。经典网络

2、申请redis的公网连接地址

3、修改白名单 允许0.0.0.0/0 访问

5.2.2 RedisTemplate与Lettuce

@Test
void testRedis(){
ValueOperations<String, String> operations = redisTemplate.opsForValue();

operations.set("hello","world");

String hello = operations.get("hello");
System.out.println(hello);
}

5.2.3 切换至jedis

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

<!-- 导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring:
redis:
host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com
port: 6379
password: lfy:Lfy123456
client-type: jedis
jedis:
pool:
max-active: 10

六、单元测试

6.1 环境

兼容问题

SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)

JUnit 5’s Vintage Engine Removed from **spring-boot-starter-test,如果需要继续兼容junit4需要自行引入vintage**

<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>

依赖

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

使用

@SpringBootTest
class Boot05WebAdminApplicationTests {


@Test
void contextLoads() {

}
}

以前:

@SpringBootTest + @RunWith(SpringTest.class)

SpringBoot整合Junit以后。

  • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  • Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚

6.2 JUnit5常用注解

JUnit5的注解与JUnit4的注解有所变化

https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

  • **@Test :**表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • **@ParameterizedTest :**表示方法是参数化测试
  • **@RepeatedTest :**表示方法可重复执行
  • **@DisplayName :**为测试类或者测试方法设置展示名称
  • **@BeforeEach :**表示在每个单元测试之前执行
  • **@AfterEach :**表示在每个单元测试之后执行
  • **@BeforeAll :**表示在所有单元测试之前执行,需要标注static
  • **@AfterAll :**表示在所有单元测试之后执行,需要标注static
  • **@Tag :**表示单元测试类别,类似于JUnit4中的@Categories
  • **@Disabled :**表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
  • **@Timeout :**表示测试方法运行如果超过了指定时间将会返回错误
  • **@ExtendWith :**为测试类或测试方法提供扩展类引用

6.3 断言(assertions)

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:

检查业务逻辑返回的数据是否合理。

所有的测试运行结束以后,会有一个详细的测试报告;

  • 前面的断言失败情况下,后续所有代码及断言都不会执行
  • 导入静态包进行使用import static org.junit.jupiter.api.Assertions.*;

6.3.1 简单断言

用来对单个值进行简单的验证。如:

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
// 参数一:期望值
// 参数二:实际返回值
assertNotEquals(3, 1 + 1,"业务逻辑失败");

assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);

assertFalse(1 > 2);
assertTrue(1 < 2);

assertNull(null);
assertNotNull(new Object());
}

6.3.2 数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

6.3.3 组合断言

assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言

@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}

6.3.4 异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用**@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows()** ,配合函数式编程就可以进行使用。

@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));

}

6.3.5 超时断言

Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间

@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}

6.3.6 快速失败

通过 fail 方法直接使得测试失败

@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}

6.4 前置条件(assumptions)

JUnit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";

@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}

@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}

assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

6.5 嵌套测试

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。

@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}

@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());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}

6.6 参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

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

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

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

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

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

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

@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");
}

七、指标监控

未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。

7.1 pom导入依赖

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

HTTP访问:http://localhost/actuator/health

CMD访问:jconsole

7.2 暴露所有监控信息为HTTP

# management是所有actuator的配置
management:
endpoints:
enabled-by-default: true # 默认开启所有监控端点
web:
exposure:
include: '*' # 以web方式暴露所有端点
# management.endpoint.端点名.xxx 对某个端点的具体配置
endpoint:
health:
show-details: always

测试

7.3 Actuator Endpoint

最常使用的端点

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 执行线程转储。

如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:

ID 描述
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile 返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

7.3.1 Health Endpoint

健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

重要的几点:

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
  • 可以很容易的添加自定义的健康检查机制
// 20230823233557
// http://localhost/actuator/health

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

7.3.2 Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;

  • 通过Metrics对接多种监控系统
  • 简化核心Metrics开发
  • 添加自定义Metrics或者扩展已有Metrics
// 20230823233833
// http://localhost/actuator/metrics

{
"names": [
"application.ready.time",
"application.started.time",
"disk.free",
"disk.total",
"executor.active",
"executor.completed",
"executor.pool.core",
"executor.pool.max",
"executor.pool.size",
"executor.queue.remaining",
"executor.queued",
"http.server.requests",
"jvm.buffer.count",
"jvm.buffer.memory.used",
"jvm.buffer.total.capacity",
"jvm.classes.loaded",
"jvm.classes.unloaded",
"jvm.gc.live.data.size",
"jvm.gc.max.data.size",
"jvm.gc.memory.allocated",
"jvm.gc.memory.promoted",
"jvm.gc.overhead",
"jvm.memory.committed",
"jvm.memory.max",
"jvm.memory.usage.after.gc",
"jvm.memory.used",
"jvm.threads.daemon",
"jvm.threads.live",
"jvm.threads.peak",
"jvm.threads.states",
"logback.events",
"process.cpu.usage",
"process.start.time",
"process.uptime",
"system.cpu.count",
"system.cpu.usage",
"tomcat.sessions.active.current",
"tomcat.sessions.active.max",
"tomcat.sessions.alive.max",
"tomcat.sessions.created",
"tomcat.sessions.expired",
"tomcat.sessions.rejected"
]
}

7.3.3 管理Endpoints

① 开启与禁用Endpoints

  • 默认所有的Endpoint除过shutdown都是开启的。
  • 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint..enabled = true
management:
endpoint:
beans:
enabled: true
  • 或者禁用所有的Endpoint然后手动开启指定的Endpoint
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true

② 暴露Endpoints

支持的暴露方式

  • HTTP:默认只暴露healthinfo Endpoint
  • JMX:默认暴露所有Endpoint
  • 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
ID JMX Web
auditevents Yes No
beans Yes No
caches Yes No
conditions Yes No
configprops Yes No
env Yes No
flyway Yes No
health Yes Yes
heapdump N/A No
httptrace Yes No
info Yes Yes
integrationgraph Yes No
jolokia N/A No
logfile N/A No
loggers Yes No
liquibase Yes No
metrics Yes No
mappings Yes No
prometheus N/A No
scheduledtasks Yes No
sessions Yes No
shutdown Yes No
startup Yes No
threaddump Yes No

7.4 定制Endpoint

7.4.1 定制Health

  • 继承HealthIndicator的实现类AbstractHealthIndicator
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Map<String, Object> map = new HashMap<>();
// 检查完成
if (1 == 1) {
// builder.up();
builder.status(Status.UP);
map.put("count", 1);
map.put("ms", 100);
} else {
// builder.down();
builder.status(Status.DOWN);
map.put("err", "连接超时");
map.put("ms", 300);
}
builder.withDetail("code", 100)
.withDetails(map);
}
}

访问HTTP查看结果

// 20230824100256
// http://localhost/actuator/health

{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": {
"total": 1000080404480,
"free": 301467234304,
"threshold": 10485760,
"exists": true
}
},
"myCom": {
"status": "UP",
"details": {
"code": 100,
"ms": 100,
"count": 1
}
},
"ping": {
"status": "UP"
}
}
}

7.4.2 定制Info信息

② 编写配置文件

info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@

② 编写InfoContributor

import java.util.Collections;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;

@Component
public class ExampleInfoContributor implements InfoContributor {

@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}

}

http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息

7.4.3 定制Metrics信息

① SpringBoot支持自动适配的Metrics

  • JVM metrics, report utilization of:

    • Various memory and buffer pools
    • Statistics related to garbage collection
    • Threads utilization
    • Number of classes loaded/unloaded
  • CPU metrics

  • File descriptor metrics

  • Kafka consumer and producer metrics

  • Log4j2 metrics: record the number of events logged to Log4j2 at each level

  • Logback metrics: record the number of events logged to Logback at each level

  • Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time

  • Tomcat metrics (server.tomcat.mbeanregistry.enabled must be set to true for all Tomcat metrics to be registered)

  • Spring Integration metrics

② 增加定制Metrics

class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}

public void hello() {
counter.increment();
}
}


//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}

7.4.4 定制Endpoint

@Component
@Endpoint(id = "container")
public class DockerEndpoint {


@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}

@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}

}

场景:开发ReadinessEndpoint来管理程序是否就绪,或者Liveness****Endpoint来管理程序是否存活;

当然,这个也可以直接使用 https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes

八、高级特性

8.1 Profile功能

为了方便多环境适配,springboot简化了profile功能。

8.1.1 application-profile功能

  • 默认配置文件 application.yaml;任何时候都会加载

  • 指定环境配置文件 application-{env}.yaml

  • 激活指定环境

    • 配置文件激活
    • 命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
      • 修改配置文件的任意值,命令行优先
  • 默认配置与环境配置同时生效

  • 同名配置项,profile配置优先

# application.yml
spring:
profiles:
# 引用application-dev.yml
active: dev

8.1.2 @Profile条件装配功能

  • 指定某个环境下生效哪一个配置类,可以用于@Configuration(proxyBeanMethods = false)@ConfigurationProperties("person")
  • 也可以用于某一个@Bean
  • 可以标注在类和方法上
@Configuration(proxyBeanMethods = false)
@Profile("production")
public class ProductionConfiguration {

// ...

}

8.1.3 profile分组

spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq

使用:--spring.profiles.active=production 激活

8.2 外部化配置

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
  3. Config data (such as **application.properties** files)
  4. A RandomValuePropertySource that has properties only in random.*.
  5. OS environment variables.
  6. Java System properties (System.getProperties()).
  7. JNDI attributes from java:comp/env.
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. Command line arguments.
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.

8.2.1 外部配置源

常用:Java属性文件YAML文件环境变量命令行参数

8.2.2 配置文件查找位置

(1) classpath 根路径

(2) classpath 根路径下config目录

(3) jar包当前目录

(4) jar包当前目录【已经打包完成的项目XXX.jar】的config目录

(5) /config子目录的直接子目录【这里指的是linux绝对路径,在Windows不生效】

等级:(1) < (2) < (3) < (4)

8.2.3 配置文件加载顺序

  1.  当前jar包内部的application.properties和application.yml
  2.  当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
  3.  引用的外部jar包的application.properties和application.yml
  4.  引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
  • 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项

8.3 自定义Starter

8.3.1 Starter启动原理

  • starter-pom引入 autoconfigurer 包

    • starter => autoconfigure => spring-boot-starter
  • autoconfigure包中配置使用 META-INF/spring.factories 中 EnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类

  • 编写自动配置类 xxxAutoConfiguration -> xxxxProperties

    • @Configuration
    • @Conditional
    • @EnableConfigurationProperties
    • @Bean
    • ……

引入starter — xxxAutoConfiguration — 容器中放入组件 —- 绑定xxxProperties —- 配置项

8.3.2 自定义starter

① 创建项目

  1. 空项目:customer-starter
  2. Maven项目:bamboo-hello-spring-boot-starter
  3. SpringBoot空依赖项目:bamboo-hello-spring-boot-starter-autoconfigure

② 依赖关联

  1. 删除bamboo-hello-spring-boot-starter-autoconfigure项目中的无用文件

    • springboot启动器
    • application.yml
  2. 修改bamboo-hello-spring-boot-starter-autoconfigure项目的pom文件,只留下springboot-starter

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.15</version>
    <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bamboo</groupId>
    <artifactId>bamboo-hello-spring-boot-starter-autoconfigure</artifactId>
    <version>1.0.0</version>
    <name>bamboo-hello-spring-boot-starter-autoconfigure</name>
    <description>bamboo-hello-spring-boot-starter-autoconfigure</description>
    <properties>
    <java.version>1.8</java.version>
    </properties>
    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    </dependency>
    </dependencies>
    </project>

  3. 在bamboo-hello-spring-boot-starter的pom中导入bamboo-hello-spring-boot-starter-autoconfigure的坐标

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

    <groupId>com.bamboo</groupId>
    <artifactId>bamboo-hello-spring-boot-starter</artifactId>
    <version>1.0.0</version>

    <dependencies>
    <dependency>
    <groupId>com.bamboo</groupId>
    <artifactId>bamboo-hello-spring-boot-starter-autoconfigure</artifactId>
    <version>1.0.0</version>
    </dependency>
    </dependencies>
    </project>

③ 添加启动器功能

  1. 添加Bean

    package com.example.bamboo.hello.service;

    import com.example.bamboo.hello.bean.HelloProperties;
    import org.springframework.beans.factory.annotation.Autowired;

    /**
    * @version v1.0
    * @auther Bamboo
    * @create 2023/8/25 17:33
    * 默认不要放在容器中
    */
    public class HelloService {

    @Autowired
    private HelloProperties helloProperties;

    public String sayHello(String userName) {
    return helloProperties.getPrefix() + ": " + userName + ">" + helloProperties.getSuffix();
    }
    }

  2. 添加配置类

    package com.example.bamboo.hello.bean;
    import org.springframework.boot.context.properties.ConfigurationProperties;

    /**
    * @version v1.0
    * @auther Bamboo
    * @create 2023/8/25 17:35
    */

    @ConfigurationProperties("bamboo.hello")
    // 没有进行标注@Componet时,通过配置类的@EnableConfigurationProperties进行注册到容器中
    public class HelloProperties {
    private String prefix;
    private String suffix;

    public String getPrefix() {
    return prefix;
    }

    public void setPrefix(String prefix) {
    this.prefix = prefix;
    }

    public String getSuffix() {
    return suffix;
    }

    public void setSuffix(String suffix) {
    this.suffix = suffix;
    }
    }
  3. 添加自动配置类

    package com.example.bamboo.hello.auto;
    import com.example.bamboo.hello.bean.HelloProperties;
    import com.example.bamboo.hello.service.HelloService;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    /**
    * @version v1.0
    * @auther Bamboo
    * @create 2023/8/25 17:38
    */
    @Configuration
    @EnableConfigurationProperties(HelloProperties.class)
    // 将HelloProperties.class默认放到容器中
    public class HelloServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(HelloService.class)
    // 当容器中不存在HelloService的Bean时进行注册
    public HelloService helloService() {
    return new HelloService();
    }
    }

④ 配置自动加载文件

  • 原2.4位置是:spring-boot-autoconfigure\2.7.15\spring-boot-autoconfigure-2.7.15.jar!\META-INF\spring.factories,现已无法确认,寻求其他自定义启动器
  • 在mybatis启动器中查询到需配置以下参数
  1. 在resources目录添加META-INF/spring.factories文件

  2. 添加以下代码

    # Auto Configuration
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.example.bamboo.hello.auto.HelloServiceAutoConfiguration

⑤ 安装starter

  • mvn的clear+install
  • 优先安装autoconfigure的项目【因为starter依赖于它】
  • 其次安装自定义的starter项目

⑥ 测试

  1. 创建一个只包含web的SpringBoot项目

  2. 引入自定义Starter

    <dependency>
    <groupId>com.bamboo</groupId>
    <artifactId>bamboo-hello-spring-boot-starter</artifactId>
    <version>1.0.0</version>
    </dependency>
  3. 修改yml配置文件

    bamboo:
    hello:
    prefix: 欢迎您
    suffix: 同志
  4. 编写Controller

    @RestController
    public class HelloController {

    @Autowired
    private HelloService helloService;

    @GetMapping("/hello")
    public String getHelloService() {
    return helloService.sayHello("张三");
    }
    }