SpringBoot2学习笔记-2

45、web实验-抽取公共页面

官方文档 - Template Layout

  • 公共页面/templates/common.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"><!--注意要添加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"

引用公共片段标签

  1. th:insert="~{common::demo1}"
  2. th:insert="common::demo1"
  3. th:insert="~{common::#id}"
  4. 同时给引用片段的 关键字
    • 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="http://www.thymeleaf.org">
<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、视图解析-【源码分析】-视图解析器与视图

视图解析原理流程

  1. 目标方法处理的过程中(阅读**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 里面,其中包括数据和视图地址

  2. 方法的参数是一个自定义类型对象【user】(从请求参数中确定的),把他也放在 ModelAndViewContainer

  3. 任何目标方法执行完成以后都会返回ModelAndView(数据和视图地址), 使用反射机制mav = invokeHandlerMethod(request, response, handlerMethod) 执行的目标方法。

  4. 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);
    			}
    		}
    	}
    
  5. processDispatchResult() 处理派发结果(页面如何响应)

  • render(mv, request, response); 进行页面渲染逻辑

  • 根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑

    • 所有的**视图解析器(ViewResolver)**尝试是否能根据当前返回值得到View对象
    • 得到了 redirect:/main.html --> Thymeleaf 创建了 RedirectView()
  1. ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。

  2. 视图对象view调用自己的render进行页面渲染工作 —>view.render(mv.getModelInternal(), request, response);

  • RedirectView 如何渲染【重定向到一个页面】
    1. 获取目标url地址
    2. 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); //页面渲染,主要的方法是---> renderMergedOutputModel
			if (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 forward
			return;
		}
		if (mappedHandler != null) {
			// Exception (if any) is already handled..
			mappedHandler.triggerAfterCompletion(request, response, null);
		}
	}

AbstractView中的render方法逻辑

	@Override
	public 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的方法

视图解析

  1. 返回值以 forward: 开始new InternalResourceView(forwardUrl); --> 转发request.getRequestDispatcher(path).forward(request, response);
  2. 返回值以 redirect: 开始new RedirectView() --> render就是重定向
  3. 返回值是普通字符串new ThymeleafView()—> 模板引擎调用process方法,以IO流写出

自定义视图+自定义解析器可以返回你想要的更多视图:如excel,word等等。

重定向携带数据

可以将当前页面的参数通过 RedirectAttributes 传输到重定向的页面,即在重定向的页面中可以拿到数据


阅读源码:最好自己在IDE,打断点,且Debug模式运行实例,这样比较没那么沉闷。

48、拦截器-登录检查与静态资源放行

小提示:如果同时设置了 监听器,过滤器,拦截器他们的启动生效顺序是:监听器-->过滤器-->拦截器

  1. 编写一个拦截器实现HandlerInterceptor接口

  2. 拦截器注册到容器中(实现WebMvcConfigurer的**addInterceptors()**)

  3. 指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】

