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 ); } 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 ); } 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) { } 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 { private String adminSecretKey; private long adminTtl; private String adminTokenName; private String userSecretKey; private long userTtl; private String userTokenName; }
② JWTUtil
public class JwtUtil { public static String createJWT (String secretKey, long ttlMillis, Map<String, Object> claims) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long expMillis = System.currentTimeMillis() + ttlMillis; Date exp = new Date (expMillis); JwtBuilder builder = Jwts.builder() .setClaims(claims) .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8)) .setExpiration(exp); return builder.compact(); } public static Claims parseJWT (String secretKey, String token) { Claims claims = Jwts.parser() .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) .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; public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("当前线程的ID是:" +Thread.currentThread().getId()); if (!(handler instanceof HandlerMethod)) { return true ; } String token = request.getHeader(jwtProperties.getUserTokenName()); try { log.info("jwt校验:{}" , token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long empId = Long.valueOf(claims.get("userId" ).toString()); log.info("当前用户的id:" , empId); BaseContext.setCurrentId(empId); return true ; } catch (Exception ex) { 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; @PostMapping("/login") @ApiOperation("微信登录") public Result<UserLoginVO> login (@RequestBody UserLoginDTO userLoginDTO) { log.info("微信用户登录:{}" , userLoginDTO.getCode()); User user = userService.wxLogin(userLoginDTO); 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
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
@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: 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依赖
<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 欢迎页支持
3.1.5 默认访问静态路径修改 【URL地址访问的静态资源】
spring: mvc: static-path-pattern: /res/**
3.1.6 禁用静态资源配置 spring: web: resources: add-mappings: false 禁用所有静态资源规则
3.2 请求参数处理
3.2.1 手动开启Rest功能【表单提交】 ① yml配置 spring: mvc: # 开启Rest功能 hiddenmethod: filter: enabled: true
form表单
<form action ="/user" method ="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) 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 { @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)获取结果
{ "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" > 邮箱 :<input type ="text" name ="email" id ="email" > <input type ="submit" value ="submit" > </form >
3)获取结果
{ "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)响应
{ "reqMethod_msg" : "success..." , "annotation_msg" : "success..." }
3)解析
使用HttpServletRequest请求可以将属性放入request请求域中
@RequestAttribute
与HttpServletRequest.getAttribute功能类似,都可以获取request域中保存的值
④ 矩阵变量@MatrixVariable 1)解除矩阵变量限制
使用implements WebMvcConfigurer的方式
@Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Override public void configurePathMatch (PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper (); urlPathHelper.setRemoveSemicolonContent(false ); configurer.setUrlPathHelper(urlPathHelper); } }
使用@Bean的方式
@Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer () { @Override public void configurePathMatch (PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper (); urlPathHelper.setRemoveSemicolonContent(false ); configurer.setUrlPathHelper(urlPathHelper); } }; } }
2)Controller
@RestController public class ParameterController { @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)结果
{ "path" : "sell" , "low" : 34 , "brand" : [ "byd" , "audi" , "yd" ] }
5)特殊情况
@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 复杂类型 Map 、Model(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 自定义对象参数 可以自动类型转换与格式化,可以级联封装。
@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 自定义配置 案例:页面提交
<input type ="text" name ="pet" value ="阿猫,6" />
代码【也可采用实现的方式重写方法】:
@Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer () { @Override public void addFormatters (FormatterRegistry registry) { registry.addConverter(new Converter <String, Pet>() { @Override public Pet convert (String source) { 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 ; } }); } }; } }
返回结果
{ "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 { protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html" ) .addResourceLocations("classpath:/META-INF/resources/" ); registry.addResourceHandler("/webjars/**" ) .addResourceLocations("classpath:/META-INF/resources/webjars/" ); } @Override protected void extendMessageConverters (List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..." ); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter (); converter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 ,converter); } }
3.3.3 自定义 MessageConverters 内容协商实现JSON转换 @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Override protected void extendMessageConverters (List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..." ); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter (); converter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 ,converter); } }
3.3.4 自定义 Interceptors 拦截器 @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; @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) { 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()); } }
@Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public WebMvcConfigurer webMvcConfigurer () { return new WebMvcConfigurer () { @Override public void extendMessageConverters (List<HttpMessageConverter<?>> converters) { converters.add(new BambooMessageConverter ()); } }; } }
③ 添加配置实现自定义参数内容协商 @Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter () { HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter (); methodFilter.setMethodParam("_m" ); return methodFilter; } @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); 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来实现路径统一存放
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进行注入 ServletRegistrationBean
、FilterRegistrationBean
、ServletListenerRegistrationBean
去掉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 (); 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 {}
3.12.2 原理分析套路 场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项
四、整合第三方技术 4.1 JUnit springboot创建时默认添加test测试功能
@SpringBootTest => class
@Test => method
一、创建测试对象
public interface BookDao { void say () ; } @Repository public class BookDaoImpl implements BookDao { @Override public void say () { System.out.println("bookdao run..." ); } }
二、**@SpringBootTest测试**
@SpringBootTest class Module0615ApplicationTests { @Autowired private BookDao bookDao; @Test void contextLoads () { 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 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 { @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
@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使用
导入spring data redis的maven坐标
<dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
配置redis数据源
spring: redis: host: localhost port: 6379 password: 123456 database: 10
编写配置类,创建RedisTemplate对象
@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate (RedisConnectionFactory redisConnectionFactory) { log.info("开始创建redis模板对象..." ); RedisTemplate redisTemplate = new RedisTemplate (); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer ()); return redisTemplate; } }
通过RedisTemplate对象操作Redis
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;@SpringBootTest public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedisTemplate () { System.out.println(redisTemplate); ValueOperations valueOperations = redisTemplate.opsForValue(); HashOperations hashOperations = redisTemplate.opsForHash(); ListOperations listOperations = redisTemplate.opsForList(); SetOperations setOperations = redisTemplate.opsForSet(); ZSetOperations zSetOperations = redisTemplate.opsForZSet(); } @Test public void testString () { ValueOperations valueOperations = redisTemplate.opsForValue(); 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 () { 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 () { 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 () { 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 () { 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 () { 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 > </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 () { 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
整合第三方技术的两种方式
② 自定义方式 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" />
推荐方式
导入druid依赖
<dependency > <groupId > com.alibaba</groupId > <artifactId > druid</artifactId > <version > 1.1.17</version > </dependency >
编写配置类
@Configuration public class MyDataSourceConfig { @Bean @ConfigurationProperties("spring.datasource") public DataSource dataSource () { DruidDataSource dataSource = new DruidDataSource (); return dataSource; } }
测试
@Slf4j @SpringBootTest class MainApplicationTests { @Autowired private DataSource dataSource; @Test void contextLoads () { log.info("datasource类型是:" +dataSource.getClass()); } }
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; } @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.* filters: stat,wall stat-view-servlet: enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: 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: config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mapper/*.xml 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: 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) { Page<User> page = new Page <>(pn, 2 ); Page<User> userPage = userService.page(page, null ); 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> {}
④ 分页写法
分页插件
@EnableTransactionManagement @Configuration @MapperScan("com.bamboo.warehouseerp.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor addPaginationInnerInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); return interceptor; } }
Mapper.java
@Mapper @Repository public interface CustomerMapper extends BaseMapper <Customer> { IPage<Customer> selectPage (Page<Customer> pageParam,@Param("vo") CustomerVo customerVo) ; }
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 > = #{vo.oldDate} </if > <if test ="vo.newDate != null and vo.newDate != ''" > and bill_date < = #{vo.newDate} </if > and is_deleted = 0 </where > order by id desc </select > </mapper >
Service.java
public interface CustomerService extends IService <Customer> { IPage<Customer> selectPage (Page<Customer> pageParam, CustomerVo customerVo) ; }
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); } }
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) 与范围查询, bitmaps , hyperloglogs 和 地理空间(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 > <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 () { 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: endpoints: enabled-by-default: true web: exposure: include: '*' 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.name
或logging.file.path
属性)。支持使用HTTPRange
标头来检索部分日志文件的内容。
prometheus
以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus
。
最常用的Endpoint
Health:监控状况
Metrics:运行时指标
Loggers:日志记录
7.3.1 Health Endpoint 健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
很多的健康检查默认已经自动配置好了,比如:数据库、redis等
可以很容易的添加自定义的健康检查机制
{ "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
{ "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:默认只暴露health 和info 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.status(Status.UP); map.put("count" , 1 ); map.put("ms" , 100 ); } else { builder.status(Status.DOWN); map.put("err" , "连接超时" ); map.put("ms" , 300 ); } builder.withDetail("code" , 100 ) .withDetails(map); } }
访问HTTP查看结果
{ "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@ 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功能
spring: profiles: 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
Default properties (specified by setting SpringApplication.setDefaultProperties
).
@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.
Config data (such as **application.properties**
files)
A RandomValuePropertySource
that has properties only in random.*
.
OS environment variables.
Java System properties (System.getProperties()
).
JNDI attributes from java:comp/env
.
ServletContext
init parameters.
ServletConfig
init parameters.
Properties from SPRING_APPLICATION_JSON
(inline JSON embedded in an environment variable or system property).
Command line arguments.
properties
attribute on your tests. Available on @SpringBootTest
and the test annotations for testing a particular slice of your application .
@TestPropertySource
annotations on your tests.
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 配置文件加载顺序
当前jar包内部的application.properties和application.yml
当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
引用的外部jar包的application.properties和application.yml
引用的外部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 ① 创建项目
空项目:customer-starter
Maven项目:bamboo-hello-spring-boot-starter
SpringBoot空依赖项目:bamboo-hello-spring-boot-starter-autoconfigure
② 依赖关联
删除bamboo-hello-spring-boot-starter-autoconfigure项目中的无用文件
springboot启动器
application.yml
修改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 /> </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 >
在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 >
③ 添加启动器功能
添加Bean
package com.example.bamboo.hello.service;import com.example.bamboo.hello.bean.HelloProperties;import org.springframework.beans.factory.annotation.Autowired;public class HelloService { @Autowired private HelloProperties helloProperties; public String sayHello (String userName) { return helloProperties.getPrefix() + ": " + userName + ">" + helloProperties.getSuffix(); } }
添加配置类
package com.example.bamboo.hello.bean;import org.springframework.boot.context.properties.ConfigurationProperties;@ConfigurationProperties("bamboo.hello") 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; } }
添加自动配置类
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;@Configuration @EnableConfigurationProperties(HelloProperties.class) public class HelloServiceAutoConfiguration { @Bean @ConditionalOnMissingBean(HelloService.class) 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启动器中查询到需配置以下参数
在resources目录添加META-INF/spring.factories
文件
添加以下代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration =\ com.example.bamboo.hello.auto.HelloServiceAutoConfiguration
⑤ 安装starter
mvn的clear+install
优先安装autoconfigure的项目【因为starter依赖于它】
其次安装自定义的starter项目
⑥ 测试
创建一个只包含web的SpringBoot项目
引入自定义Starter
<dependency > <groupId > com.bamboo</groupId > <artifactId > bamboo-hello-spring-boot-starter</artifactId > <version > 1.0.0</version > </dependency >
修改yml配置文件
bamboo: hello: prefix: 欢迎您 suffix: 同志
编写Controller
@RestController public class HelloController { @Autowired private HelloService helloService; @GetMapping("/hello") public String getHelloService () { return helloService.sayHello("张三" ); } }