学习笔记"/>
SpringBoot2学习笔记
45、web实验-抽取公共页面
官方文档 - Template Layout
- 公共页面
/templates/common.html
<!DOCTYPE html>
<html lang="en" xmlns:th=""><!--注意要添加xmlns:th才能添加thymeleaf的标签-->
<head th:fragment="commonheader"><!--common--> <link href="css/style.css" th:href="@{/css/style.css}" rel="stylesheet"><link href="css/style-responsive.css" th:href="@{/css/style-responsive.css}" rel="stylesheet">...
</head>
<body>
<!-- left side start-->
<div id="leftmenu" class="left-side sticky-left-side">...<div class="left-side-inner">...<!--sidebar nav start--><ul class="nav nav-pills nav-stacked custom-nav"><li><a th:href="@{/main.html}"><i class="fa fa-home"></i> <span>Dashboard</span></a></li>...<li class="menu-list nav-active"><a href="#"><i class="fa fa-th-list"></i> <span>Data Tables</span></a><ul class="sub-menu-list"><li><a th:href="@{/basic_table}"> Basic Table</a></li><li><a th:href="@{/dynamic_table}"> Advanced Table</a></li><li><a th:href="@{/responsive_table}"> Responsive Table</a></li><li><a th:href="@{/editable_table}"> Edit Table</a></li></ul></li>...</ul><!--sidebar nav end--></div>
</div>
<!-- left side end--><!-- header section start-->
<div th:fragment="headermenu" class="header-section"><!--toggle button start--><a class="toggle-btn"><i class="fa fa-bars"></i></a><!--toggle button end-->...
</div>
<!-- header section end--><div id="commonscript"><!-- Placed js at the end of the document so the pages load faster --><script th:src="@{/js/jquery-1.10.2.min.js}"></script><script th:src="@{/js/jquery-ui-1.9.2.custom.min.js}"></script><script th:src="@{/js/jquery-migrate-1.2.1.min.js}"></script><script th:src="@{/js/bootstrap.min.js}"></script><script th:src="@{/js/modernizr.min.js}"></script><script th:src="@{/js/jquery.nicescroll.js}"></script><!--common scripts for all pages--><script th:src="@{/js/scripts.js}"></script>
</div>
</body>
</html>
使用th:href/src={xxx}可以,替换原来的路径,以相对路径拿到静态资源,比如项目本身添加了路径前缀,这里就可以动态改变
声明公共片段标签:th:fragment="demo1"
引用公共片段标签:
th:insert="~{common::demo1}"
th:insert="common::demo1"
th:insert="~{common::#id}"
- 同时给引用片段的 关键字有
- insert 直接将片段引入, d i v 标签中 直接将片段引入,div标签中 直接将片段引入,div标签中
- replace 将片段引入,替换 d i v 标签 将片段引入,替换div标签 将片段引入,替换div标签
- include 把片段标签里面的内容,直接放在 d i v 下 把片段标签里面的内容,直接放在div下 把片段标签里面的内容,直接放在div下
/templates/table/basic_table.html
<!DOCTYPE html>
<html lang="en" xmlns:th="">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"><meta name="description" content=""><meta name="author" content="ThemeBucket"><link rel="shortcut icon" href="#" type="image/png"><title>Basic Table</title><div th:include="common :: commonheader"> </div><!--将common.html的代码段 插进来-->
</head><body class="sticky-header"><section>
<div th:replace="common :: #leftmenu"></div><!-- main content start--><div class="main-content" ><div th:replace="common :: headermenu"></div>...</div><!-- main content end-->
</section><!-- Placed js at the end of the document so the pages load faster -->
<div th:replace="common :: #commonscript"></div></body>
</html>
46、web实验-遍历数据与页面bug修改
控制层代码:
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){//表格内容的遍历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);return "table/dynamic_table";
}
页面代码:
<table class="display table table-bordered" id="hidden-table-info"><thead><tr><th>#</th><th>用户名</th><th>密码</th></tr></thead><tbody><tr class="gradeX" th:each="user,stats:${users}"><td th:text="${stats.count}">Trident</td><td th:text="${user.userName}">Internet</td><td >[[${user.password}]]</td></tr></tbody>
</table>
内置对象 stats使用,一般用于拿到后端的数据进行遍历
47、视图解析-【源码分析】-视图解析器与视图
视图解析原理流程:
-
目标方法处理的过程中(阅读**
DispatcherServlet
** 源码 the most important is// Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // step into 到 requestMappingHandlerAdapter
),如果是
/login
请求,需要能处理它的处理器 V i e w N a m e M e t h o d R e t u r n V a l u e H a n d l e r ViewNameMethodReturnValueHandler ViewNameMethodReturnValueHandler –> 判断当前请求没有返回值或者返回值为字符串就选择这个,不过它在处理器中属于靠后位置的
,所有数据都会被放在ModelAndViewContainer
里面,其中包括数据和视图地址。 -
方法的参数是一个自定义类型对象【user】(从请求参数中确定的),把他也放在
ModelAndViewContainer
。 -
任何目标方法执行完成以后都会返回
ModelAndView
(数据和视图地址), 使用反射机制mav = invokeHandlerMethod(request, response, handlerMethod)
执行的目标方法。 -
private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {if (mv != null && !mv.hasView()) { // dispatchServlet 中检查mv是否有viewName,否这里设定一个默认值String defaultViewName = getDefaultViewName(request);if (defaultViewName != null) {mv.setViewName(defaultViewName);}}}
-
processDispatchResult()
处理派发结果(页面如何响应)
-
render(mv, request, response);
进行页面渲染逻辑 -
根据方法的
String
返回值得到View
对象【定义了页面的渲染逻辑】- 所有的**视图解析器(ViewResolver)**尝试是否能根据当前返回值得到
View
对象 - 得到了
redirect:/main.html --> Thymeleaf 创建了 RedirectView()
。
- 所有的**视图解析器(ViewResolver)**尝试是否能根据当前返回值得到
-
ContentNegotiationViewResolver
里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。 -
视图对象view调用自己的render进行页面渲染工作 —>
view.render(mv.getModelInternal(), request, response);
RedirectView
如何渲染【重定向到一个页面】- 获取目标url地址
response.sendRedirect(encodedURL);
跳到对应视图
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {boolean errorView = false;if (exception != null) {if (exception instanceof ModelAndViewDefiningException) {logger.debug("ModelAndViewDefiningException encountered", exception);mv = ((ModelAndViewDefiningException) exception).getModelAndView();}else {Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);mv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}// Did the handler return a view to render?if (mv != null && !mv.wasCleared()) {render(mv, request, response); //页面渲染,主要的方法是---> renderMergedOutputModelif (errorView) {WebUtils.clearErrorRequestAttributes(request);}}else {if (logger.isTraceEnabled()) {logger.trace("No view rendering, null ModelAndView returned.");}}if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {// Concurrent handling started during a forwardreturn;}if (mappedHandler != null) {// Exception (if any) is already handled..mappedHandler.triggerAfterCompletion(request, response, null);}}
AbstractView中的render方法逻辑
@Overridepublic void render(@Nullable Map<String, ?> model, HttpServletRequest request,HttpServletResponse response) throws Exception {if (logger.isDebugEnabled()) {logger.debug("View " + formatViewName() +", model " + (model != null ? model : Collections.emptyMap()) +(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));}Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);prepareResponse(request, response);renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); // <----重点
}
renderMergedOutputModel方法,会根据当前方法返回值得到的类型视图类 RedirectView,调用对应的renderMergedOutputModel的方法
视图解析:
- 返回值以
forward:
开始:new InternalResourceView(forwardUrl);
--> 转发request.getRequestDispatcher(path).forward(request, response);
- 返回值以
redirect:
开始:new RedirectView()
-->render
就是重定向 - 返回值是普通字符串:
new ThymeleafView()
—> 模板引擎调用process方法,以IO流写出
自定义视图+自定义解析器可以返回你想要的更多视图:如excel,word等等。
重定向携带数据
可以将当前页面的参数通过 RedirectAttributes 传输到重定向的页面,即在重定向的页面中可以拿到数据
阅读源码:最好自己在IDE,打断点,且Debug模式运行实例,这样比较没那么沉闷。
48、拦截器-登录检查与静态资源放行
小提示:如果同时设置了 监听器,过滤器,拦截器他们的启动生效顺序是:监听器-->过滤器-->拦截器
-
编写一个拦截器实现
HandlerInterceptor
接口 -
拦截器注册到容器中(实现
WebMvcConfigurer
的**addInterceptors()
**) -
指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】
编写一个实现HandlerInterceptor
接口的拦截器:
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {/*** 目标方法执行之前 ---> 一般用于登录拦截,身份验证*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String requestURI = request.getRequestURI();log.info("preHandle拦截的请求路径是{}",requestURI);//登录检查逻辑HttpSession session = request.getSession();Object loginUser = session.getAttribute("loginUser");if(loginUser != null){//放行return true;}//拦截住。未登录。跳转到登录页request.setAttribute("msg","请先登录");
// response.sendRedirect("/");request.getRequestDispatcher("/").forward(request,response); // 能访问到msg信息return false;}/*** 目标方法执行完成以后 --*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("postHandle执行{}",modelAndView);}/*** 页面渲染以后*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("afterCompletion执行异常{}",ex);}
}
拦截器注册到容器中 && 指定拦截规则:
// 配置好拦截器如何拦截之后,需要在这里注册拦截器,让他生效(以前springmvc是在配置文件中写),拦截那些请求,放行那些请求
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor())//拦截器注册到容器中.addPathPatterns("/**") //所有请求都被拦截包括静态资源.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**","/aa/**"); //放行的请求
}
配置静态的资源的访问前路径(前缀)
spring:mvc:static-path-pattern: /static/** # 指定静态资源前都要加一个 /static (拦截器放行静态资源路径,可以只写一个 /static/**)
49、拦截器-【源码分析】-拦截器的执行时机和原理
拦截器底层生效的流程顺序
- 根据当前请求,找到
HandlerExecutionChain
处理器链 - 先来顺序执行 所有拦截器的
preHandle()
方法。
- 如果当前拦截器
preHandle()
返回为true
。则执行下一个拦截器的preHandle()
- 如果当前拦截器返回为
false
。直接倒序执行所有已经执行了的拦截器的afterCompletion();
-
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for (int i = 0; i < this.interceptorList.size(); i++) {HandlerInterceptor interceptor = this.interceptorList.get(i);if (!interceptor.preHandle(request, response, this.handler)) { // 如果出现了一个为falsetriggerAfterCompletion(request, response, null);//倒序执行已经执行过preHandle方法的拦截的afterCompletion方法return false;}this.interceptorIndex = i;}return true;}
- 如果任何一个拦截器返回
false
,直接跳出不执行目标方法。 - 所有拦截器都返回
true
,才执行目标方法。 - 然后倒序执行所有拦截器的
postHandle()
方法。 - 前面的步骤有任何异常都会直接倒序触发
afterCompletion()
。 - 页面成功渲染完成以后,也会倒序触发
afterCompletion()
。 - 已经执行的preHandle的拦截器的afterCompletion 必执行,可以和finally挂钩,放在finally代码块中
DispatcherServlet
中涉及到HandlerInterceptor
的地方:
public class DispatcherServlet extends FrameworkServlet {...protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null; //HandlerExecutionChain处理器链主要执行拦截器的三个方法boolean multipartRequestParsed = false;// 异步管理器WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;...1.//该方法内调用HandlerInterceptor的preHandle() (实现了,目标方法执行之前先进行拦截器操作,来判断是否登录)if (!mappedHandler.applyPreHandle(processedRequest, response)) {return; // 任意一个拦截器返回为false,直接返回了}// Actually invoke the handler. --- 执行目标方法mv = ha.handle(processedRequest, response, mappedHandler.getHandler());...2.//该方法内调用HandlerInterceptor的postHandle(),只有所有拦截器都成功了mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {// As of 4.3, we're processing Errors thrown from handler methods as well,// making them available for @ExceptionHandler methods and other scenarios.dispatchException = new NestedServletException("Handler dispatch failed", err);}3.//该方法内调用HandlerInterceptor接口的afterCompletion方法processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {3.1//该方法内调用HandlerInterceptor接口的afterCompletion方法triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {3.2//该方法内调用HandlerInterceptor接口的afterCompletion方法triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));}finally {// 如果拦截器返回false,方法提前return,finally中执行afterCompletion...}}private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {if (mappedHandler != null) {//该方法内调用HandlerInterceptor接口的afterCompletion方法mappedHandler.triggerAfterCompletion(request, response, ex);}throw ex;}private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {...if (mappedHandler != null) {//该方法内调用HandlerInterceptor接口的afterCompletion方法// Exception (if any) is already handled..mappedHandler.triggerAfterCompletion(request, response, null);}}
}
HandlerExecutionChain 处理执行链
public class HandlerExecutionChain {...boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {for (int i = 0; i < this.interceptorList.size(); i++) { //--顺序--遍历所有的拦截器HandlerInterceptor interceptor = this.interceptorList.get(i);//HandlerInterceptor的 preHandle方法if (!interceptor.preHandle(request, response, this.handler)) {triggerAfterCompletion(request, response, null);return false;}this.interceptorIndex = i;}return true;}void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)throws Exception {for (int i = this.interceptorList.size() - 1; i >= 0; i--) { //--倒序--遍历所有拦截器HandlerInterceptor interceptor = this.interceptorList.get(i);//HandlerInterceptor接口的postHandle方法interceptor.postHandle(request, response, this.handler, mv);}}void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {for (int i = this.interceptorIndex; i >= 0; i--) { //--倒序-- 遍历已经执行过prehandle方法的拦截器HandlerInterceptor interceptor = this.interceptorList.get(i);try {//HandlerInterceptor接口的afterCompletion方法interceptor.afterCompletion(request, response, this.handler, ex);}catch (Throwable ex2) {logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);}}}void applyAfterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response) {xxx//倒序执行拦截器afterxxx }
}
TODO
:过滤器和监听器,maybe can DEBUG一下
50、文件上传-单文件与多文件上传的使用
- 页面代码
/static/form/form_layouts.html
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data"><div class="form-group"><label for="exampleInputEmail1">邮箱</label><input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email"></div><div class="form-group"><label for="exampleInputPassword1">名字</label><input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password"></div><div class="form-group"><label for="exampleInputFile">头像</label><!--单文件上传--><input type="file" name="headerImg" id="exampleInputFile"></div><div class="form-group"><label for="exampleInputFile">生活照</label><!---多文件上传--><input type="file" name="photos" multiple></div><div class="checkbox"><label><input type="checkbox"> Check me out</label></div><button type="submit" class="btn btn-primary">提交</button>
</form>
文件上传:type=file
;多文件上传:给input标签添加 multiple 属性
- 控制层代码
@Slf4j
@Controller
public class FormTestController {@GetMapping("/form_layouts")public String form_layouts(){return "form/form_layouts";}@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={},headerImgSize={},photosLength={}",email,username,headerImg.getSize(),photos.length);if(!headerImg.isEmpty()){//保存到文件服务器,OSS服务器String originalFilename = headerImg.getOriginalFilename();// <-- 拿到原始文件名headerImg.transferTo(new File("H:\\cache\\"+originalFilename)); //--->transferTo方法(把当前文件传输入到指定位置(流的方式))}if(photos.length > 0){for (MultipartFile photo : photos) {if(!photo.isEmpty()){String originalFilename = photo.getOriginalFilename();photo.transferTo(new File("H:\\cache\\"+originalFilename));}}}return "main";}
}
文件上传接收对应的参数,使用的注解是@RequestPart
文件上传相关的配置类:
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartProperties
文件大小相关配置项:
如果使用多文件上传,有可能文件总大小会超过默认允许的大小,可以在配置文件中修改
spring.servlet.multipart.max-file-size=10MB # 默认1MB
spring.servlet.multipart.max-request-size=100MB # 默认10MB
51、文件上传-【源码流程】文件上传参数解析器
- 文件上传相关的自动配置类
MultipartAutoConfiguration
- 自动配置类中有创建文件上传参数解析器
StandardServletMultipartResolver
。
原理步骤
-
请求进来使用文件上传解析器判断 isMultipart并封装 resolveMultipart,返回 MultipartHttpServletRequest (文件上传请求)
-
参数解析器来解析请求中的文件内容封装成 MultipartFiles
-
将request请求中的信息,封装成一个 LinkedMultiValueMap<String,MultipartFile> 的map集合
-
FileCopyUtils用来实现文件的流传输
MultipartAutoConfiguration 文件自动配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {private final MultipartProperties multipartProperties;public MultipartAutoConfiguration(MultipartProperties multipartProperties) {this.multipartProperties = multipartProperties;}@Bean@ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })public MultipartConfigElement multipartConfigElement() {return this.multipartProperties.createMultipartConfig();}@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)@ConditionalOnMissingBean(MultipartResolver.class)public StandardServletMultipartResolver multipartResolver() {//配置好文件上传解析器StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());return multipartResolver;}}
StandardServletMultipartResolver 文件上传参数解析器
//文件上传解析器
public class StandardServletMultipartResolver implements MultipartResolver {private boolean resolveLazily = false;public void setResolveLazily(boolean resolveLazily) {this.resolveLazily = resolveLazily;}@Overridepublic boolean isMultipart(HttpServletRequest request) {return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");}@Overridepublic MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {return new StandardMultipartHttpServletRequest(request, this.resolveLazily);}@Overridepublic void cleanupMultipart(MultipartHttpServletRequest request) {if (!(request instanceof AbstractMultipartHttpServletRequest) ||((AbstractMultipartHttpServletRequest) request).isResolved()) {// To be on the safe side: explicitly delete the parts,// but only actual file parts (for Resin compatibility)try {for (Part part : request.getParts()) {if (request.getFile(part.getName()) != null) {part.delete();}}}catch (Throwable ex) {LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);}}}}
前端控制器 DispatcherServlet
checkMultipart(request); 使用了文件上传解析器的方法,判断当期请求是否有文件上传请求。
如果有,就用processedRproequest替换原来的 request(即为增强request),执行后面的方法
public class DispatcherServlet extends FrameworkServlet {@Nullableprivate MultipartResolver multipartResolver;private void initMultipartResolver(ApplicationContext context) {...//这个就是配置类配置的StandardServletMultipartResolver文件上传解析器this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);...}protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRproequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;//最后finally的回收flag...try {ModelAndView mv = null;Exception dispatchException = null;try {//做预处理,如果有上传文件 就new StandardMultipartHttpServletRequest包装类/*重新添加了一个httpServlet,包装原来的request,新增multipartNames和Files--->*/ processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); //判断是否有文件上传请求// Determine handler for the current request.mappedHandler = getHandler(processedRequest);...// Determine handler adapter for the current request.HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());...// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());}...finally {... if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { //是不是文件上传请求...return this.multipartResolver.resolveMultipart(request);...}}protected void cleanupMultipart(HttpServletRequest request) {if (this.multipartResolver != null) {MultipartHttpServletRequest multipartRequest =WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);if (multipartRequest != null) {this.multipartResolver.cleanupMultipart(multipartRequest);}}}
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
跳到以下的类
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapterimplements BeanFactoryAware, InitializingBean {@Overrideprotected ModelAndView handleInternal(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ModelAndView mav;...// 执行目标方法mav = invokeHandlerMethod(request, response, handlerMethod);...return mav;}@Nullableprotected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {ServletWebRequest webRequest = new ServletWebRequest(request, response);try {WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);if (this.argumentResolvers != null) {//关注点 ---> 参数解析器invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);}...invocableMethod.invokeAndHandle(webRequest, mavContainer);...return getModelAndView(mavContainer, modelFactory, webRequest);}finally {webRequest.requestCompleted();}}}
this.argumentResolvers
其中主角类RequestPartMethodArgumentResolver
用来生成
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {...public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);...}@Nullablepublic Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {//方法2Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);...//方法1return doInvoke(args);//反射调用}//方法1@Nullableprotected Object doInvoke(Object... args) throws Exception {Method method = getBridgedMethod();ReflectionUtils.makeAccessible(method);return method.invoke(getBean(), args);...}//方法2//处理得出multipart参数,准备稍后的反射调用(@PostMapping标记的上传方法)protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {MethodParameter[] parameters = getMethodParameters();...Object[] args = new Object[parameters.length];for (int i = 0; i < parameters.length; i++) {MethodParameter parameter = parameters[i];parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);args[i] = findProvidedArgument(parameter, providedArgs);if (args[i] != null) {continue;}//关注点1if (!this.resolvers.supportsParameter(parameter)) {throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));}try {//关注点2args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);}catch (Exception ex) {...}}return args;}}
RequestPartMethodArgumentResolver 接收文件上传参数的解析器
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {//对应上面代码关注点1@Overridepublic boolean supportsParameter(MethodParameter parameter) {//标注@RequestPart的参数 就可以由当前解析器处理(被检测)if (parameter.hasParameterAnnotation(RequestPart.class)) {return true;}else {if (parameter.hasParameterAnnotation(RequestParam.class)) { // 用的requestParam就失效return false;}return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional()); // 不用注解}}// 不用注解调用的判断方法 --判断参数类型(来自MultipartResolutionDelegate类)public static boolean isMultipartArgument(MethodParameter parameter) {Class<?> paramType = parameter.getNestedParameterType();return MultipartFile.class == paramType || isMultipartFileCollection(parameter) || isMultipartFileArray(parameter) || Part.class == paramType || isPartCollection(parameter) || isPartArray(parameter);}//对应上面代码关注点2@Override@Nullablepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);Assert.state(servletRequest != null, "No HttpServletRequest");RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional());String name = getPartName(parameter, requestPart);parameter = parameter.nestedIfOptional();Object arg = null;//封装成MultipartFile 类型的对象作参数Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {arg = mpArg;}...return adaptArgumentIfNecessary(arg, parameter);}
}
52、错误处理-SpringBoot默认错误处理机制
Spring Boot官方文档 - Error Handling
默认规则:
-
默认情况下,Spring Boot提供
/error
映射,处理所有错误的映射 -
机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“ whitelabel”错误视图,以HTML格式呈现相同的数据 视图由StaticView提供
{ //机器客户端响应的内容"timestamp": "2020-11-22T05:53:28.416+00:00","status": 404,"error": "Not Found","message": "No message available","path": "/asadada"
}
// 如果是5xx,会有一个 trace属性,显示异常的出错的详细路径
- 要对其进行自定义,添加
View
解析为error
- 要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制但替换其内容 /templates/error/
下的4xx,5xx页面 会被自动解析- springboot错误请求返回的解析的信息,可以直接在前端使用 thymeleaf显示出来 :
th:text="${message} th:text="${path}"
53、错误处理-【源码分析】底层组件功能分析
-
ErrorMvcAutoConfiguration
自动配置异常处理规则 重要组件 5个-
给容器中的加入组件:类型:
DefaultErrorAttributes
-> **id:errorAttributes
** 😉==管数据==public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
DefaultErrorAttributes
:定义错误页面中可以包含哪些数据(异常明细,堆栈信息等)。
①Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) 方法封装了错误信息
-
给容器中加入组件:类型:
BasicErrorController
-> id:basicErrorController
(json+白页 适配响应)😉==管页面==- 处理默认
/error
路径的请求,页面响应new ModelAndView("error", model);
非页面响应new ReponseEntity<>(status)
- 默认处理 /error的请求,实际上是因为
server.error.path
默认为/error;那么我们也可以修改这个错误页面请求,yaml中:server.error.path="/xxx"
- 处理默认
-
给容器中的加入组件
View
->id是error;(响应默认错误页,生成一个默认的erroView)private final StaticView defaultErrorView = new StaticView();@Bean(name = "error")@ConditionalOnMissingBean(name = "error")public View defaultErrorView() {return this.defaultErrorView;}
-
给容器中的加入组件
BeanNameViewResolver
(视图解析器);按照返回的视图名作为组件的id去容器中找View
对象。
-
-
容器中的组件:类型:
DefaultErrorViewResolver
-> id:conventionErrorViewResolver
😉==管错误视图路径==-
@Override // 解析一个错误视图,返回它的modelAndView public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); // viewName == 响应状态码}return modelAndView; }
-
private ModelAndView resolve(String viewName, Map<String, Object> model) {String errorViewName = "error/" + viewName; // 在error文件夹下寻找对应的错误视图(这就是为什么直接在templates下添加error视图就行了)TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext);if (provider != null) {return new ModelAndView(errorViewName, model);}return resolveResource(errorViewName, model); }
-
-
如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面(主要作用)。
- error/404、5xx.html (拼接视图名)
- 如果想要返回页面,就会找error视图(
StaticView
默认是一个白页)。
54、错误处理-【源码流程】异常处理流程
譬如写一个会抛出异常的控制层:
@Slf4j
@RestController
public class HelloController {@RequestMapping("/hello")public String handle01(){int i = 1 / 0;//将会抛出ArithmeticExceptionlog.info("Hello, Spring Boot 2!");return "Hello, Spring Boot 2!";}
}
当浏览器发出/hello
请求,DispatcherServlet
的doDispatch()
的mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
将会抛出ArithmeticException
。
异常处理步骤流程
-
执行目标方法,目标方法运行期间有任何异常都会被catch,而且标志当前请求结束;捕捉异常封装给
dispatchException
(用来给视图提供信息) -
捕捉后,进入视图解析流程 (页面渲染)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
mv是空的,因为执行异常-
处理handler发生的异常:
mv=processHandlerException(request, response, handler, exception)
-
遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【
HandlerExceptionResolver
处理器异常解析器】// 接口只有一个方法,处理异常 @NullableModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex); //handler 发生异常的方法 ;返回一个mv
-
下一点在这段代码后
public class DispatcherServlet extends FrameworkServlet {...protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {...//将会抛出ArithmeticExceptionmv = ha.handle(processedRequest, response, mappedHandler.getHandler());applyDefaultViewName(processedRequest, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {//将会捕捉ArithmeticExceptiondispatchException = ex;}catch (Throwable err) {dispatchException = new NestedServletException("Handler dispatch failed", err);}//捕捉后,继续运行processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));}finally {...}}private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {boolean errorView = false;if (exception != null) {if (exception instanceof ModelAndViewDefiningException) {...}else {Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);//ArithmeticException将在这处理mv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}...}protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,@Nullable Object handler, Exception ex) throws Exception {//移除request的一些属性request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);ModelAndView exMv = null;if (this.handlerExceptionResolvers != null) {//遍历所有的 handlerExceptionResolvers,看谁能处理当前异常HandlerExceptionResolver处理器异常解析器for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {exMv = resolver.resolveException(request, response, handler, ex);if (exMv != null) {break;}}}...//若只有系统的自带的异常解析器(没有自定义的),异常还是会抛出throw ex;} }
-
系统自带的异常解析器:
-
DefaultErrorAttributes
先来处理异常,它主要功能把异常信息保存到request域,并且返回null 相当于帮做一件事,但不负责收尾
-
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {...//处理异常的方法 来自 HandlerExceptionResolverpublic ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {this.storeErrorAttributes(request, ex);return null; // <--- 注意,返回是空 (没有mv)}private void storeErrorAttributes(HttpServletRequest request, Exception ex) {request.setAttribute(ERROR_ATTRIBUTE, ex); //把异常信息保存到request域}...
}
-
ExceptionHandlerExceptionResolver
: 只对加了ExceptionHandle 注解的方法起作用 -
ResponseStatusExceptionResolver
: 只对加了 ResponseStatus注解的方法起作用 -
DefaultHandlerExceptionResolver
:无法处理当前请求,它负责处理spring本身的异常:比如发送一个 response.sendError(x,x) -
默认没有任何解析器(上图的
HandlerExceptionResolverComposite
)能处理异常,所以最后异常会被抛出。 -
最终底层就会转发
/error
请求。会被**底层的BasicErrorController
**接收处理。- 调用errorHtml方法处理,其中解析异常视图方法:resolveErrorView
resolveErrorView
会调用容器中的**defaultErrorViewResolver
**进行处理- 这个解析器,根据状态码拼接
/error
得到 视图名 ==>/error/5xx.html
- 最后返回 mv给模板引擎,响应这个页面
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {HttpStatus status = getStatus(request);Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));response.setStatus(status.value());// 解析异常视图,返回mvModelAndView modelAndView = resolveErrorView(request, response, status, model);//如果/template/error内没有4**.html或5**.html,//modelAndView为空,最终还是返回viewName为error的modelAndViewreturn (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}...
}
// 父类中的方法
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,Map<String, Object> model) {for (ErrorViewResolver resolver : this.errorViewResolvers) { // 只有一个defaultErrorViewResolver ,由它来解决 ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);if (modelAndView != null) {return modelAndView;}}return null;}
如果匹配不到4xx,5xx页面怎么办?接着看下面
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {...protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {...// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());...//渲染页面processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);...}private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {boolean errorView = false;...// Did the handler return a view to render?if (mv != null && !mv.wasCleared()) {render(mv, request, response);if (errorView) {WebUtils.clearErrorRequestAttributes(request);}}...}// 如果自己没有设计自定义的4xx,5xx页面,就返回默认的异常页面protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {...View view;String viewName = mv.getViewName();if (viewName != null) {// We need to resolve the view name.//找出合适error的View,如果/template/error内没有4**.html或5**.html,//将会返回默认异常页面ErrorMvcAutoConfiguration.StaticView//这里按需深究代码吧!view = resolveViewName(viewName, mv.getModelInternal(), locale, request);...}...try {if (mv.getStatus() != null) {response.setStatus(mv.getStatus().value());}//看下面代码块的StaticView的render块view.render(mv.getModelInternal(), request, response);}catch (Exception ex) {...}}}
ErrorMvcAutoConfiguration 错误自动配置类 (包含五个重要组件的类)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {... @Configuration(proxyBeanMethods = false)@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)@Conditional(ErrorTemplateMissingCondition.class)protected static class WhitelabelErrorViewConfiguration {//将创建一个名为error的系统默认异常页面View的Beanprivate final StaticView defaultErrorView = new StaticView();@Bean(name = "error")@ConditionalOnMissingBean(name = "error")public View defaultErrorView() {return this.defaultErrorView;}// If the user adds @EnableWebMvc then the bean name view resolver from// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.@Bean@ConditionalOnMissingBeanpublic BeanNameViewResolver beanNameViewResolver() {BeanNameViewResolver resolver = new BeanNameViewResolver();resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);return resolver;}} private static class StaticView implements View {private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);private static final Log logger = LogFactory.getLog(StaticView.class);@Overridepublic void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)throws Exception {if (response.isCommitted()) {String message = getMessage(model);logger.error(message);return;}response.setContentType(TEXT_HTML_UTF8.toString());StringBuilder builder = new StringBuilder();Object timestamp = model.get("timestamp");Object message = model.get("message");Object trace = model.get("trace");if (response.getContentType() == null) {response.setContentType(getContentType());}//系统默认异常页面html代码builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error"))).append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");if (message != null) {builder.append("<div>").append(htmlEscape(message)).append("</div>");}if (trace != null) {builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");}builder.append("</body></html>");response.getWriter().append(builder.toString());}private String htmlEscape(Object input) {return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;}private String getMessage(Map<String, ?> model) {Object path = model.get("path");String message = "Cannot render error page for request [" + path + "]";if (model.get("message") != null) {message += " and exception [" + model.get("message") + "]";}message += " as the response has already been committed.";message += " As a result, the response may have the wrong status code.";return message;}@Overridepublic String getContentType() {return "text/html";}}
}
55、错误处理-【源码流程】几种异常处理原理
几种处理错误方式的操作
- 自定义错误页
- error/404.html error/5xx.html;有精确的错误状态码页面 就匹配精确
- ,没有就找 4xx.html;
- 模糊匹配的前缀状态有五个(1,2,3,4,5),由底层一个枚举类封装起来
- 如果都没有就触发白页
-
@ControllerAdvice
+@ExceptionHandler
处理全局异常;底层是ExceptionHandlerExceptionResolver
支持的。 全局异常处理器的实现@Slf4j @ControllerAdvice public class GlobalExceptionHandler {@ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //捕获处理对应异常public String handleArithException(Exception e){log.error("异常是:{}",e);return "login"; //视图地址} }
-
@ResponseStatus
+自定义异常 ;底层是ResponseStatusExceptionResolver
。把responseStatus注解的信息解析出来,封装信息statusCode和resolvedReason,然后调用response.sendError(statusCode, resolvedReason)
–产生—>tomcat发送的/error
请求,再次被BasicErrorxxx处理,用默认的解析器处理@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多") // FORBIDDEN(403)禁止异常 public class UserTooManyException extends RuntimeException {// 自定义异常public UserTooManyException(){}public UserTooManyException(String message){super(message);} }
@Controller
public class TableController {@GetMapping("/dynamic_table")public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){//表格内容的遍历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();//抛出自定义异常}return "table/dynamic_table";}}
-
4.Spring自家异常:如
org.springframework.web.bind.MissingServletRequestParameterException
,DefaultHandlerExceptionResolver
处理Spring本身原始异常。response.sendError(HttpServletResponse.SC_BAD_REQUEST/*400*/, ex.getMessage());
- 在上面的代码发出请求后也会来到错误页(这就是上一节讲的
DefaultHandlerExceptionResolver
作用,)
-
5.
ErrorViewResolver
实现自定义处理异常,默认处理错误异常页response.sendError(HttpServletResponse.SC_NOT_FOUND)
【手动sendError】,error请求就会转给controller。- 你的异常没有任何人能处理,tomcat底层调用
response.sendError()
,error请求就会转给controller。 basicErrorController
要去的页面地址是ErrorViewResolver
。
自定义异常解析器
自定义实现 HandlerExceptionResolver
处理异常;可以作为默认的全局异常处理规则
@Order(value= Ordered.HIGHEST_PRECEDENCE) //优先级,数字越小优先级越高(排在第一位)
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {@Overridepublic ModelAndView resolveException(HttpServletRequest request,HttpServletResponse response,Object handler, Exception ex) {try {response.sendError(511,"我喜欢的错误"); // 还是使用sendError交给默认解析器} catch (IOException e) {e.printStackTrace();}return new ModelAndView();}
}
sendError触发这个解析器来处理错误异常
@FunctionalInterface
public interface ErrorViewResolver {ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
}
56、原生组件注入-原生注解与Spring方式注入
官方文档 - Servlets, Filters, and listeners web三大原生组件
解释一下Servlet (不太熟悉) :servlet,小服务程序或服务连接器,用java编写的服务器端程序,具有独立于平台和协议的特性,主要用于交互式地浏览和生成数据,生成动态的Web内容
使用原生的注解
使用原生的注解方式,注入三大组件
Servlet
@WebServlet(urlPatterns = "/my") // 处理的路径
public class MyServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.getWriter().write("66666");}
}
Filter
@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"}) // 拦截路径,做过滤工作
public class MyFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {log.info("MyFilter初始化完成");}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {// 核心功能,过滤一些请求(网络上过滤掉恶意或无用的请求)log.info("MyFilter工作");chain.doFilter(request,response);}@Overridepublic void destroy() {log.info("MyFilter销毁");}
}
Listener
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {@Overridepublic void contextInitialized(ServletContextEvent sce) { log.info("MySwervletContextListener监听到项目初始化完成");}@Overridepublic void contextDestroyed(ServletContextEvent sce) {log.info("MySwervletContextListener监听到项目销毁");}
}
最后还要在主启动类添加注解**@ServletComponentScan
**
@ServletComponentScan(basePackages = "com.xxxx")//指定原生的Servlet组件都在哪里
@SpringBootApplication(exclude = RedisAutoConfiguration.class)
public class Boot05WebAdminApplication {public static void main(String[] args) {SpringApplication.run(Boot05WebAdminApplication.class, args);}
}
监听器>过滤器>拦截器
Spring方式注入
使用spring方式,注入三大组件
ServletRegistrationBean
,
FilterRegistrationBean
,
ServletListenerRegistrationBean
@Configuration(proxyBeanMethods = true) // 防止组件冗余,保证其单实例
public class MyRegistConfig {@Beanpublic ServletRegistrationBean myServlet(){MyServlet myServlet = new MyServlet();return new ServletRegistrationBean(myServlet,"/my","/my02");// 指定处理路径}@Beanpublic FilterRegistrationBean myFilter(){MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet()); // 直接拦截某个Servlet,它管理什么路径,就拦截什么FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));return filterRegistrationBean;}@Beanpublic ServletListenerRegistrationBean myListener(){MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();return new ServletListenerRegistrationBean(mySwervletContextListener);}
}
注册的Servlet组件管理的/my请求
为什么没有被拦截器拦截,直接就可以访问?
扩展:DispatchServlet是如何注册到容器中的?
-
容器中自动配置了
DispatchServlet
属性绑定到WebMvcProperties
类上,对应的配置文件前缀是 : spring.mvc -
老版本:通过 继承
ServletRegistrationBean<DispatcherServlet>
DispatchServlet 也是Servlet,所以注册方式也跟Servlet相关,然后在配置类中使用 @Bean 注册进入容器
新版本:先注册一个dispatchSerlvet组件进入容器:@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {DispatcherServlet dispatcherServlet = new DispatcherServlet();dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());return dispatcherServlet;}
然后 在 DispatcherServletRegistrationConfiguration类中(也在xxxautoConfiguration类中),注册一个属于dispatchServlet的注册
DispatcherServletRegistrationBean
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,webMvcProperties.getServlet().getPath()); // 指定 dispatchSerlvet的默认映射路径registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());multipartConfig.ifAvailable(registration::setMultipartConfig);return registration;}public String getPath() {return this.path; //path---> "/"}
-
其默认的映射路径是 “/”
解释问题:
如果有多个Servlet能处理同一个路径,优先选择最精确的匹配 “/my”,而"/" 是优先级最低的匹配
既然都没有被dispatchServlet处理,那么配置在这上面的拦截也当然不管用
58、嵌入式Servlet容器-【源码分析】切换web服务器与定制化
TODO:迷糊一点,再一遍
-
默认支持的WebServer
Tomcat
,Jetty
, orUndertow
。ServletWebServerApplicationContext
容器启动寻找ServletWebServerFactory
并引导创建服务器。
-
原理
-
SpringBoot应用启动发现当前是Web应用,web场景包-导入tomcat。
-
web应用会创建一个web版的IOC容器:
ServletWebServerApplicationContext
。 -
ServletWebServerApplicationContext
启动的时候寻找ServletWebServerFactory
来自方法createWebServer中的getServerWebFactory
(Servlet 的web服务器工厂—produce—>Servlet 的web服务器)。 -
底层直接会有一个对应的自动配置类**
ServletWebServerFactoryAutoConfiguration
**。 -
ServletWebServerFactoryAutoConfiguration
导入了ServletWebServerFactoryConfiguration
(配置类)。- SpringBoot底层默认有很多的WebServer工厂(
ServletWebServerFactoryConfiguration
内创建Bean),如:TomcatServletWebServerFactory
这个工厂生产还有对应的条件(扫描是否有Servlet,Tomcat)JettyServletWebServerFactory
UndertowServletWebServerFactory
ServletWebServerFactoryConfiguration
根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有TomcatServletWebServerFactory
- SpringBoot底层默认有很多的WebServer工厂(
-
TomcatServletWebServerFactory
创建出Tomcat服务器并启动;TomcatWebServer
的有参构造器拥有初始化方法initialize
——this.tomcat.start(); 启动服务器
WebServer接口public interface WebServer {void start() throws WebServerException;void stop() throws WebServerException;int getPort();default void shutDownGracefully(GracefulShutdownCallback callback) {callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);} }
-
//getWebServer 方法是来自 ServletWebServerFactory接口 @Overridepublic WebServer getWebServer(ServletContextInitializer... initializers) {if (this.disableMBeanRegistry) {Registry.disableRegistry();}Tomcat tomcat = new Tomcat();File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");tomcat.setBaseDir(baseDir.getAbsolutePath());for (LifecycleListener listener : this.serverLifecycleListeners) {tomcat.getServer().addLifecycleListener(listener);}//以前手动启动服务器的操作,用固定代码来搞定Connector connector = new Connector(this.protocol);connector.setThrowOnFailure(true);tomcat.getService().addConnector(connector);customizeConnector(connector);tomcat.setConnector(connector);tomcat.getHost().setAutoDeploy(false);configureEngine(tomcat.getEngine());for (Connector additionalConnector : this.additionalTomcatConnectors) {tomcat.getService().addConnector(additionalConnector);}prepareContext(tomcat.getHost(), initializers);return getTomcatWebServer(tomcat); // 返回 TomcatWebServer}
-
内嵌服务器":与以前手动把启动服务器的操作,改成现在使用代码启动(tomcat核心jar包存在)。
切换服务器
-
Spring Boot默认使用Tomcat服务器,若需更改其他服务器,则修改工程pom.xml,移除TomCat服务器的导入,并导入其他的服务器:
<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> <!--引入其他的服务器:jetty服务器--> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jetty</artifactId> </dependency>
-
但是建议使用tomcat,因为熟悉
-
定制Servlet容器
第一种方法更加常用
-
修改配置文件
server.xxx
-
直接自定义
ConfigurableServletWebServerFactory
-
实现
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
- 在
ServletWebServerFactoryAutoConfiguration
中注册了这个定制化器 - 把配置文件的值和
ServletWebServerFactory
进行绑定 - 定制化器,后置地修改服务器工厂的一些参数
xxxxxCustomizer
:定制化器,可以改变xxxx的默认规则
import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.stereotype.Component;@Component public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {@Overridepublic void customize(ConfigurableServletWebServerFactory server) {server.setPort(9000);} }
- 在
-
实现
WebServerFactoryCustomizer<TomcatServletWebServerFactory>
跟第三种方法本质是一样的,是ConfigurableServletWebServerFactory
的特别的定制化处理。这样相同的还有JettyServletWebServerFactory
andUndertowServletWebServerFactory
59、定制化原理-SpringBoot定制化组件的几种方式(小结)
定制化的常见方式
Four Kinds
-
修改配置文件
-
xxxxxCustomizer
定制化器,后置修改(类似配置文件)- 编写from movies,boxoffice where movies.id=boxoffice.movie_id;
xxxConfiguration
+@Bean
替换、增加容器中默认组件webMvcConfigurer,视图解析器
- 编写from movies,boxoffice where movies.id=boxoffice.movie_id;
-
Web应用 编写一个配置类实现
WebMvcConfigurer
即可定制化web功能 +@Bean
给容器中再扩展一些组件@Configuration public class AdminWebConfig implements WebMvcConfigurer{ }
-
@
Configuration+@EnableWebMvc注解
+实现WebMvcConfigurer
—@Bean
可以全面接管SpringMVC,所有规则全部自己重新配置;
实现定制和扩展功能(高级功能,初学者退避三舍)。- 原理:
WebMvcAutoConfiguration
默认的SpringMVC的自动配置功能类,如静态资源、欢迎页等。- 一旦使用
@EnableWebMvc
,会**@Import(DelegatingWebMvcConfiguration.class)
** —> 关闭自动配置的效果,只留有springMVC最基本的功能 DelegatingWebMvcConfiguration
的作用,只保证SpringMVC最基本的使用- 把所有系统中的
WebMvcConfigurer
拿过来,所有功能的定制(资源绑定,拦截器设定…)都是这些WebMvcConfigurer
合起来一起生效。 批量处理的configurer的工具类是WebMvcConfigurerComposite
- 自动配置了一些非常底层的组件,如
RequestMappingHandlerMapping
,这些组件依赖的组件都是从容器中获取的。 public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
。
- 把所有系统中的
WebMvcAutoConfiguration
里面的配置要能生效必须有个条件@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
。WebMvcConfigurationSupport
是DelegatingWebMvcConfiguration
的父类- 所以,@EnableWebMvc 导致了WebMvcAutoConfiguration 没有生效,因为条件不满足,导致没有注入bean,没有生效。
- 原理:
原理分析套路
场景依赖starter – xxxxAutoConfiguration
– 导入xxx组件 – 绑定xxxProperties
– 绑定配置文件项。
源码部分基本已经分析完毕,反复看笔记,反复看debug,养成看源码解决问题的习惯
配置类和自动配置类:@AutoConfiguration(xxx),自动配置类也算是配置类,只是它再=在boot启动的时候,这些自动配置类,会根据Order顺序,以及自身的条件生效自己类中定义的组件和操作
60、数据访问-数据库场景的自动配置分析与整合测试
导入JDBC场景
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
导入了那些?
但是没有导入数据库驱动依赖,因为springboot不知道你要用什么数据库
接着导入数据库驱动包(MySQL为例)。
<!--默认版本:-->
<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
: 数据源的自动配置。- 修改数据源相关的配置,DataSourceProperties:配置前缀
spring.datasource
。 - 数据库连接池的配置,是自己容器中没有DataSource才自动配置的。
- 底层配置好的连接池是:
HikariDataSource
,我们也可以使用其他的连接池,常用的是德鲁伊。
- 修改数据源相关的配置,DataSourceProperties:配置前缀
-
DataSourceTransactionManagerAutoConfiguration
: 事务管理器的自动配置。 -
JdbcTemplateAutoConfiguration
:JdbcTemplate
的自动配置,可以来对数据库进行CRUD。- 可以修改前缀为
spring.jdbc
的配置项来修改JdbcTemplate
。 @Bean @Primary JdbcTemplate
:Spring容器中有这个JdbcTemplate
组件,使用@Autowired
即可从容器拿到。
- 可以修改前缀为
-
JndiDataSourceAutoConfiguration
:JNDI的自动配置。 -
XADataSourceAutoConfiguration
:分布式事务相关的。
修改配置项
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 1024url: jdbc:mysql://localhost:3306/kuangweb?userUnicode=true&characterEncoding=utf8
如果不配置数据源,而且导入了相关场景,启动就会报错
单元测试数据源
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;@SpringBootTest
class Boot05WebAdminApplicationTests {@AutowiredJdbcTemplate jdbcTemplate;@Test//用@org.junit.Test会报空指针异常,可能跟JUnit新版本有关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);}
}
61、数据访问-自定义方式整合druid数据源
Druid官网
Druid是什么?
它是数据库连接池,它能够提供强大的监控和扩展功能。
官方文档 - Druid连接池介绍
Spring Boot整合第三方技术的两种方式:
-
自定义=>XML
-
找starter场景依赖
自定义方式
添加依赖:
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.17</version>
</dependency>
配置Druid数据源:
用代码配置数据源,当然也可以使用配置文件,@ConfigurationProperties("spring.datasource")
,复用datasource的信息
@Configuration
public class MyConfig {@Bean@ConfigurationProperties("spring.datasource") //复用配置文件的数据源配置public DataSource dataSource() throws SQLException {DruidDataSource druidDataSource = new DruidDataSource();
// druidDataSource.setUrl();
// druidDataSource.setUsername();
// druidDataSource.setPassword();return druidDataSource;}
}
Druid数据源的更多配置,可以使用XML进行配置
配置Druid的监控页功能:
后面的链接,都是记载这原先使用MVC时,如何配置-
Druid内置提供了一个
StatViewServlet
用于展示Druid的统计信息。官方文档 - 配置。
这个StatViewServlet
的用途包括:- 提供监控信息展示的html页面
- 提供监控信息的JSON API
-
Druid内置提供一个
StatFilter
,用于统计监控信息。官方文档 - 配置_StatFilter -
提供了一个
WebStatFilter
用于采集web-jdbc关联监控的数据,如SQL监控、URI监控。官方文档 - 配置_配置WebStatFilter -
Druid提供了
WallFilter
,它是基于SQL语义分析来实现防御SQL注入攻击的。官方文档 - 配置 wallfilter注意,联合监控使用时:先写谁是有讲究的: 如果
setFilters("stat,wall")
:防火前拦截检测时间是会被统计的,反之不会!!
上面的配置现在都可以在Springboot中使用代码完成
@Configuration
public class MyConfig {@Bean@ConfigurationProperties("spring.datasource")public DataSource dataSource() throws SQLException {DruidDataSource druidDataSource = new DruidDataSource();//加入监控和防火墙功能功能 ==》 以前MVC也是在数据源的配置中,增加一条过滤器器属性 // <property name="filters" value="stat" />druidDataSource.setFilters("stat,wall"); // 逗号分隔//druidDataSource.setFilters("wall,stat"); // 这样,防火墙拦截检测的时间不在StatFilter统计的SQL执行时间内。return druidDataSource;}/*** 配置 druid的监控页功能* @return*/@Beanpublic ServletRegistrationBean statViewServlet(){StatViewServlet statViewServlet = new StatViewServlet();ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");//配置只拦截的路径,否则,直接拦截全部走这个servlet//监控页账号密码 <以初始化参数的形式添加>registrationBean.addInitParameter("loginUsername","admin");registrationBean.addInitParameter("loginPassword","123456");return registrationBean;}/*** WebStatFilter 用于采集web-jdbc关联监控的数据,监控web应用。*/@Beanpublic FilterRegistrationBean webStatFilter(){WebStatFilter webStatFilter = new WebStatFilter();FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));//初始化参数中设置: 忽略不过滤的请求 exclusions filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");return filterRegistrationBean;}}
62、数据访问-druid数据源starter整合方式
官方文档 - Druid Spring Boot Starter
引入依赖:
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.17</version>
</dependency>
分析依赖的自动配置:
-
扩展配置项
spring.datasource.druid
数据源配置项spring.datasource
(也和druid默认绑定起了) -
自动配置类
DruidDataSourceAutoConfigure
- 需要有DruidDataSource :
@ConditionalOnClass({DruidDataSource.class})
- 在官方的数据源自动配置前先配置下来:
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
,因为一个web服务只需要一个数据源,而装配数据源时,会先检查当前容器中是否已经有了数据源 @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
绑定属性类
- 需要有DruidDataSource :
-
另外导入的类 Import
DruidSpringAopConfiguration.class
, 监控SpringBean(spring组件)的 配置项前缀: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.filtermons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
配置示例:
spring:datasource:url: jdbc:mysql://localhost:3306/db_accountusername: rootpassword: 123456driver-class-name: com.mysql.jdbc.Driverdruid: # 德鲁伊配置在datasource下一层aop-patterns: com.morSun.* #监控SpringBean的位置filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)stat-view-servlet: # 配置监控页功能enabled: truelogin-username: adminlogin-password: adminresetEnable: falseUrlMappings: '/druid/*' #指定监控servlet映射路径,默认拦截所有web-stat-filter: # 监控webenabled: trueurlPattern: /*exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'filter: # 进行更精细的配置stat: # 对上面filters里面的stat的详细配置slow-sql-millis: 1000logSlowSql: trueenabled: truewall:enabled: trueconfig:drop-table-allow: false
63、数据访问-整合MyBatis-配置版
MyBatis的GitHub仓库
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>
配置模式:
-
引入了 **
MybatisAutoConfiguration
**和MybatisLanguageDriverConfiguration
-
全局配置文件
-
SqlSessionFactory
:自动配置好了 -
SqlSession
:自动配置了SqlSessionTemplate
组合了SqlSession
-
@Import(AutoConfiguredMapperScannerRegistrar.class)
-
Mapper
: 只要我们写的操作MyBatis的接口标准了@Mapper
就会被自动扫描进来
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) // 有mybatis的核心工厂类
@ConditionalOnSingleCandidate(DataSource.class) // 允许有且仅有一个数据源
@EnableConfigurationProperties(MybatisProperties.class) : MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{...
}@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties{...
}
配置文件:
spring:datasource:username: rootpassword: 1234url: jdbc:mysql://localhost:3306/mydriver-class-name: com.mysql.jdbc.Driver# 配置mybatis规则
mybatis:config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置mapper-locations: classpath:mybatis/*.xml #sql映射文件位置
mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis//DTD Config 3.0//EN"".dtd">
<configuration><!-- 由于Spring Boot自动配置缘故,此处不必配置,只用来做做样。-->
</configuration>
Mapper接口:
SQL映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis//DTD Mapper 3.0//EN"".dtd">
<mapper namespace="com.lun.boot.mapper.UserMapper"><select id="getUser" resultType="com.lun.boot.bean.User">select * from user where id=#{id}</select>
</mapper>
import com.lun.boot.bean.User;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface UserMapper {public User getUser(Integer id);
}
POJO:
public class User {private Integer id;private String name;//getters and setters...
}
DB:
CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(45) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
Controller and Service:
@Controller
public class UserController {@Autowiredprivate UserService userService;@ResponseBody@GetMapping("/user/{id}")public User getUser(@PathVariable("id") Integer id){return userService.getUser(id);}}
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;//IDEA下标红线,可忽视这红线public User getUser(Integer id){return userMapper.getUser(id);}}
配置private Configuration configuration;
也就是配置mybatis.configuration
相关的,就是相当于改mybatis全局配置文件中的值。(也就是说配置了mybatis.configuration
,就不需配置mybatis全局配置文件了)
# 配置mybatis规则
mybatis:mapper-locations: classpath:mybatis/mapper/*.xml# 可以不写全局配置文件,所有全局配置文件的配置都放在configuration配置项中了。# config-location: classpath:mybatis/mybatis-config.xmlconfiguration:map-underscore-to-camel-case: true
小结
- 导入MyBatis官方Starter。
- 编写Mapper接口,需
@Mapper
注解。 - 编写SQL映射文件并绑定Mapper接口。
- 在
application.yaml
中指定Mapper配置文件的所处位置,以及指定全局配置文件的信息 (建议:配置在mybatis.configuration,不用写在全局配置文件中
)==》 只写其中一种配置。
64、数据访问-整合MyBatis-注解配置混合版
你可以通过Spring Initializr添加MyBatis的Starer。
注解与配置混合搭配,干活不累:
@Mapper
public interface UserMapper {public User getUser(Integer id);@Select("select * from user where id=#{id}")public User getUser2(Integer id);public void saveUser(User user);@Insert("insert into user(`name`) values(#{name})")@Options(useGeneratedKeys = true, keyProperty = "id")public void saveUser2(User user);}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis//DTD Mapper 3.0//EN"".dtd">
<mapper namespace="com.lun.boot.mapper.UserMapper"><select id="getUser" resultType="com.lun.boot.bean.User">select * from user where id=#{id}</select><insert id="saveUser" useGeneratedKeys="true" keyProperty="id">insert into user(`name`) values(#{name})</insert></mapper>
-
简单DAO方法就写在注解上。复杂的就写在配置文件里。
-
使用**
@MapperScan("com.lun.boot.mapper")
简化**,Mapper接口就可以不用标注@Mapper
注解。
@MapperScan("com.lun.boot.mapper")
@SpringBootApplication
public class MainApplication {public static void main(String[] args) {SpringApplication.run(MainApplication.class, args);}}
65、数据访问-整合MyBatisPlus操作数据库
IDEA的MyBatis的插件 - MyBatisX
MyBatisPlus官网
MyBatisPlus官方文档
MyBatisPlus是什么
MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
添加依赖:
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.1</version>
</dependency>
-
MybatisPlusAutoConfiguration
配置类,MybatisPlusProperties
配置项绑定。 -
SqlSessionFactory
自动配置好,底层是容器中默认的数据源。 -
mapperLocations
自动配置好的,有默认值**classpath*:/mapper/**/*.xml
,这表示任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件**。 建议以后sql映射文件放在 mapper下。 -
容器中也自动配置好了
SqlSessionTemplate
。 -
@Mapper
标注的接口也会被自动扫描,建议直接@MapperScan("com.lun.boot.mapper")
批量扫描。 -
MyBatisPlus优点之一:只需要我们的Mapper继承MyBatisPlus的
BaseMapper
就可以拥有CRUD能力,减轻开发工作。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lun.hellomybatisplus.model.User;public interface UserMapper extends BaseMapper<User> {}
设计POJO类时,有时候会遇到类中的数据没有在数据库拥有对应的字段,所以可以使用
@TableField(exist=false)
@TableName(value ="user")// 指定表名,放在类上@TableField(exist = false)private String mmID;/*** 主键ID*/@TableId(type = IdType.AUTO)private Long id;
66、数据访问-CRUD实验-数据列表展示
官方文档 - CRUD接口
使用MyBatis Plus提供的IService
,ServiceImpl
,减轻Service层开发工作。
import com.lun.hellomybatisplus.model.User;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/*** Service 的CRUD也不用写了*/
public interface UserService extends IService<User> {
}
import com.lun.hellomybatisplus.model.User;
import com.lun.hellomybatisplus.mapper.UserMapper;
import com.lun.hellomybatisplus.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {//此处故意为空
}
与下一节联合在一起
67、数据访问-CRUD实验-分页数据展示
与下一节联合在一起
68、数据访问-CRUD实验-删除用户完成
添加分页插件:
@Configuration
public class MyBatisConfig {/*** MybatisPlusInterceptortodo: 分析其实现原理 * @return*/@Beanpublic MybatisPlusInterceptor paginationInterceptor() {MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false// paginationInterceptor.setOverflow(false);// 设置最大单页限制数量,默认 500 条,-1 不受限制// paginationInterceptor.setLimit(500);// 开启 count 的 join 优化,只针对部分 left join//这是分页拦截器PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();paginationInnerInterceptor.setOverflow(true);paginationInnerInterceptor.setMaxLimit(500L);mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);return mybatisPlusInterceptor;}
}
使用分页效果前端应该这样改
<table class="display table table-bordered table-striped" id="dynamic-table"><thead><tr><th>#</th><th>name</th><th>age</th><th>email</th><th>操作</th></tr></thead><tbody><tr class="gradeX" th:each="user: ${users.records}"><td th:text="${user.id}"></td><td>[[${user.name}]]</td><td th:text="${user.age}">Win 95+</td><td th:text="${user.email}">4</td><td>1.a标签使用button样式,变成可跳转的button<a th:href="@{/user/delete/{id}(id=${user.id},pn=${users.current})}" class="btn btn-danger btn-sm" type="button">删除</a></td></tr></tfoot>
</table><div class="row-fluid"><div class="span6"><div class="dataTables_info" id="dynamic-table_info">当前第[[${users.current}]]页 总计 [[${users.pages}]]页 共[[${users.total}]]条记录</div></div><div class="span6"><div class="dataTables_paginate paging_bootstrap pagination"><ul><li class="prev disabled"><a href="#">← 前一页</a></li>1.这里的按钮样式写法可借鉴(合理使用thymeleaf标签)<li th:class="${num == users.current?'active':''}" th:each="num:${#numbers.sequence(1,users.pages)}" ><a th:href="@{/dynamic_table(pn=${num})}">[[${num}]]</a> 2. ${num}行内写法,要加双中括号</li><li class="next disabled"><a href="#">下一页 → </a></li></ul></div></div>
</div>
#numbers
表示methods for formatting numeric objects.使用说明文档- 这是thymeleaf的内置对象发挥的作用,**sequence(from,to)**方法的作用是 生成一个由from到to的序列 ,此处是遍历查出的表单数据页
<a th:href="@{/dynamic_table(pn=${num})}">[[${num}]]</a>
这里给请求路径带?参数的写法在thymeleaf中规定,应该将参数用括号括起来<a th:href="@{/user/delete/{id}(id=${user.id},pn=${users.current})}"
**使用{id}**写法是因为要将id作为pattern,拼接到路径上
@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){// 注意这个pn如果没有拿到就会报错//表格内容的遍历//从数据库中查出user表中的用户进行展示//构造分页参数Page<User> page = new Page<>(pn, 2);//调用page进行分页 ,Paged对象包含几乎前端需要用到的所有信息Page<User> userPage = userService.page(page, null);model.addAttribute("users",userPage);return "table/dynamic_table";
}
如何进行分页查询?
-
配置好数据库连接,建好三层架构
-
配置好分页插件:用处后面toseeing….
@Bean 1.// 分页插件注入,主要的作用是做一些拦截的工作public MybatisPlusInterceptor paginationInterceptor() {MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();❤//定义分页拦截器PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认falsepaginationInnerInterceptor.setOverflow(true);// 设置最大单页限制数量,默认 500 条,-1 不受限制paginationInnerInterceptor.setMaxLimit(500L);//注册拦截器mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);return mybatisPlusInterceptor;}
-
使用Page封装返回对象,Page对象中的属性对于前端分页完全足够
-
Page对象创建应该传入,当前页码和一页应该分多少条数
重定向参数RedirectAttributes的使用
ra.addAttribute(“pn”,pn);
- 重定向参数的使用 ,将当前request请求的数据放在里面,重定向后仍然可以拿到(自动添加到url)
- 比如重定向的路径是:
/demo
加上重定向参数就会变成 :/demo?pn=xx
- 所以
/demo
请求的controller应该接收一个名叫 "pn"的参数
69、数据访问-准备阿里云Redis环境
添加依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><!--导入jedis,后面测试-->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>
-
RedisAutoConfiguration
自动配置类,RedisProperties 属性类 --> 配装前缀:spring.redis
-
Import
注解导入连接工厂LettuceConnectionConfiguration
、JedisConnectionConfiguration
是准备好的。LettuceConnectionConfiguration
条件是如果检测到为RedisClient客户端:自动导入默认客户端资源DefaultClientResources
以及Lettuce连接工厂LettuceConnectionFactory
JedisConnectionConfiguration
需要导入Jedis依赖,而且没有配置RedisConnectionFactory
就会创建Jedis连接
-
❗自动注入了**
RedisTemplate<Object, Object>
**,xxxTemplate
,操作redis 的工具 -
❗自动注入了
StringRedisTemplate
,key,value都是String -
底层只要我们使用
StringRedisTemplate
、RedisTemplate
就可以操作Redis。
外网Redis环境搭建:
-
阿里云按量付费Redis,其中选择经典网络。
-
阿里云中购买,选择 云数据库Redis版 <选择详情如下>
-
购买完成后,前往控制端
-
-
申请Redis的公网连接地址。
-
修改白名单,允许
0.0.0.0/0
访问。(所有人都可以访问) -
登录方式
-
Redis Desktop Manager测试一下
连接公网地址
密码 账号:密码 -
最后使用完记得释放,否则一直扣钱
70、数据访问-Redis操作与统计小实验
相关Redis配置:
spring:redis:
# url: redis://wang:wang!123@r-wz98rtfw492qqex6cipd.redis.rds.aliyuncs:6379host: r-wz98rtfw492qqex6cipd.redis.rds.aliyuncsport: 6379password: wang:wang!123
# 指定客户端的类型是jedisclient-type: jedisjedis:pool:max-active: 10
# lettuce:# 另一个用来连接redis的java框架
# pool:
# max-active: 10
# min-idle: 5
测试Redis连接:
@SpringBootTest
public class Boot05WebAdminApplicationTests {
// 操作redis的工具类@AutowiredStringRedisTemplate redisTemplate;@AutowiredRedisConnectionFactory redisConnectionFactory;@Testvoid testRedis(){ValueOperations<String, String> operations = redisTemplate.opsForValue();operations.set("msg","你好,我是MorSun");String msg = operations.get("msg");System.out.println(msg);System.out.println(redisConnectionFactory.getClass()); //org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory (使用redis)//使用 jedis ,连接工厂类型就不一样了-->org.springframework.data.redis.connection.jedis.JedisConnectionFactory (这两种工厂就是RedisAutoConfiguration Import导入的类)}
}
Redis Desktop Manager:可视化Redis管理软件。
URL统计拦截器:
@Component // 将他放入容器的原因是为了保证拦截器实体唯一,主要因为使用了容器中的组件StringRedisTemplate
public class RedisUrlCountInterceptor implements HandlerInterceptor {@AutowiredStringRedisTemplate redisTemplate; // 操作redis@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();//默认每次访问当前uri就会计数+1 key是uri,value是计数值 //使用 increment 是value自增redisTemplate.opsForValue().increment(uri);return true;}
}
注册URL统计拦截器:
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{@Autowired //RedisUrlCountInterceptor redisUrlCountInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(redisUrlCountInterceptor) // 从容器中拿到的拦截器,而不是new出来,这样redisTemplate操作的redis才是同一个 .addPathPatterns("/**").excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**","/aa/**");}
}
Filter、Interceptor 几乎拥有相同的功能?他们的区别是什么
- Filter是Servlet定义的原生组件,它的好处是 脱离Spring应用也能使用。
- Interceptor是Spring定义的接口,可以使用Spring的自动装配等功能。
为什么要给这个拦截添加到容器中?
注册拦截器的时候, @Autowired RedisUrlCountInterceptor redisUrlCountInterceptor;
,从容器中那这个定义的拦截器,因为只有这样,StringRedisTemplate和之前定义的才是同一个,才能保证操作redis是同一个,如果使用new的方法,就不同了
拿出Redis内的统计数据:
@Slf4j
@Controller
public class IndexController { // 将url访问次数,从redis中拿出来,传递到主页@AutowiredStringRedisTemplate redisTemplate;@GetMapping("/main.html")public String mainPage(HttpSession session,Model model){log.info("当前方法是:{}","mainPage");ValueOperations<String, String> opsForValue =redisTemplate.opsForValue();String s = opsForValue.get("/main.html");String s1 = opsForValue.get("/sql");model.addAttribute("mainCount",s);model.addAttribute("sqlCount",s1);return "main";}
}
可视化Redis软件查看拦截的url
TODO: 还需要系统学习一下 Redis 的API方法
71、单元测试-JUnit5简介
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
-
JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
-
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。
-
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,JUnit3.x的测试引擎。
注意:
-
SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入(不能使用JUnit4的功能 @Test)
-
JUnit 5’s Vintage已经从
spring-boot-starter-test
从移除。如果需要继续兼容Junit4需要自行引入Vintage依赖:
<dependency> <!---兼容JUnit4,Junit3--><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>
- 使用添加JUnit 5,添加对应的starter:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
- Spring的JUnit 5的基本单元测试模板(Spring的JUnit4的是
@SpringBootTest
+@RunWith(SpringRunner.class)
):
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;//注意不是org.junit.Test(JUnit4版本)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class SpringBootApplicationTests {@Autowiredprivate Component component;@Test//@Transactional 标注后连接数据库有回滚功能 都是属于Junit的注解public void contextLoads() {Assertions.assertEquals(5, component.getFive());}
}
72、单元测试-常用测试注解
官方文档 - Annotations包含了各种各样的注解的解释
-
@Test:表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
-
@ParameterizedTest:表示方法是参数化测试。
-
@RepeatedTest:表示方法可重复执行。
-
@DisplayName:为测试类或者测试方法设置展示名称。
-
@BeforeEach:表示在每个单元测试之前执行。要执行多次
-
@AfterEach:表示在每个单元测试之后执行。
-
@BeforeAll:表示在所有单元测试之前执行。 执行1次
-
@AfterAll:表示在所有单元测试之后执行。
BeforeAll
和AfterAll
标注的方法必须是静态方法,因为它只执行一次
-
@Tag:表示单元测试类别,类似于JUnit4中的@Categories。
-
@Disabled:表示测试类或测试方法不执行,类似于JUnit4中的==@Ignore==。
-
@Timeout:表示测试方法运行如果超过了指定时间将会返回错误。
-
@ExtendWith:为测试类或测试方法提供扩展类引用,提供一些额外的功能。
import org.junit.jupiter.api.*;@DisplayName("junit5功能测试类")
public class Junit5Test {@DisplayName("测试displayname注解")@Testvoid testDisplayName() {System.out.println(1);System.out.println(jdbcTemplate);}@ParameterizedTest@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })void palindromes(String candidate) {assertTrue(StringUtils.isPalindrome(candidate));}@Disabled@DisplayName("测试方法2")@Testvoid test2() {System.out.println(2);}@RepeatedTest(5) // 可重复执行5次@Testvoid test3() {System.out.println(5);}/*** 规定方法超时时间。超出时间测试出异常* @throws InterruptedException*/@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)@Testvoid testTimeout() throws InterruptedException {Thread.sleep(600);}@BeforeEachvoid testBeforeEach() {System.out.println("测试就要开始了...");}@AfterEachvoid testAfterEach() {System.out.println("测试结束了...");}@BeforeAllstatic void testBeforeAll() {System.out.println("所有测试就要开始了...");}@AfterAllstatic void testAfterAll() {System.out.println("所有测试以及结束了...");}
}
73、单元测试-断言机制
如果错误,Error表示,如果成功,Success
断言Assertion是测试方法中的核心部分,用来对测试需要满足的条件进行验证 。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后(使用idea的test功能,一次为项目所有的测试方法进行运行是否通过),会有一个详细的测试报告。
JUnit 5 内置的断言可以分成如下几个类别:
简单断言
用来对单个值进行简单的验证。如:
方法 | 说明 |
---|---|
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"); // message="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());
}
数组断言
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等。
@Test
@DisplayName("array assertion")
public void array() {assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}
组合断言
assertAll(String ,EXecutable)
方法接受多个 org.junit.jupiter.api.Executable
函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。 Executable 没有提供值,有返回值为Boolean(生产者)
@Test
@DisplayName("assert all")
public void all() {// all里面的多个断言必须全部成功,才能返回成功trueassertAll("Math",() -> assertEquals(2, 1 + 1),() -> assertTrue(1 > 0));
}
异常断言
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule
注解的ExpectedException
变量还是比较麻烦的。
而JUnit5提供了一种新的断言方式**Assertions.assertThrows()
**,配合函数式编程就可以进行使用。
@Test
@DisplayName("异常测试")
public void exceptionTest() {//如果没有抛出异常就会出错ArithmeticException exception = Assertions.assertThrows(//扔出断言异常ArithmeticException.class, () -> System.out.println(1 % 0),"哇!这个逻辑居然正常运行了");
}
超时断言
JUnit5还提供了Assertions.assertTimeout()为测试方法设置了超时时间。
@Test
@DisplayName("超时测试")
public void timeoutTest() {//如果测试方法时间超过1s将会异常 (超时时间,操作)Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
快速失败
通过 fail 方法直接使得测试失败。
@Test
@DisplayName("fail")
public void shouldFail() {fail("This should fail");// 直接让测试方法失败,fail()打断测试
}
断言官方文档详解
74、单元测试-前置条件
Unit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言assertions会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。
前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要 。
@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
对象才会被执行;当条件不满足时,测试执行并不会终止。
75、单元测试-嵌套测试
官方文档 - Nested Tests
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<>();//在嵌套的测试情况下,外层的Test不能驱动内层的Before/Afterxxx的测试方法assertNotNull(stack);// 判断stack是否为空 ==> 为空}@Nested@DisplayName("when new")class WhenNew {@BeforeEachvoid 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";@BeforeEachvoid pushAnElement() {stack.push(anElement);}// 内层的测试可以驱动外层的测试(before/afterxxxx),即这时候stack已经不是空了@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());}}}
}
76、单元测试**-参数化测试**
官方文档 - Parameterized Tests
参数化测试是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) { // string从strings中取值System.out.println(string);Assertions.assertTrue(StringUtils.isNotBlank(string)); // true,true,true
}@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {System.out.println(name);Assertions.assertNotNull(name);
}
/*注意修饰符为static,才能被直接引用*/
static Stream<String> method() {return Stream.of("apple", "banana");
}
迁移指南
官方文档 - Migrating from JUnit 4
在进行迁移的时候需要注意如下的变化:
- 注解在
org.junit.jupiter.api
包中,断言在org.junit.jupiter.api.Assertions
类中,前置条件在org.junit.jupiter.api.Assumptions
类中。 - 把
@Before
和@After
替换成@BeforeEach
和@AfterEach
。 - 把
@BeforeClass
和@AfterClass
替换成@BeforeAll
和@AfterAll。 - 把
@Ignore
替换成@Disabled
。 - 把
@Category
替换成@Tag
。 - 把
@RunWith
、@Rule
和@ClassRule
替换成@ExtendWith
。
77、指标监控-SpringBoot Actuator与Endpoint
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
官方文档 - Spring Boot Actuator: Production-ready Features
1.x与2.x版本的不同:
-
SpringBoot Actuator 1.x
- 支持SpringMVC
- 基于继承方式进行扩展
- 层级Metrics配置
- 自定义Metrics收集
- 默认较少的安全策略
-
SpringBoot Actuator 2.x
- 支持SpringMVC、JAX-RS以及Webflux 增加了兼容
- 注解驱动进行扩展
- 层级&名称空间Metrics
- 底层使用MicroMeter,简单、强大、便捷默认丰富的安全策略
如何使用
- 添加依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 访问
http://localhost:8080/actuator/**
。 - 暴露所有监控信息为HTTP。
- 默认只有几个endpoints为web暴露
- 大部分几乎所有都是以JMX方式 Exposing的 (Jconsole界面可以查看)
management:endpoints:enabled-by-default: true #暴露所有端点信息,web:exposure:include: '*' #以web方式暴露所有端点,都可以 以http的方式(url)访问
- 测试例子
- http://localhost:8080/actuator/beans 获取组件
- http://localhost:8080/actuator/configprops 展示配置属性列表
- http://localhost:8080/actuator/metrics 显示出当前项目的指标
- http://localhost:8080/actuator/metrics/jvm.gc.pause 访问某个指标的具体信息
- http://localhost:8080/actuator/metrics/endpointName/detailPath
78、指标监控-常使用的端点及开启与禁用
常使用的端点
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:日志记录
Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
- health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告 。
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis等。
- 可以很容易的添加自定义的健康检查机制。
Metrics Endpoint
提供详细的、层级的、空间指标信息,【需要二次请求】,这些信息可以被pull(主动推送)或者push(被动获取)方式得到:
- 通过Metrics对接多种监控系统。
- 简化核心Metrics开发。
- 添加自定义Metrics或者扩展已有Metrics。
management.endpoint.<endpointName>.xxx
表示具体对某个端点进行配置
开启与禁用Endpoints
- 默认所有的Endpoint除过shutdown都是开启的。
- 需要开启或者禁用某个Endpoint。配置模式为
management.endpoint.<endpointName>.enabled = true
management:endpoint:beans:enabled: true # 单独开启beans
- 或者禁用所有的Endpoint然后手动开启指定的Endpoint。(只开启自己想要的)
management:endpoints:enabled-by-default: falseendpoint:beans:enabled: truehealth:enabled: true
暴露Endpoints
支持的暴露方式
- HTTP:默认只暴露health和info。
- JMX:默认暴露所有Endpoint。
- 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入Spring Security,则会默认配置安全访问规则。 todo:未理解
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 |
若要更改暴露的Endpoint,请配置以下的包含和排除属性:
Property | Default |
---|---|
management.endpoints.jmx.exposure.exclude | |
management.endpoints.jmx.exposure.include | * |
management.endpoints.web.exposure.exclude | |
management.endpoints.web.exposure.include | info, health |
官方文档 - Exposing Endpoints
79、指标监控-定制Endpoint
定制 Health 信息
两种写法都可以参考一下
management:health:enabled: trueshow-details: always #总是显示详细信息。可显示每个模块的状态信息
通过实现
HealthIndicator
接口,或继承MyComHealthIndicator
类。
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;@Component
public class MyHealthIndicator implements HealthIndicator {@Overridepublic Health health() {int errorCode = check(); // perform some specific health checkif (errorCode != 0) {return Health.down().withDetail("Error Code", errorCode).build();}return Health.up().build();}
}/*
构建Health
Health build = Health.down().withDetail("msg", "error service").withDetail("code", "500").withException(new RuntimeException()).build();
*/
继承AbstractHealthIndicator抽象类,实现健康监控端点
@Component // 注入的组件名: MyCom
public class MyComHealthIndicator extends AbstractHealthIndicator {/*** 真实的检查方法,根据自己的业务代码,来设定项目的健康状态* @param builder* @throws Exception*/@Overrideprotected void doHealthCheck(Health.Builder builder) throws Exception {//mongodb。 获取连接进行测试Map<String,Object> map = new HashMap<>();// 检查完成if(1 == 2){
// builder.up(); //健康builder.status(Status.UP); // Status是个枚举类map.put("count",1);map.put("ms",100);}else {
// builder.down();builder.status(Status.OUT_OF_SERVICE);map.put("err","连接超时");map.put("ms",3000);}builder.withDetail("code",100).withDetails(map);}
}
定制info信息
常用两种方式:
- 编写配置文件
info:appName: boot-admin version: 2.0.1mavenProjectName: @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 {@Overridepublic void contribute(Info.Builder builder) {builder.withDetail("example", // withDetail 定制信息Collections.singletonMap("key", "value"));}}
http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息
定制Metrics信息
如果我想要对某一个请求(“/main”)或一个业务操作进行指标监控,就需要定制一个e
Spring Boot支持的metrics
增加定制Metrics:
class MyService{Counter counter;// 在构造的时候,注册一个meter注册器public MyService(MeterRegistry meterRegistry){counter = meterRegistry.counter("myservice.method.running.counter"); // 指定自定义指标的名字(/actutator/会显示出这个指标)} ///counter用来计数public void hello() {counter.increment(); //每调用一次hello,监控计数+1}
}
下面这种方式:todo:看不明白
//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
定制Endpoint
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来管理程序是否就绪。
- 开发LivenessEndpoint来管理程序是否存活。
80、指标监控-Boot Admin Server
引入依赖
<!--指标监控使用--><dependency><groupId>de.codecentric</groupId><artifactId>spring-boot-admin-starter-server</artifactId><version>2.3.1</version> <!--springboot3.0 不兼容以上的版本--></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
给主启动类添加注解:@EnableAdminServer
启动监控服务,然后访问localhost:8888/ 直接访问主页
BUG:springboot版本不能高于3.0,否则访问监控主页失败
官方文档
可视化指标监控
What is Spring Boot Admin?
Codecentric的SpringBootAdmin是一个社区项目,用于管理和监视SpringBoot@应用程序。应用程序注册到我们的SpringBootAdmin客户端(通过HTTP),或者使用SpringCloud@(例如Eureka,Con领事)发现。UL只是SpringBootActuator端点之上的Vue.js应用程序。
快速开始使用方法
日志,jvm,内存,项目装填….等等信息
高级特性&原理特性
81、高级特性-Profile环境切换
为了方便多环境适配,Spring Boot简化了profile功能。
- 默认配置文件
application.yaml
任何时候都会加载。 - 指定环境配置文件
application-{env}.yaml
,env
通常替代为test
, - 激活指定环境
- 配置文件激活:
spring.profiles.active=prod
- 命令行激活:
java -jar xxx.jar --spring.profiles.active=prod --person.name=haha
(修改配置文件的任意值,命令行优先)
- 配置文件激活:
- 默认配置与指定环境配置同时生效
- 同名配置项,profile配置优先,指定环境配置会覆盖默认环境配置
@Profile条件装配功能
@Data
@Component //放入容器
@ConfigurationProperties("person")//ConfigurationProperties让其可以在配置文件中配置
public class Person{private String name;private Integer age;
}
application.yaml
person: name: lunage: 8
@Profile可以修饰类
绑定同一个接口不同子类,如何加载其数据?
public interface Person {String getName();Integer getAge();}@Profile("test")//加载application-test.yaml里的数据
@Component
@ConfigurationProperties("person")
@Data
public class Worker implements Person {private String name;private Integer age;
}@Profile(value = {"prod","default"})//加载application-prod.yaml里的
@Component
@ConfigurationProperties("person")
@Data
public class Boss implements Person {private String name;private Integer age;
}
appliation-test.yaml
person:name: test-张三server:port: 7000
application-prod.yaml
person:name: prod-张三server:port: 8000
application.properties
# 激活prod配置文件
spring.profiles.active=prod
@Autowired
private Person person;// 这里使用person,运用了java的多态性@GetMapping("/")
public String hello(){//激活了prod,则返回Boss name=prod-张三;激活了test,则返回Worker name=test-张三return person.getClass().toString();
}
@Profile还可以修饰在方法上:
class Color {
}
/*使用配置类的方法*/
@Configuration
public class MyConfig {@Profile("prod") //只有在当前 prod环境下,才会有red这个组件@Beanpublic Color red(){return new Color();}@Profile("test")//只有在当前test环境下,才会有green这个组件@Beanpublic Color green(){return new Color();}
}
可以激活一组:主要是为了防止所有配置全在一个配置文件中显得冗余且难查阅 [可以使用]
spring.profiles.active=productionspring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
application-proddb.yaml和application-prodmq.yaml
82、高级特性-配置加载优先级
外部化配置
官方文档 - Externalized Configuration
import org.springframework.stereotype.*;
import org.springframework.beans.factory.annotation.*;@Component
public class MyBean {@Value("${name}")//以这种方式可以获得配置值private String name;// ...}
public static void main(String[] args) {ConfigurableApplicationContext run = SpringApplication.run(BootAdminServerApplication.class, args);ConfigurableEnvironment environment = run.getEnvironment();// 获取环境Map<String, Object> systemEnvironment = environment.getSystemEnvironment();// 获取系统环境变量 Map<String, Object> systemProperties = environment.getSystemProperties();// 获取系统环境配置属性System.out.println(systemEnvironment); //这里面的属性,都可以根据@Value拿到值System.out.println(systemProperties);}
- 外部配置源
- Java属性文件properties
- YAML文件
- 环境变量 (电脑环境变量) @Value(${环境变量的key})
- 命令行参数 最后修改属性的一步,起决定性作用
- 配置文件查找位置
- classpath 根路径。❤
- classpath 根路径下config目录。❤
- jar包当前目录。<创建一个yaml放在jar当前目录>❤
- jar包当前目录的config目录下。
- jar包当前目录的 /config子目录的直接子目录。🤍
- 配置文件加载顺序:
- 当前jar包内部的
application.properties
和application.yml
。 - 当前jar包内部的
application-{profile}.properties
和application-{profile}.yml
。 - 引用的外部jar包的
application.properties
和application.yml
。 - 引用的外部jar包的
application-{profile}.properties
和application-{profile}.yml
。 - 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项。(越后加载越优先)
- 当前jar包内部的
83、高级特性-自定义starter依赖细节
starter启动原理
- starter的pom.xml引入autoconfigure依赖
-
autoconfigure包中配置使用
META-INF/spring.factories
中EnableAutoConfiguration
的值,使得项目启动加载指定的自动配置类 -
编写自动配置类
xxxAutoConfiguration
->xxxxProperties
-
@Configuration
@Conditional
@EnableConfigurationProperties
@Bean
- …
-
引入starter —
xxxAutoConfiguration
— 容器中放入组件 ----绑定xxxProperties
---- 配置项
自定义starter
-
目标:创建
HelloService
的自定义starter。 -
创建两个工程,分别命名为
hello-spring-boot-starter
(普通Maven工程),hello-spring-boot-starter-autoconfigure
(需用用到Spring Initializr创建的Maven工程)。 -
hello-spring-boot-starter
无需编写什么代码,只需让该工程引入hello-spring-boot-starter-autoconfigure
依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=".0.0"xmlns:xsi=""xsi:schemaLocation=".0.0 .0.0.xsd"><modelVersion>4.0.0</modelVersion>
<!--以后用来导入的依赖,xxx-spring-boot-starter--><groupId>com.lun</groupId><artifactId>hello-spring-boot-starter</artifactId><version>1.0.0-SNAPSHOT</version><dependencies><!--导入自动配置模块--><dependency><groupId>com.lun</groupId><artifactId>hello-spring-boot-starter-autoconfigure</artifactId><version>1.0.0-SNAPSHOT</version></dependency></dependencies></project>
hello-spring-boot-starter-autoconfigure
的pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=".0.0" xmlns:xsi=""xsi:schemaLocation=".0.0 .0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.2</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lun</groupId><artifactId>hello-spring-boot-starter-autoconfigure</artifactId><version>1.0.0-SNAPSHOT</version><name>hello-spring-boot-starter-autoconfigure</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency></dependencies>
</project>
-
创建4个文件:
com/lun/hello/auto/HelloServiceAutoConfiguration
com/lun/hello/bean/HelloProperties
com/lun/hello/service/HelloService
src/main/resources/META-INF/spring.factories
除此以外不需要其他的文件,包括主启动类以及测试类等
import com.lun.hello.bean.HelloProperties;
import com.lun.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)//默认HelloProperties放在容器中,还与配置文件绑定
public class HelloServiceAutoConfiguration { @ConditionalOnMissingBean(HelloService.class)// 保证唯一性@Beanpublic HelloService helloService(){return new HelloService();}}
如果 @ConditionalOnMissingBean(HelloService.class)放在类上,那配置绑定都不会生效,就会导致包报错
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("hello")// 配置文件绑定,还可以将他放入容器中,前缀是 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;}
}
import com.lun.hello.bean.HelloProperties;
import org.springframework.beans.factory.annotation.Autowired;
/*** 默认不要放在容器中---->service;因为用户可以自定义helloService,如果在这里定死了,自动配置类时也无法按条件注入HelloService了*/
public class HelloService {@Autowiredprivate HelloProperties helloProperties;public String sayHello(String userName){return helloProperties.getPrefix() + ": " + userName + " > " + helloProperties.getSuffix();}
}
如何生效?要被springboot扫描这个starter,要保证他在spring.factories中有声明 (自动配置原理)。现在添加进去
指定auto包下的自动配置类
# Auto Configure 用EnableAutoConfiguration指定
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.hellospringbootstarterautoconfigure.auto.HelloServiceAutoConfiguration
使用自定义Starter
-
用maven插件,将两工程clean和install到本地。
-
接下来,测试使用自定义starter,用Spring Initializr创建名为
hello-spring-boot-starter-test
工程,引入hello-spring-boot-starter
依赖(哪个Maven项目中的starter),其pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=".0.0" xmlns:xsi=""xsi:schemaLocation=".0.0 .0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.2</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.lun</groupId><artifactId>hello-spring-boot-starter-test</artifactId><version>1.0.0-SNAPSHOT</version><name>hello-spring-boot-starter-test</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- 引入`hello-spring-boot-starter`依赖,所以为啥叫xxx-spring-boot-starter,因为项目名叫这样 --><dependency><groupId>com.lun</groupId><artifactId>hello-spring-boot-starter</artifactId><version>1.0.0-SNAPSHOT</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
- 添加配置文件
application.properties
:
hello.prefix=hello
hello.suffix=666
- 添加单元测试类:
import com.lun.hello.service.HelloService;//来自自定义starter
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
class HelloSpringBootStarterTestApplicationTests {@Autowiredprivate HelloService helloService; /// 直接拿到容器中的service,执行sayhello方法@Testvoid contextLoads() {// System.out.println(helloService.sayHello("lun"));Assertions.assertEquals("hello: lun > 666", helloService.sayHello("lun")); //true}
}
当然测试类也可以自己自定义一个HelloService,那自动配置的就是失效了
TODO:整个流程可以跑一遍 (这就是maven中的依赖来源,只是那个比较复杂)
84、原理解析-SpringApplication创建初始化流程
spring原理(组件),springmvc,自动配置
SpringBoot启动过程
Spring Boot应用的启动类: 看一下大概有那些?
@SpringBootApplication(exclude = RedisAutoConfiguration.class)
可以除去关于redis的所有自动配置
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class HelloSpringBootStarterTestApplication {public static void main(String[] args) {ConfigurableApplicationContext run =SpringApplication.run(HelloSpringBootStarterTestApplication.class, args);}
}
public class SpringApplication {...public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {return run(new Class<?>[] { primarySource }, args);}public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {return new SpringApplication(primarySources).run(args);}0. //先看看new SpringApplication(primarySources),下一节再看看run()public SpringApplication(Class<?>... primarySources) {this(null, primarySources);}/*诠释为什么自动配置要扫描spring.factories*/public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;Assert.notNull(primarySources, "PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));1.//WebApplicationType是枚举类,有NONE,SERVLET,REACTIVE,下行webApplicationType是SERVLET ---> 判断当前web的类型this.webApplicationType = WebApplicationType.deduceFromClasspath();2.//初始启动引导器-->去spring.factories文件中找org.springframework.boot.Bootstrapper,但我找不到实现Bootstrapper接口的类this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));3. //初始化器,去spring.factories找 ApplicationContextInitializersetInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));4.//去spring.factories找 ApplicationListenersetListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));this.mainApplicationClass = deduceMainApplicationClass();}private Class<?> deduceMainApplicationClass() { // 找出主程序try {StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); //扫描堆 栈for (StackTraceElement stackTraceElement : stackTrace) {if ("main".equals(stackTraceElement.getMethodName())) { //找出主程序 (找第一个)return Class.forName(stackTraceElement.getClassName());}}}catch (ClassNotFoundException ex) {// Swallow and continue}return null;}...}
重要的自动配置引导文件
spring.factories:
...# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener...
85、原理解析-SpringBoot完整启动过程
todo:有空再看一遍
继续上一节,接着讨论return new SpringApplication(primarySources).run(args)
的**run
方法**—> 运行Springboot
public class SpringApplication {...public ConfigurableApplicationContext run(String... args) {StopWatch stopWatch = new StopWatch();//开始计时器stopWatch.start();//所有监听器开始计时//1.//创建引导上下文(Context环境)createBootstrapContext()//这个方法会获取到所有之前的 ‘bootstrappers’ 挨个执行 intitialize() 来完成对引导启动器上下文环境设置DefaultBootstrapContext bootstrapContext = createBootstrapContext();// 调用者是上下文获取的//2.到最后该方法会返回这contextConfigurableApplicationContext context = null;//3.让当前应用进入headless模式 (自力更生)configureHeadlessProperty();//4.获取所有 RunListener(运行监听器),为了方便所有Listener进行事件感知//还是去指定的spring.factories,找到【SpringApplicationRunListeners】SpringApplicationRunListeners listeners = getRunListeners(args);//5. 遍历 SpringApplicationRunListener 调用 【starting】 方法;// 相当于 通知 所有感兴趣系统正在启动过程的人,项目正在 starting。listeners.starting(bootstrapContext, this.mainApplicationClass);try {//6.保存命令行参数 ApplicationArguments,以便后面使用ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);//7.准备环境 ---> /*7.1返回或创建基础的环境信息 :standardServletEnvironment7.2 配置环境信息--》对其添加一些类型转换器;加载配置信息(注解,文件,命令行。。。其他数据源);profile的绑定监听器listeners遍历调用每个listener的environmentPrepared即通知所有的监听器,当前环境准备完成:*/ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);// 环境信息要忽略的信息configureIgnoreBeanInfo(environment);/*打印标志. ____ _ __ _ _/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/:: Spring Boot :: (v2.4.2)*/Banner printedBanner = printBanner(environment);重要❗❗❗ // 8.创建IOC容器(createApplicationContext())// 根据项目类型webApplicationType(NONE,SERVLET,REACTIVE)创建容器, web的项目类型//因为Servlet所以当前会创建 AnnotationConfigServletWebServerApplicationContextcontext = createApplicationContext();context.setApplicationStartup(this.applicationStartup);//9.准备ApplicationContext IOC容器的基本信息 [prepareContext]/*9.1 保存环境信息9.2 IOC容器的后置处理程序9.3 应用初始化器:-遍历所有的ApplicationContextinitializer ,调用initialize,来对IOC容器进行初始化扩展功能-遍历所有的listener调用contextPerpared,。EventPublishListener 通知所有的监听器contextPerpared准备完成了9.4通知所有的监听器,调用contextLoaded,完成事件*/prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);//10.刷新IOC容器/*调用IOC的refresh方法,创建容器中的所有组件,Spring框架的内容*/refreshContext(context);//该方法没内容,大概为将来填入--> 在刷新完成后要完成哪些工作,可以写在这儿afterRefresh(context, applicationArguments);stopWatch.stop();//所有监听器停止计时if (this.logStartupInfo) {//this.logStartupInfo默认是truenew StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}//11.listeners.started(context);//12.调用所有runners/*获取容器中的ApplicationRunner和CommandLineRunner合并所有的Runner,进行排序order如果有runner,遍历runner根据其类型(app/command)执行对应的方法-如果出现了异常,调用监听器的failed方法-如果正常执行到这儿了,调用所有监听器的Running方法【listener.running(context)】通知所有的监听器启动当然如果这里错误了,也会执行了failed方法,*/callRunners(context, applicationArguments);}catch (Throwable ex) {//13.handleRunFailure(context, ex, listeners);throw new IllegalStateException(ex);}try {//12.listeners.running(context);}catch (Throwable ex) {//13.handleRunFailure(context, ex, null);throw new IllegalStateException(ex);}return context;}//1. private DefaultBootstrapContext createBootstrapContext() {DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();this.bootstrappers.forEach((initializer) -> initializer.intitialize(bootstrapContext));return bootstrapContext;}//3.private void configureHeadlessProperty() {//this.headless默认为trueSystem.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));}private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";//4.private SpringApplicationRunListeners getRunListeners(String[] args) {Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };//getSpringFactoriesInstances 去 spring.factories 找 SpringApplicationRunListenerreturn new SpringApplicationRunListeners(logger,getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),this.applicationStartup);}//7.准备环境private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {// Create and configure the environment//返回或者创建基础环境信息对象,如:StandardServletEnvironment, StandardReactiveWebEnvironmentConfigurableEnvironment environment = getOrCreateEnvironment();//配置环境信息对象,读取所有的配置源的配置属性值。configureEnvironment(environment, applicationArguments.getSourceArgs());//绑定环境信息ConfigurationPropertySources.attach(environment);//7.1 通知所有的监听器当前环境准备完成listeners.environmentPrepared(bootstrapContext, environment);DefaultPropertiesPropertySource.moveToEnd(environment);configureAdditionalProfiles(environment);bindToSpringApplication(environment);if (!this.isCustomEnvironment) {environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());}ConfigurationPropertySources.attach(environment);return environment;}//8.private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments, Banner printedBanner) {//保存环境信息context.setEnvironment(environment);//IOC容器的后置处理流程postProcessApplicationContext(context);//应用初始化器applyInitializers(context);//8.1 遍历所有的 listener 调用 contextPrepared。//EventPublishRunListenr 通知所有的监听器contextPreparedlisteners.contextPrepared(context);bootstrapContext.close(context);if (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context);}// Add boot specific singleton beansConfigurableListableBeanFactory beanFactory = context.getBeanFactory();beanFactory.registerSingleton("springApplicationArguments", applicationArguments);if (printedBanner != null) {beanFactory.registerSingleton("springBootBanner", printedBanner);}if (beanFactory instanceof DefaultListableBeanFactory) {((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);}if (this.lazyInitialization) {context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());}// Load the sourcesSet<Object> sources = getAllSources();Assert.notEmpty(sources, "Sources must not be empty");load(context, sources.toArray(new Object[0]));//8.2listeners.contextLoaded(context);}//12.调用所有runnersprivate void callRunners(ApplicationContext context, ApplicationArguments args) {List<Object> runners = new ArrayList<>();//获取容器中的 ApplicationRunnerrunners.addAll(context.getBeansOfType(ApplicationRunner.class).values());//获取容器中的 CommandLineRunnerrunners.addAll(context.getBeansOfType(CommandLineRunner.class).values());//合并所有runner并且按照@Order进行排序AnnotationAwareOrderComparator.sort(runners);//遍历所有的runner。调用 run 方法for (Object runner : new LinkedHashSet<>(runners)) {if (runner instanceof ApplicationRunner) {callRunner((ApplicationRunner) runner, args);}if (runner instanceof CommandLineRunner) {callRunner((CommandLineRunner) runner, args);}}}//13.private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,SpringApplicationRunListeners listeners) {try {try {handleExitCode(context, exception);if (listeners != null) {//14.listeners.failed(context, exception);}}finally {reportFailure(getExceptionReporters(context), exception);if (context != null) {context.close();}}}catch (Exception ex) {logger.warn("Unable to close ApplicationContext", ex);}ReflectionUtils.rethrowRuntimeException(exception);}...
}
SpringBoot启动的重要参与文件和类
//2. new SpringApplication(primarySources).run(args) 最后返回的接口类型
public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {String CONFIG_LOCATION_DELIMITERS = ",; \t\n";String CONVERSION_SERVICE_BEAN_NAME = "conversionService";String LOAD_TIME_WEAVER_BEAN_NAME = "loadTimeWeaver";String ENVIRONMENT_BEAN_NAME = "environment";String SYSTEM_PROPERTIES_BEAN_NAME = "systemProperties";String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment";String APPLICATION_STARTUP_BEAN_NAME = "applicationStartup";String SHUTDOWN_HOOK_THREAD_NAME = "SpringContextShutdownHook";void setId(String var1);void setParent(@Nullable ApplicationContext var1);void setEnvironment(ConfigurableEnvironment var1);ConfigurableEnvironment getEnvironment();// 获取当前环境,然后获取信息void setApplicationStartup(ApplicationStartup var1);ApplicationStartup getApplicationStartup();void addBeanFactoryPostProcessor(BeanFactoryPostProcessor var1);void addApplicationListener(ApplicationListener<?> var1);void setClassLoader(ClassLoader var1);void addProtocolResolver(ProtocolResolver var1);void refresh() throws BeansException, IllegalStateException;void registerShutdownHook();void close();boolean isActive();ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;
}
#4.
#spring.factories
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
class SpringApplicationRunListeners {private final Log log;private final List<SpringApplicationRunListener> listeners;private final ApplicationStartup applicationStartup;SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners,ApplicationStartup applicationStartup) {this.log = log;this.listeners = new ArrayList<>(listeners);this.applicationStartup = applicationStartup;}//5.遍历 SpringApplicationRunListener 调用 starting 方法;//相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),(step) -> {if (mainApplicationClass != null) {step.tag("mainApplicationClass", mainApplicationClass.getName());}});}//7.1void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {doWithListeners("spring.boot.application.environment-prepared",(listener) -> listener.environmentPrepared(bootstrapContext, environment));}//8.1void contextPrepared(ConfigurableApplicationContext context) {doWithListeners("spring.boot.application.context-prepared", (listener) -> listener.contextPrepared(context));}//8.2void contextLoaded(ConfigurableApplicationContext context) {doWithListeners("spring.boot.application.context-loaded", (listener) -> listener.contextLoaded(context));}//10.void started(ConfigurableApplicationContext context) {doWithListeners("spring.boot.application.started", (listener) -> listener.started(context));}//12.void running(ConfigurableApplicationContext context) {doWithListeners("spring.boot.application.running", (listener) -> listener.running(context));}//14.void failed(ConfigurableApplicationContext context, Throwable exception) {doWithListeners("spring.boot.application.failed",(listener) -> callFailedListener(listener, context, exception), (step) -> {step.tag("exception", exception.getClass().toString());step.tag("message", exception.getMessage());});}private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction,Consumer<StartupStep> stepAction) {StartupStep step = this.applicationStartup.start(stepName);this.listeners.forEach(listenerAction);if (stepAction != null) {stepAction.accept(step);}step.end();}...}
86、原理解析-自定义事件监听组件
以下的几个组件也是Springboot启动的关键工作组件 【03-startedBootProcess模块】
1.MyApplicationContextInitializer.java
初始化应用容器
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;public class MyApplicationContextInitializer implements ApplicationContextInitializer {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {System.out.println("MyApplicationContextInitializer ....initialize.... ");}
}
2.MyApplicationListener.java
监听事件
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;public class MyApplicationListener implements ApplicationListener {@Overridepublic void onApplicationEvent(ApplicationEvent event) {System.out.println("MyApplicationListener.....onApplicationEvent...");}
}
3.MyApplicationRunner.java
应用启动器
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Order(1)
@Component//放入容器
public class MyApplicationRunner implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {System.out.println("MyApplicationRunner...run...");}
}
4.MyCommandLineRunner.java
命令行启动器
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/*** 应用启动做一个一次性事情,就可以使用这个*/
@Order(2)
@Component//放入容器
public class MyCommandLineRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {System.out.println("MyCommandLineRunner....run....");}
}
5.MySpringApplicationRunListener.java
Spring应用运行监听器 包含spring启动的几个过程
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;public class MySpringApplicationRunListener implements SpringApplicationRunListener {private SpringApplication application; // 这个组件listener是可以拿到应用的所有信息的public MySpringApplicationRunListener(SpringApplication application, String[] args){this.application = application;}@Overridepublic void starting(ConfigurableBootstrapContext bootstrapContext) {System.out.println("MySpringApplicationRunListener....starting....");}@Overridepublic void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {System.out.println("MySpringApplicationRunListener....environmentPrepared....");}@Overridepublic void contextPrepared(ConfigurableApplicationContext context) {System.out.println("MySpringApplicationRunListener....contextPrepared....");}@Overridepublic void contextLoaded(ConfigurableApplicationContext context) {System.out.println("MySpringApplicationRunListener....contextLoaded....");}@Overridepublic void started(ConfigurableApplicationContext context) {System.out.println("MySpringApplicationRunListener....started....");}
/*现在的SpringApplicationRunListener接口已经没有这个方法了,进而改变成了ready方法*/ @Overridepublic void running(ConfigurableApplicationContext context) {System.out.println("MySpringApplicationRunListener....running....");}@Overridepublic void failed(ConfigurableApplicationContext context, Throwable exception) {System.out.println("MySpringApplicationRunListener....failed....");}
}
😀注册MyApplicationContextInitializer
,MyApplicationListener
,MySpringApplicationRunListener
:
resources / META-INF / spring.factories
:
org.springframework.context.ApplicationContextInitializer=\com.lun.boot.listener.MyApplicationContextInitializerorg.springframework.context.ApplicationListener=\com.lun.boot.listener.MyApplicationListenerorg.springframework.boot.SpringApplicationRunListener=\com.lun.boot.listener.MySpringApplicationRunListener
😀**ApplicationRunner
和CommandLineRunner
会从容器中拿取,所以只将他们注入容器即可**
🤣 根据上面的组件来理解springboot的启动流程
87、后会有期
路漫漫其修远兮,吾将上下而求索。
纸上得来终觉浅,绝知此事要躬行。
更多推荐
SpringBoot2学习笔记
发布评论