编写一个实现HandlerInterceptor接口的拦截器:

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 目标方法执行之前 ---> 一般用于登录拦截,身份验证
     */
    @Override
    public 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;
    }

    /**
     * 目标方法执行完成以后  --
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle执行{}",modelAndView);
    }

    /**
     * 页面渲染以后
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion执行异常{}",ex);
    }
}

拦截器注册到容器中 && 指定拦截规则:

// 配置好拦截器如何拦截之后,需要在这里注册拦截器,让他生效(以前springmvc是在配置文件中写),拦截那些请求,放行那些请求
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{
    @Override
    public 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、拦截器-【源码分析】-拦截器的执行时机和原理

拦截器底层生效的流程顺序

  1. 根据当前请求,找到HandlerExecutionChain 处理器链
  2. 先来顺序执行 所有拦截器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)) { // 如果出现了一个为false
    				triggerAfterCompletion(request, response, null);//倒序执行已经执行过preHandle方法的拦截的afterCompletion方法
    				return false;
    			}
    			this.interceptorIndex = i;
    		}
    		return true;
    	}
    
  1. 如果任何一个拦截器返回false,直接跳出不执行目标方法。
  2. 所有拦截器都返回true,才执行目标方法。
  3. 然后倒序执行所有拦截器的postHandle()方法。
  4. 前面的步骤有任何异常都会直接倒序触发 afterCompletion()
  5. 页面成功渲染完成以后,也会倒序触发 afterCompletion()
  6. 已经执行的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、文件上传-【源码流程】文件上传参数解析器

  1. 文件上传相关的自动配置类MultipartAutoConfiguration
  2. 自动配置类中有创建文件上传参数解析器StandardServletMultipartResolver

原理步骤

  1. 请求进来使用文件上传解析器判断 isMultipart并封装 resolveMultipart,返回 MultipartHttpServletRequest (文件上传请求)

  2. 参数解析器来解析请求中的文件内容封装成 MultipartFiles

  3. 将request请求中的信息,封装成一个 LinkedMultiValueMap<String,MultipartFile> 的map集合

  4. 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;
	}

	@Override
	public boolean isMultipart(HttpServletRequest request) {
		return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
	}

	@Override
	public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
		return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
	}

	@Override
	public 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 {
    
    @Nullable
	private 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 AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {
	@Override
	protected ModelAndView handleInternal(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
		ModelAndView mav;
		...
            // 执行目标方法
		mav = invokeHandlerMethod(request, response, handlerMethod);
        ...
		return mav;
	}
    
    @Nullable
	protected 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);
		...
	}
    
	@Nullable
	public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {
		//方法2
		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
		...
            //方法1
		return doInvoke(args);//反射调用
	}
    //方法1
	@Nullable
	protected 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;
			}
            //关注点1
			if (!this.resolvers.supportsParameter(parameter)) {
				throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
			}
			try {
                //关注点2
				args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
			}
			catch (Exception ex) {
				...
			}
		}
		return args;
	}
    
}

RequestPartMethodArgumentResolver 接收文件上传参数的解析器

public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {

    //对应上面代码关注点1
    @Override
	public 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
	@Nullable
	public 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、错误处理-【源码分析】底层组件功能分析

  1. ErrorMvcAutoConfiguration 自动配置异常处理规则 重要组件 5个

    • 给容器中的加入组件:类型:DefaultErrorAttributes -> **id:errorAttributes ** 😉==管数据==

      1. public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
      2. 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对象。

  2. 容器中的组件:类型: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);
      }
      
  3. 如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面(主要作用)。

  • error/404、5xx.html (拼接视图名)
  • 如果想要返回页面,就会找error视图(StaticView默认是一个白页)。

54、错误处理-【源码流程】异常处理流程

譬如写一个会抛出异常的控制层:

@Slf4j
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String handle01(){
        int i = 1 / 0;//将会抛出ArithmeticException
        log.info("Hello, Spring Boot 2!");
        return "Hello, Spring Boot 2!";
    }
}

当浏览器发出/hello请求,DispatcherServletdoDispatch()mv = ha.handle(processedRequest, response, mappedHandler.getHandler());将会抛出ArithmeticException

异常处理步骤流程

  1. 执行目标方法,目标方法运行期间有任何异常都会被catch,而且标志当前请求结束;捕捉异常封装给dispatchException(用来给视图提供信息)

  2. 捕捉后,进入视图解析流程 (页面渲染)
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); mv是空的,因为执行异常

    • 处理handler发生的异常mv=processHandlerException(request, response, handler, exception)

    • 遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】

      // 接口只有一个方法,处理异常
      @Nullable
      	ModelAndView 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 {
    		...
                	//将会抛出ArithmeticException
    				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    				applyDefaultViewName(processedRequest, mv);
    				mappedHandler.applyPostHandle(processedRequest, response, mv);
    			}
    			catch (Exception ex) {
                    //将会捕捉ArithmeticException
    				dispatchException = 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 {
    ...
        //处理异常的方法 来自 HandlerExceptionResolver
    public 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());
        // 解析异常视图,返回mv
       ModelAndView modelAndView = resolveErrorView(request, response, status, model);
       //如果/template/error内没有4**.html或5**.html,
       //modelAndView为空,最终还是返回viewName为error的modelAndView
       return (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的Bean
		private 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
		@ConditionalOnMissingBean
		public 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);

		@Override
		public 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;
		}

		@Override
		public String getContentType() {
			return "text/html";
		}

	}
}

55、错误处理-【源码流程】几种异常处理原理

几种处理错误方式的操作

  1. 自定义错误页
  • error/404.html error/5xx.html;有精确的错误状态码页面 就匹配精确
  • ,没有就找 4xx.html;
  • 模糊匹配的前缀状态有五个(1,2,3,4,5),由底层一个枚举类封装起来
  • 如果都没有就触发白页
  1. @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的。 全局异常处理器的实现

    @Slf4j
    @ControllerAdvice
    public class GlobalExceptionHandler {
        @ExceptionHandler({ArithmeticException.class,NullPointerException.class})  //捕获处理对应异常
        public String handleArithException(Exception e){
            log.error("异常是:{}",e);
            return "login"; //视图地址
        }
    }
    
  2. @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.MissingServletRequestParameterExceptionDefaultHandlerExceptionResolver 处理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 {
    @Override
    public 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 {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("66666");
    }
}

Filter

@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"}) // 拦截路径,做过滤工作
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("MyFilter初始化完成");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 核心功能,过滤一些请求(网络上过滤掉恶意或无用的请求)
        log.info("MyFilter工作");
        chain.doFilter(request,response);
    }

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

Listener

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

    @Override
    public 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 {
    @Bean
    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet,"/my","/my02");//  指定处理路径
    }

    @Bean
    public 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;
    }

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

注册的Servlet组件管理的/my请求 为什么没有被拦截器拦截,直接就可以访问?

扩展:DispatchServlet是如何注册到容器中的?

  1. 容器中自动配置了 DispatchServlet属性绑定到 WebMvcProperties类上,对应的配置文件前缀是 : spring.mvc

  2. 老版本:通过 继承 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--->  "/"
    		}
    
  3. 默认的映射路径是 “/”

解释问题:

如果有多个Servlet能处理同一个路径,优先选择最精确的匹配 “/my”,而"/" 是优先级最低的匹配

既然都没有被dispatchServlet处理,那么配置在这上面的拦截也当然不管用

58、嵌入式Servlet容器-【源码分析】切换web服务器与定制化

TODO:迷糊一点,再一遍

  • 默认支持的WebServer

    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器。
  • 原理

    • SpringBoot应用启动发现当前是Web应用,web场景包-导入tomcat。

    • web应用会创建一个web版的IOC容器ServletWebServerApplicationContext

    • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory 来自方法 createWebServer中的getServerWebFactoryServlet 的web服务器工厂—produce—>Servlet 的web服务器)。

    • 底层直接会有一个对应的自动配置类**ServletWebServerFactoryAutoConfiguration**。

    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)

      • SpringBoot底层默认有很多的WebServer工厂ServletWebServerFactoryConfiguration内创建Bean),如:
        • TomcatServletWebServerFactory 这个工厂生产还有对应的条件(扫描是否有Servlet,Tomcat)
        • JettyServletWebServerFactory
        • UndertowServletWebServerFactory
      • ServletWebServerFactoryConfiguration 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
    • 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接口
      @Override
      	public 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包存在)。

    切换服务器

    1. 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>
      
    2. 但是建议使用tomcat,因为熟悉

定制Servlet容器

第一种方法更加常用

  1. 修改配置文件 server.xxx

  2. 直接自定义 ConfigurableServletWebServerFactory

  3. 实现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> {
        @Override
        public void customize(ConfigurableServletWebServerFactory server) {
            server.setPort(9000);
        }
    }
    
  4. 实现WebServerFactoryCustomizer<TomcatServletWebServerFactory> 跟第三种方法本质是一样的,是ConfigurableServletWebServerFactory的特别的定制化处理。这样相同的还有JettyServletWebServerFactory and UndertowServletWebServerFactory

59、定制化原理-SpringBoot定制化组件的几种方式(小结)

定制化的常见方式

Four Kinds

  1. 修改配置文件

  2. xxxxxCustomizer 定制化器,后置修改(类似配置文件)

    1. 编写from movies,boxoffice where movies.id=boxoffice.movie_id; xxxConfiguration + @Bean替换、增加容器中默认组件webMvcConfigurer,视图解析器
  3. Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件

    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer{
    }
    
  4. @Configuration+@EnableWebMvc注解 + 实现WebMvcConfigurer@Bean 可以全面接管SpringMVC,所有规则全部自己重新配置
    实现定制和扩展功能(高级功能,初学者退避三舍)。

    • 原理:
      1. WebMvcAutoConfiguration默认的SpringMVC的自动配置功能类,如静态资源、欢迎页等。
      2. 一旦使用 @EnableWebMvc ,会**@Import(DelegatingWebMvcConfiguration.class)** —> 关闭自动配置的效果,只留有springMVC最基本的功能
      3. DelegatingWebMvcConfiguration的作用,只保证SpringMVC最基本的使用
        • 把所有系统中的WebMvcConfigurer拿过来,所有功能的定制(资源绑定,拦截器设定…)都是这些WebMvcConfigurer合起来一起生效。 批量处理的configurer的工具类是 WebMvcConfigurerComposite
        • 自动配置了一些非常底层的组件,如RequestMappingHandlerMapping,这些组件依赖的组件都是从容器中获取的。
        • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
      4. WebMvcAutoConfiguration里面的配置要能生效必须有个条件 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)WebMvcConfigurationSupportDelegatingWebMvcConfiguration的父类
      5. 所以,@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,我们也可以使用其他的连接池,常用的是德鲁伊。
  • DataSourceTransactionManagerAutoConfiguration事务管理器的自动配置

  • JdbcTemplateAutoConfigurationJdbcTemplate的自动配置,可以来对数据库进行CRUD

    • 可以修改前缀为spring.jdbc的配置项来修改JdbcTemplate
    • @Bean @Primary JdbcTemplate:Spring容器中有这个JdbcTemplate组件,使用@Autowired即可从容器拿到
  • JndiDataSourceAutoConfigurationJNDI的自动配置

  • XADataSourceAutoConfiguration分布式事务相关的

修改配置项

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1024
    url: 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 {

    @Autowired
    JdbcTemplate 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
     */
    @Bean
    public 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应用。
     */
    @Bean
    public 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>

分析依赖的自动配置

  1. 扩展配置项 spring.datasource.druid 数据源配置项 spring.datasource (也和druid默认绑定起了)

  2. 自动配置类DruidDataSourceAutoConfigure

    • 需要有DruidDataSource :@ConditionalOnClass({DruidDataSource.class})
    • 在官方的数据源自动配置前先配置下来@AutoConfigureBefore({DataSourceAutoConfiguration.class}),因为一个web服务只需要一个数据源,而装配数据源时,会先检查当前容器中是否已经有了数据源
    • @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class}) 绑定属性类
  3. 另外导入的类 Import

    1. DruidSpringAopConfiguration.class, 监控SpringBean(spring组件)的 配置项前缀:spring.datasource.druid.aop-patterns
    2. DruidStatViewServletConfiguration.class, 监控页的配置spring.datasource.druid.stat-view-servlet默认开启。
    3. DruidWebStatFilterConfiguration.classweb监控配置spring.datasource.druid.web-stat-filter默认开启。
    4. DruidFilterConfiguration.class所有Druid的filter的配置
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";

配置示例

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

    druid: # 德鲁伊配置在datasource下一层
      aop-patterns: com.morSun.*  #监控SpringBean的位置
      filters: stat,wall     # 底层开启功能,stat(sql监控),wall(防火墙)

      stat-view-servlet:   # 配置监控页功能
        enabled: true
        login-username: admin
        login-password: admin
        resetEnable: false
        UrlMappings: '/druid/*'  #指定监控servlet映射路径,默认拦截所有

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

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

63、数据访问-整合MyBatis-配置版

MyBatis的GitHub仓库

MyBatis官方

starter的命名方式

  1. SpringBoot官方的Starter:spring-boot-starter-*
  2. 第三方的: *-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: root
    password: 1234
    url: jdbc:mysql://localhost:3306/my
    driver-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 configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 由于Spring Boot自动配置缘故,此处不必配置,只用来做做样。-->
</configuration>

Mapper接口

SQL映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.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 {

    @Autowired
    private UserService userService;

    @ResponseBody
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable("id") Integer id){

        return userService.getUser(id);
    }

}
@Service
public class UserService {

    @Autowired
    private 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.xml
  configuration:
    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 mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.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提供的IServiceServiceImpl,减轻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 {
    /**
     * MybatisPlusInterceptor
     todo: 分析其实现原理   
     * @return
     */
    @Bean
    public 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>

  1. #numbers表示methods for formatting numeric objects.使用说明文档
    • 这是thymeleaf的内置对象发挥的作用,**sequence(from,to)**方法的作用是 生成一个由from到to的序列 ,此处是遍历查出的表单数据页
  2. <a th:href="@{/dynamic_table(pn=${num})}">[[${num}]]</a> 这里给请求路径带?参数的写法在thymeleaf中规定,应该将参数用括号括起来
  3. <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";
}

如何进行分页查询?

  1. 配置好数据库连接,建好三层架构

  2. 配置好分页插件:用处后面toseeing….

        @Bean 1.// 分页插件注入,主要的作用是做一些拦截的工作
        public MybatisPlusInterceptor paginationInterceptor() {
            MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();//定义分页拦截器
            PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
                    // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
            paginationInnerInterceptor.setOverflow(true);
                    // 设置最大单页限制数量,默认 500 条,-1 不受限制
            paginationInnerInterceptor.setMaxLimit(500L);
            //注册拦截器
            mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
            return mybatisPlusInterceptor;
        }
    
  3. 使用Page封装返回对象,Page对象中的属性对于前端分页完全足够

  4. Page对象创建应该传入,当前页码和一页应该分多少条数

重定向参数RedirectAttributes的使用

ra.addAttribute(“pn”,pn);

  1. 重定向参数的使用 ,将当前request请求的数据放在里面,重定向后仍然可以拿到(自动添加到url)
  2. 比如重定向的路径是:/demo 加上重定向参数就会变成 :/demo?pn=xx
  3. 所以 /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注解导入连接工厂LettuceConnectionConfigurationJedisConnectionConfiguration是准备好的。

    1. LettuceConnectionConfiguration 条件是如果检测到为RedisClient客户端:自动导入默认客户端资源 DefaultClientResources 以及Lettuce连接工厂 LettuceConnectionFactory
    2. JedisConnectionConfiguration需要导入Jedis依赖,而且没有配置RedisConnectionFactory 就会创建Jedis连接
  • ❗自动注入了**RedisTemplate<Object, Object>**,xxxTemplate操作redis 的工具

  • ❗自动注入了StringRedisTemplatekey,value都是String

  • 底层只要我们使用StringRedisTemplateRedisTemplate就可以操作Redis。

外网Redis环境搭建

  1. 阿里云按量付费Redis,其中选择经典网络

    • 阿里云中购买,选择 云数据库Redis版 <选择详情如下>

    • 购买完成后,前往控制端

  2. 申请Redis的公网连接地址。
    image-20221130114623531

  3. 修改白名单,允许0.0.0.0/0访问。(所有人都可以访问)

  4. 登录方式

  5. Redis Desktop Manager测试一下
    连接公网地址
    密码 账号:密码

  6. 最后使用完记得释放,否则一直扣钱

70、数据访问-Redis操作与统计小实验

相关Redis配置:

spring:
  redis:
#   url: redis://wang:wang!123@r-wz98rtfw492qqex6cipd.redis.rds.aliyuncs.com:6379
    host: r-wz98rtfw492qqex6cipd.redis.rds.aliyuncs.com
    port: 6379
    password: wang:wang!123
# 指定客户端的类型是jedis
    client-type: jedis
    jedis:
      pool:
        max-active: 10
#   lettuce:# 另一个用来连接redis的java框架
#      pool:
#        max-active: 10
#        min-idle: 5

测试Redis连接:

@SpringBootTest
public class Boot05WebAdminApplicationTests {
// 操作redis的工具类
    @Autowired
    StringRedisTemplate redisTemplate;
        @Autowired
        RedisConnectionFactory redisConnectionFactory;

    @Test
    void 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 {

    @Autowired
    StringRedisTemplate redisTemplate; // 操作redis

    @Override
    public 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;


    @Override
    public 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中拿出来,传递到主页

	@Autowired
    StringRedisTemplate 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 {
    @Autowired
    private 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:表示在所有单元测试之后执行。

    • BeforeAllAfterAll 标注的方法必须是静态方法,因为它只执行一次
  • @Tag表示单元测试类别,类似于JUnit4中的@Categories。

  • @Disabled:表示测试类或测试方法不执行,类似于JUnit4中的==@Ignore==。

  • @Timeout:表示测试方法运行如果超过了指定时间将会返回错误

  • @ExtendWith为测试类或测试方法提供扩展类引用,提供一些额外的功能

import org.junit.jupiter.api.*;

@DisplayName("junit5功能测试类")
public class Junit5Test {


    @DisplayName("测试displayname注解")
    @Test
    void 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")
    @Test
    void test2() {
        System.out.println(2);
    }

    @RepeatedTest(5) // 可重复执行5次
    @Test
    void test3() {
        System.out.println(5);
    }

    /**
     * 规定方法超时时间。超出时间测试出异常
     * @throws InterruptedException
     */
    @Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
    @Test
    void testTimeout() throws InterruptedException {
        Thread.sleep(600);
    }

    @BeforeEach
    void testBeforeEach() {
        System.out.println("测试就要开始了...");
    }

    @AfterEach
    void testAfterEach() {
        System.out.println("测试结束了...");
    }

    @BeforeAll
    static void testBeforeAll() {
        System.out.println("所有测试就要开始了...");
    }

    @AfterAll
    static 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里面的多个断言必须全部成功,才能返回成功true
 assertAll("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") // 可执行对象
        );
    }
}

assumeTrueassumFalse 确保给定的条件为 truefalse,不满足条件会使得测试执行终止。

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 {
        @BeforeEach
        void createNewStack() {
            stack = new Stack<>(); // 影响不到外层测试
        }
        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }
        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, stack::pop);
        }
        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, stack::peek);
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {
            String anElement = "an element";
		
            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }
            // 内层的测试可以驱动外层的测试(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

在进行迁移的时候需要注意如下的变化:

  1. 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
  2. @Before@After 替换成@BeforeEach@AfterEach
  3. @BeforeClass@AfterClass 替换成@BeforeAll 和@AfterAll。
  4. @Ignore 替换成@Disabled
  5. @Category 替换成@Tag
  6. @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.namelogging.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: false
  endpoint:
    beans:
      enabled: true
    health:
      enabled: true
暴露Endpoints

支持的暴露方式

  • HTTP:默认只暴露health和info。
  • JMX:默认暴露所有Endpoint。
  • 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入Spring Security,则会默认配置安全访问规则。 todo:未理解
IDJMXWeb
auditeventsYesNo
beansYesNo
cachesYesNo
conditionsYesNo
configpropsYesNo
envYesNo
flywayYesNo
healthYesYes
heapdumpN/ANo
httptraceYesNo
infoYesYes
integrationgraphYesNo
jolokiaN/ANo
logfileN/ANo
loggersYesNo
liquibaseYesNo
metricsYesNo
mappingsYesNo
prometheusN/ANo
scheduledtasksYesNo
sessionsYesNo
shutdownYesNo
startupYesNo
threaddumpYesNo

若要更改暴露的Endpoint,请配置以下的包含和排除属性:

PropertyDefault
management.endpoints.jmx.exposure.exclude
management.endpoints.jmx.exposure.include*
management.endpoints.web.exposure.exclude
management.endpoints.web.exposure.includeinfo, health

官方文档 - Exposing Endpoints

79、指标监控-定制Endpoint

定制 Health 信息

两种写法都可以参考一下

management:
    health:
      enabled: true
      show-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 {
    @Override
    public Health health() {
        int errorCode = check(); // perform some specific health check
        if (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
     */
    @Override
    protected 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.1
  mavenProjectName: @project.artifactId@  #使用@@可以获取maven的pom文件值
  mavenProjectVersion: @project.version@
  • 编写InfoContributor
import java.util.Collections;

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

@Component
public class ExampleInfoContributor implements InfoContributor {

    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("example", // 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}.yamlenv通常替代为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: lun
  age: 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这个组件
    @Bean
    public Color red(){
        return new Color();
    }

    @Profile("test")//只有在当前test环境下,才会有green这个组件
    @Bean
    public Color green(){
        return new Color();
    }
}

可以激活一组:主要是为了防止所有配置全在一个配置文件中显得冗余且难查阅 [可以使用]

spring.profiles.active=production

spring.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})
    • 命令行参数 最后修改属性的一步,起决定性作用
  • 配置文件查找位置
    1. classpath 根路径。❤
    2. classpath 根路径下config目录。❤
    3. jar包当前目录。<创建一个yaml放在jar当前目录>❤
    4. jar包当前目录的config目录下。
    5. jar包当前目录的 /config子目录的直接子目录。🤍
  • 配置文件加载顺序:
    1. 当前jar包内部的application.propertiesapplication.yml
    2. 当前jar包内部的application-{profile}.propertiesapplication-{profile}.yml
    3. 引用的外部jar包application.propertiesapplication.yml
    4. 引用的外部jar包的application-{profile}.propertiesapplication-{profile}.yml
    5. 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项。(越后加载越优先)

83、高级特性-自定义starter依赖细节

starter启动原理

  • starter的pom.xml引入autoconfigure依赖
starter
autoconfigure
spring-boot-starter
  • autoconfigure包中配置使用META-INF/spring.factoriesEnableAutoConfiguration的值,使得项目启动加载指定的自动配置类

  • 编写自动配置类 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="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
<!--以后用来导入的依赖,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="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.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)// 保证唯一性
    @Bean
    public 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 {
    @Autowired
    private 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="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.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 {

    @Autowired
    private HelloService helloService; /// 直接拿到容器中的service,执行sayhello方法

    @Test
    void 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找 ApplicationContextInitializer
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		
        4.//去spring.factories找 ApplicationListener
        setListeners((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.到最后该方法会返回这context
        ConfigurableApplicationContext 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返回或创建基础的环境信息 :standardServletEnvironment
            7.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所以当前会创建 AnnotationConfigServletWebServerApplicationContext
			context = 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默认是true
				new 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默认为true
		System.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 找 SpringApplicationRunListener
        return 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, StandardReactiveWebEnvironment
		ConfigurableEnvironment 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  通知所有的监听器contextPrepared
		listeners.contextPrepared(context);
		bootstrapContext.close(context);
		if (this.logStartupInfo) {
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);
		}
		// Add boot specific singleton beans
		ConfigurableListableBeanFactory 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 sources
		Set<Object> sources = getAllSources();
		Assert.notEmpty(sources, "Sources must not be empty");
		load(context, sources.toArray(new Object[0]));
        //8.2
		listeners.contextLoaded(context);
	}

    //12.调用所有runners
    private void callRunners(ApplicationContext context, ApplicationArguments args) {
		List<Object> runners = new ArrayList<>();
       
        //获取容器中的 ApplicationRunner
		runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
		//获取容器中的  CommandLineRunner
        runners.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.1
    void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
		doWithListeners("spring.boot.application.environment-prepared",
				(listener) -> listener.environmentPrepared(bootstrapContext, environment));
	}
    
    //8.1
    void contextPrepared(ConfigurableApplicationContext context) {
		doWithListeners("spring.boot.application.context-prepared", (listener) -> listener.contextPrepared(context));
	}
    
    //8.2
    void 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 {
    @Override
    public 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 {
    @Override
    public 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 {
    @Override
    public 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 {
    @Override
    public 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;
    }

    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        System.out.println("MySpringApplicationRunListener....starting....");

    }


    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        System.out.println("MySpringApplicationRunListener....environmentPrepared....");
    }


    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println("MySpringApplicationRunListener....contextPrepared....");

    }

    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {
        System.out.println("MySpringApplicationRunListener....contextLoaded....");
    }

    @Override
    public void started(ConfigurableApplicationContext context) {
        System.out.println("MySpringApplicationRunListener....started....");
    }
/*现在的SpringApplicationRunListener接口已经没有这个方法了,进而改变成了ready方法*/ 
    @Override
    public void running(ConfigurableApplicationContext context) {
        System.out.println("MySpringApplicationRunListener....running....");
    }

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {
        System.out.println("MySpringApplicationRunListener....failed....");
    }
}

😀注册MyApplicationContextInitializerMyApplicationListenerMySpringApplicationRunListener:

resources / META-INF / spring.factories:

org.springframework.context.ApplicationContextInitializer=\
  com.lun.boot.listener.MyApplicationContextInitializer

org.springframework.context.ApplicationListener=\
  com.lun.boot.listener.MyApplicationListener

org.springframework.boot.SpringApplicationRunListener=\
  com.lun.boot.listener.MySpringApplicationRunListener

😀**ApplicationRunnerCommandLineRunner会从容器中拿取,所以只将他们注入容器即可**

🤣 根据上面的组件来理解springboot的启动流程

87、后会有期

路漫漫其修远兮,吾将上下而求索。

纸上得来终觉浅,绝知此事要躬行。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值