René's URL Explorer Experiment


Title: No title

direct link

Domain: github.com


Hey, it has address:
addresses; } ``` * POJO 中出现 Map 保存对象数据,参数名称与对象层次结构名称保持一致,使用映射格式描述集合中对象位置 URL: http://localhost/requestParam8?addressMap[’home’].province=bj&addressMap[’job’].province=tj ```java @RequestMapping("/requestParam8") public String requestParam8(User user){ System.out.println("user.addressMap=" + user.getAddressMap()); //user.addressMap={home=Address{p=,c=,a=},job=Address{....}} return "page.jsp"; } ``` ```java public class User { private Map addressMap; //.... } ``` *** #### 数组集合 ##### 数组类型 请求参数名与处理器方法形参名保持一致,且请求参数数量> 1个 * 访问 URL: http://localhost/requestParam9?nick=Jockme&nick=zahc ```java @RequestMapping("/requestParam9") public String requestParam9(String[] nick){ System.out.println(nick[0] + "," + nick[1]); return "page.jsp"; } ``` *** ##### 集合类型 保存简单类型数据,请求参数名与处理器方法形参名保持一致,且请求参数数量> 1个 * 访问 URL: http://localhost/requestParam10?nick=Jockme&nick=zahc ```java @RequestMapping("/requestParam10") public String requestParam10(@RequestParam("nick") List nick){ System.out.println(nick); return "page.jsp"; } ``` * 注意: SpringMVC 默认将 List 作为对象处理,赋值前先创建对象,然后将 nick **作为对象的属性**进行处理。List 是接口无法创建对象,报无法找到构造方法异常;修复类型为可创建对象的 ArrayList 类型后,对象可以创建但没有 nick 属性,因此数据为空 解决方法:需要告知 SpringMVC 的处理器 nick 是一组数据,而不是一个单一属性。通过 @RequestParam 注解,将数量大于 1 个 names 参数打包成参数数组后, SpringMVC 才能识别该数据格式,并判定形参类型是否为数组或集合,并按数组或集合对象的形式操作数据 *** #### 转换器 ##### 类型 开启转换配置:` ` 作用:提供 Controller 请求转发,Json 自动转换等功能 如果访问 URL:http://localhost/requestParam1?name=seazean&age=seazean,会出现报错,类型转化异常 ```java @RequestMapping("/requestParam1") public String requestParam1(String name ,int age){ System.out.println("name=" + name + ",age=" + age); return "page.jsp"; } ``` SpringMVC 对接收的数据进行自动类型转换,该工作通过 Converter 接口实现: * 标量转换器 * 集合、数组相关转换器 * 默认转换器 *** ##### 日期 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-date数据类型转换.png) 如果访问 URL:http://localhost/requestParam11?date=1999-09-09 会报错,所以需要日期类型转换 * 声明自定义的转换格式并覆盖系统转换格式,配置 resources / spring-mvc.xml ```xml ``` * @DateTimeFormat 类型:形参注解、成员变量注解 位置:形参前面 或 成员变量上方 作用:为当前参数或变量指定类型转换规则 ```java public String requestParam12(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date){ System.out.println("date=" + date); return "page.jsp"; } ``` ```java @DateTimeFormat(pattern = "yyyy-MM-dd") private Date date; ``` 依赖注解驱动支持,xml 开启配置: ```xml ``` *** ##### 自定义 自定义类型转换器,实现 Converter 接口或者直接容器中注入: * 方式一: ```java public class WebConfig implements WebMvcConfigurer { @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter() { @Override public Pet convert(String source) { DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date date = null; //类型转换器无法预计使用过程中出现的异常,因此必须在类型转换器内部捕获, //不允许抛出,框架无法预计此类异常如何处理 try { date = df.parse(source); } catch (ParseException e) { e.printStackTrace(); } return date; } }); } } } * 方式二: ```java //本例中的泛型填写的是String,Date,最终出现字符串转日期时,该类型转换器生效 public class MyDateConverter implements Converter { //重写接口的抽象方法,参数由泛型决定 public Date convert(String source) { DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); Date date = null; //类型转换器无法预计使用过程中出现的异常,因此必须在类型转换器内部捕获, //不允许抛出,框架无法预计此类异常如何处理 try { date = df.parse(source); } catch (ParseException e) { e.printStackTrace(); } return date; } } ``` 配置 resources / spring-mvc.xml,注册自定义转换器,将功能加入到 SpringMVC 转换服务 ConverterService 中 ```xml ``` * 使用转换器 ```java @RequestMapping("/requestParam12") public String requestParam12(Date date){ System.out.println(date); return "page.jsp"; } ``` *** ### 响应处理 #### 页面跳转 请求转发和重定向: * 请求转发: ```java @Controller public class UserController { @RequestMapping("/showPage1") public String showPage1() { System.out.println("user mvc controller is running ..."); return "forward:/WEB-INF/page/page.jsp; } } ``` * 请求重定向: ```java @RequestMapping("/showPage2") public String showPage2() { System.out.println("user mvc controller is running ..."); return "redirect:/WEB-INF/page/page.jsp";//不能访问WEB-INF下的资源 } ``` 页面访问快捷设定(InternalResourceViewResolver): * 展示页面的保存位置通常固定且结构相似,可以设定通用的访问路径简化页面配置,配置 spring-mvc.xml: ```xml ``` * 简化 ```java @RequestMapping("/showPage3") public String showPage3() { System.out.println("user mvc controller is running..."); return "page"; } @RequestMapping("/showPage4") public String showPage4() { System.out.println("user mvc controller is running..."); return "forward:page"; } @RequestMapping("/showPage5") public String showPage5() { System.out.println("user mvc controller is running..."); return "redirect:page"; } ``` * 如果未设定了返回值,使用 void 类型,则默认使用访问路径作页面地址的前缀后缀 ```java //最简页面配置方式,使用访问路径作为页面名称,省略返回值 @RequestMapping("/showPage6") public void showPage6() { System.out.println("user mvc controller is running ..."); } ``` *** #### 数据跳转 ModelAndView 是 SpringMVC 提供的一个对象,该对象可以用作控制器方法的返回值(Model 同),实现携带数据跳转 作用: + 设置数据,向请求域对象中存储数据 + 设置视图,逻辑视图 代码实现: * 使用 HttpServletRequest 类型形参进行数据传递 ```java @Controller public class BookController { @RequestMapping("/showPageAndData1") public String showPageAndData1(HttpServletRequest request) { request.setAttribute("name","seazean"); return "page"; } } ``` * 使用 Model 类型形参进行数据传递 ```java @RequestMapping("/showPageAndData2") public String showPageAndData2(Model model) { model.addAttribute("name","seazean"); Book book = new Book(); book.setName("SpringMVC入门实战"); book.setPrice(66.6d); //添加数据的方式,key对value model.addAttribute("book",book); return "page"; } ``` ```java public class Book { private String name; private Double price; } ``` * 使用 ModelAndView 类型形参进行数据传递,将该对象作为返回值传递给调用者 ```java @RequestMapping("/showPageAndData3") public ModelAndView showPageAndData3(ModelAndView modelAndView) { //ModelAndView mav = new ModelAndView(); 替换形参中的参数 Book book = new Book(); book.setName("SpringMVC入门案例"); book.setPrice(66.66d); //添加数据的方式,key对value modelAndView.addObject("book",book); modelAndView.addObject("name","Jockme"); //设置页面的方式,该方法最后一次执行的结果生效 modelAndView.setViewName("page"); //返回值设定成ModelAndView对象 return modelAndView; } ``` * ModelAndView 扩展 ```java //ModelAndView对象支持转发的手工设定,该设定不会启用前缀后缀的页面拼接格式 @RequestMapping("/showPageAndData4") public ModelAndView showPageAndData4(ModelAndView modelAndView) { modelAndView.setViewName("forward:/WEB-INF/page/page.jsp"); return modelAndView; } //ModelAndView对象支持重定向的手工设定,该设定不会启用前缀后缀的页面拼接格式 @RequestMapping("/showPageAndData5") public ModelAndView showPageAndData6(ModelAndView modelAndView) { modelAndView.setViewName("redirect:page.jsp"); return modelAndView; } ``` *** #### JSON 注解:@ResponseBody 作用:将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到 Response 的 body 区。如果返回值是字符串,那么直接将字符串返回客户端;如果是一个对象,会**将对象转化为 JSON**,返回客户端 注意:当方法上面没有写 ResponseBody,底层会将方法的返回值封装为 ModelAndView 对象 * 使用 HttpServletResponse 对象响应数据 ```java @Controller public class AccountController { @RequestMapping("/showData1") public void showData1(HttpServletResponse response) throws IOException { response.getWriter().write("message"); } } ``` * 使用 **@ResponseBody 将返回的结果作为响应内容**(页面显示),而非响应的页面名称 ```java @RequestMapping("/showData2") @ResponseBody public String showData2(){ return "{'name':'Jock'}"; } ``` * 使用 jackson 进行 json 数据格式转化 导入坐标: ```xml com.fasterxml.jackson.core jackson-core 2.9.0 com.fasterxml.jackson.core jackson-databind 2.9.0 com.fasterxml.jackson.core jackson-annotations 2.9.0 ``` ```java @RequestMapping("/showData3") @ResponseBody public String showData3() throws JsonProcessingException { Book book = new Book(); book.setName("SpringMVC入门案例"); book.setPrice(66.66d); ObjectMapper om = new ObjectMapper(); return om.writeValueAsString(book); } ``` * 使用 SpringMVC 提供的消息类型转换器将对象与集合数据自动转换为 JSON 数据 ```java //使用SpringMVC注解驱动,对标注@ResponseBody注解的控制器方法进行结果转换,由于返回值为引用类型,自动调用jackson提供的类型转换器进行格式转换 @RequestMapping("/showData4") @ResponseBody public Book showData4() { Book book = new Book(); book.setName("SpringMVC入门案例"); book.setPrice(66.66d); return book; } ``` * 手工添加信息类型转换器 ```xml ``` * 转换集合类型数据 ```java @RequestMapping("/showData5") @ResponseBody public List showData5() { Book book1 = new Book(); book1.setName("SpringMVC入门案例"); book1.setPrice(66.66d); Book book2 = new Book(); book2.setName("SpringMVC入门案例"); book2.setPrice(66.66d); ArrayList al = new ArrayList(); al.add(book1); al.add(book2); return al; } ``` **** ### Restful #### 基本介绍 Rest(REpresentational State Transfer):表现层状态转化,定义了**资源在网络传输中以某种表现形式进行状态转移**,即网络资源的访问方式 * 资源:把真实的对象数据称为资源,一个资源既可以是一个集合,也可以是单个个体;每一种资源都有特定的 URI(统一资源标识符)与之对应,如果获取这个资源,访问这个 URI 就可以,比如获取特定的班级 `/class/12`;资源也可以包含子资源,比如 `/classes/classId/teachers` 某个指定班级的所有老师 * 表现形式:资源是一种信息实体,它可以有多种外在表现形式,把资源具体呈现出来的形式比如 json、xml、image、txt 等等叫做它的"表现层/表现形式" * 状态转移:描述的服务器端资源的状态,比如增删改查(通过 HTTP 动词实现)引起资源状态的改变,互联网通信协议 HTTP 协议,是一个**无状态协议**,所有的资源状态都保存在服务器端 *** #### 访问方式 Restful 是按照 Rest 风格访问网络资源 * 传统风格访问路径:http://localhost/user/get?id=1 * Rest 风格访问路径:http://localhost/user/1 优点:隐藏资源的访问行为,通过地址无法得知做的是何种操作,书写简化 Restful 请求路径简化配置方式:`@RestController = @Controller + @ResponseBody` 相关注解:@GetMapping 注解是 @RequestMapping 注解的衍生,所以效果是一样的,建议使用 @GetMapping * `@GetMapping("/poll")` = `@RequestMapping(value = "/poll",method = RequestMethod.GET)` ```java @RequestMapping(method = RequestMethod.GET) // @GetMapping 就拥有了 @RequestMapping 的功能 public @interface GetMapping { @AliasFor(annotation = RequestMapping.class) // 与 RequestMapping 相通 String name() default ""; } ``` * `@PostMapping("/push")` = `@RequestMapping(value = "/push",method = RequestMethod.POST)` 过滤器:HiddenHttpMethodFilter 是 SpringMVC 对 Restful 风格的访问支持的过滤器 代码实现: * restful.jsp: * 页面表单**使用隐藏域提交请求类型**,参数名称固定为 _method,必须配合提交类型 method=post 使用 * GET 请求通过地址栏可以发送,也可以通过设置 form 的请求方式提交 * POST 请求必须通过 form 的请求方式提交 ```html

restful风格请求表单

``` * java / controller / UserController ```java @RestController //设置rest风格的控制器 @RequestMapping("/user/") //设置公共访问路径,配合下方访问路径使用 public class UserController { @GetMapping("/user") //@RequestMapping(value = "/user",method = RequestMethod.GET) public String getUser(){ return "GET-张三"; } @PostMapping("/user") //@RequestMapping(value = "/user",method = RequestMethod.POST) public String saveUser(){ return "POST-张三"; } @PutMapping("/user") //@RequestMapping(value = "/user",method = RequestMethod.PUT) public String putUser(){ return "PUT-张三"; } @DeleteMapping("/user") //@RequestMapping(value = "/user",method = RequestMethod.DELETE) public String deleteUser(){ return "DELETE-张三"; } } ``` * 配置拦截器 web.xml ```xml HiddenHttpMethodFilter org.springframework.web.filter.HiddenHttpMethodFilter HiddenHttpMethodFilter DispatcherServlet ``` *** #### 参数注解 Restful 开发中的参数注解 ```java @GetMapping("{id}") public String getMessage(@PathVariable("id") Integer id){ } ``` 使用 @PathVariable 注解获取路径上配置的具名变量,一般在有多个参数的时候添加 其他注解: * @RequestHeader:获取请求头 * @RequestParam:获取请求参数(指问号后的参数,url?a=1&b=2) * @CookieValue:获取 Cookie 值 * @RequestAttribute:获取 request 域属性 * @RequestBody:获取请求体 [POST] * @MatrixVariable:矩阵变量 * @ModelAttribute:自定义类型变量 ```java @RestController @RequestMapping("/user/") public class UserController { //rest风格访问路径简化书写方式,配合类注解@RequestMapping使用 @RequestMapping("{id}") public String restLocation2(@PathVariable Integer id){ System.out.println("restful is running ....get:" + id); return "success.jsp"; } //@RequestMapping(value = "{id}",method = RequestMethod.GET) @GetMapping("{id}") public String get(@PathVariable Integer id){ System.out.println("restful is running ....get:" + id); return "success.jsp"; } @PostMapping("{id}") public String post(@PathVariable Integer id){ System.out.println("restful is running ....post:" + id); return "success.jsp"; } @PutMapping("{id}") public String put(@PathVariable Integer id){ System.out.println("restful is running ....put:" + id); return "success.jsp"; } @DeleteMapping("{id}") public String delete(@PathVariable Integer id){ System.out.println("restful is running ....delete:" + id); return "success.jsp"; } } ``` **** #### 识别原理 表单提交要使用 REST 时,会带上 `_method=PUT`,请求过来被 `HiddenHttpMethodFilter` 拦截,进行过滤操作 org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(): ```java public class HiddenHttpMethodFilter extends OncePerRequestFilter { // 兼容的请求 PUT、DELETE、PATCH private static final List ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name())); // 隐藏域的名字 public static final String DEFAULT_METHOD_PARAM = "_method"; private String methodParam = DEFAULT_METHOD_PARAM; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest requestToUse = request; // 请求必须是 POST, if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { // 获取标签中 name="_method" 的 value 值 String paramValue = request.getParameter(this.methodParam); if (StringUtils.hasLength(paramValue)) { // 转成大写 String method = paramValue.toUpperCase(Locale.ENGLISH); // 兼容的请求方式 if (ALLOWED_METHODS.contains(method)) { // 包装请求 requestToUse = new HttpMethodRequestWrapper(request, method); } } } // 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的 filterChain.doFilter(requestToUse, response); } } ``` Rest 使用客户端工具,如 Postman 可直接发送 put、delete 等方式请求不被过滤 改变默认的 `_method` 的方式: ```java @Configuration(proxyBeanMethods = false) public class WebConfig{ //自定义filter @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); //通过set 方法自定义 methodFilter.setMethodParam("_m"); return methodFilter; } } ``` *** ### Servlet SpringMVC 提供访问原始 Servlet 接口的功能 * SpringMVC 提供访问原始 Servlet 接口 API 的功能,通过形参声明即可 ```java @RequestMapping("/servletApi") public String servletApi(HttpServletRequest request, HttpServletResponse response, HttpSession session){ System.out.println(request); System.out.println(response); System.out.println(session); request.setAttribute("name","seazean"); System.out.println(request.getAttribute("name")); return "page.jsp"; } ``` * Head 数据获取快捷操作方式 名称:@RequestHeader 类型:形参注解 位置:处理器类中的方法形参前方 作用:绑定请求头数据与对应处理方法形参间的关系 范例: ```java 快捷操作方式@RequestMapping("/headApi") public String headApi(@RequestHeader("Accept-Language") String headMsg){ System.out.println(headMsg); return "page"; } ``` * Cookie 数据获取快捷操作方式 名称:@CookieValue 类型:形参注解 位置:处理器类中的方法形参前方 作用:绑定请求 Cookie 数据与对应处理方法形参间的关系 范例: ```java @RequestMapping("/cookieApi") public String cookieApi(@CookieValue("JSESSIONID") String jsessionid){ System.out.println(jsessionid); return "page"; } ``` * Session 数据获取 名称:@SessionAttribute 类型:形参注解 位置:处理器类中的方法形参前方 作用:绑定请求Session数据与对应处理方法形参间的关系 范例: ```java @RequestMapping("/sessionApi") public String sessionApi(@SessionAttribute("name") String name){ System.out.println(name); return "page.jsp"; } //用于在session中放入数据 @RequestMapping("/setSessionData") public String setSessionData(HttpSession session){ session.setAttribute("name","seazean"); return "page"; } ``` * Session 数据设置 名称:@SessionAttributes 类型:类注解 位置:处理器类上方 作用:声明放入session范围的变量名称,适用于Model类型数据传参 范例: ```java @Controller //设定当前类中名称为age和gender的变量放入session范围,不常用 @SessionAttributes(names = {"age","gender"}) public class ServletController { //将数据放入session存储范围,Model对象实现数据set,@SessionAttributes注解实现范围设定 @RequestMapping("/setSessionData2") public String setSessionDate2(Model model) { model.addAttribute("age",39); model.addAttribute("gender","男"); return "page"; } @RequestMapping("/sessionApi") public String sessionApi(@SessionAttribute("age") int age, @SessionAttribute("gender") String gender){ System.out.println(name); System.out.println(age); return "page"; } } ``` * spring-mvc.xml 配置 ```xml ``` *** ## 运行原理 ### 技术架构 #### 组件介绍 核心组件: * DispatcherServlet:核心控制器, 是 SpringMVC 的核心,整体流程控制的中心,所有的请求第一步都先到达这里,由其调用其它组件处理用户的请求,它就是在 web.xml 配置的核心 Servlet,有效的降低了组件间的耦合性 * HandlerMapping:处理器映射器, 负责根据请求找到对应具体的 Handler 处理器,SpringMVC 中针对配置文件方式、注解方式等提供了不同的映射器来处理 * Handler:处理器,其实就是 Controller,业务处理的核心类,通常由开发者编写,并且必须遵守 Controller 开发的规则,这样适配器才能正确的执行。例如实现 Controller 接口,将 Controller 注册到 IOC 容器中等 * HandlAdapter:处理器适配器,根据映射器中找到的 Handler,通过 HandlerAdapter 去执行 Handler,这是适配器模式的应用 * View Resolver:视图解析器, 将 Handler 中返回的逻辑视图(ModelAndView)解析为一个具体的视图(View)对象 * View:视图, View 最后对页面进行渲染将结果返回给用户,SpringMVC 框架提供了很多的 View 视图类型,包括:jstlView、freemarkerView、pdfView 等 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-技术架构.png) 优点: * 与 Spring 集成,更好的管理资源 * 有很多参数解析器和视图解析器,支持的数据类型丰富 * 将映射器、处理器、视图解析器进行解耦,分工明确 **** #### 工作原理 在 Spring 容器初始化时会建立所有的 URL 和 Controller 的对应关系,保存到 Map 中,这样 request 就能快速根据 URL 定位到 Controller: * 在 Spring IOC 容器初始化完所有单例 bean 后 * SpringMVC 会遍历所有的 bean,获取 Controller 中对应的 URL(这里获取 URL 的实现类有多个,用于处理不同形式配置的 Controller) * 将每一个 URL 对应一个 Controller 存入 Map 中 注意:将 @Controller 注解换成 @Component,启动时不会报错,但是在浏览器中输入路径时会出现 404,说明 Spring 没有对所有的 bean 进行 URL 映射 **一个 Request 来了:** * 监听端口,获得请求:Tomcat 监听 8080 端口的请求处理,根据路径调用了 web.xml 中配置的核心控制器 DispatcherServlet,`DispatcherServlet#doDispatch` 是**核心调度方法** * **首先根据 URI 获取 HandlerMapping 处理器映射器**,RequestMappingHandlerMapping 用来处理 @RequestMapping 注解的映射规则,其中保存了所有 handler 的映射规则,最后包装成一个拦截器链返回,拦截器链对象持有 HandlerMapping。如果没有合适的处理请求的 HandlerMapping,说明请求处理失败,设置响应码 404 返回 * 根据映射器获取当前 handler,**处理器适配器执行处理方法**,适配器根据请求的 URL 去 handler 中寻找对应的处理方法: * 创建 ModelAndViewContainer (mav) 对象,用来填充数据,然后通过不同的**参数解析器**去解析 URL 中的参数,完成数据解析绑定,然后执行真正的 Controller 方法,完成 handle 处理 * 方法执行完对**返回值**进行处理,没添加 @ResponseBody 注解的返回值使用视图处理器处理,把视图名称设置进入 mav 中 * 对添加了 @ResponseBody 注解的 Controller 的按照普通的返回值进行处理,首先进行内容协商,找到一种浏览器可以接受(请求头 Accept)的并且服务器可以生成的数据类型,选择合适数据转换器,设置响应头中的数据类型,然后写出数据 * 最后把 ModelAndViewContainer 和 ModelMap 中的数据**封装到 ModelAndView 对象**返回 * **视图解析**,根据返回值创建视图,请求转发 View 实例为 InternalResourceView,重定向 View 实例为 RedirectView。最后调用 view.render 进行页面渲染,结果派发 * 请求转发时请求域中的数据不丢失,会把 ModelAndView 的数据设置到请求域中,获取 Servlet 原生的 RequestDispatcher,调用 `RequestDispatcher#forward` 实现转发 * 重定向会造成请求域中的数据丢失,使用 Servlet 原生方式实现重定向 `HttpServletResponse#sendRedirect` **** ### 调度函数 请求进入原生的 HttpServlet 的 doGet() 方法处理,调用子类 FrameworkServlet 的 doGet() 方法,最终调用 DispatcherServlet 的 doService() 方法,为请求设置相关属性后调用 doDispatch(),请求和响应的以参数的形式传入 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-请求相应的原理.png) ```java // request 和 response 为 Java 原生的类 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; // 文件上传请求 boolean multipartRequestParsed = false; // 异步管理器 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { // 文件上传相关请求 processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 找到当前请求使用哪个 HandlerMapping (Controller 的方法)处理,返回执行链 mappedHandler = getHandler(processedRequest); // 没有合适的处理请求的方式 HandlerMapping,请求失败,直接返回 404 if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // 根据映射器获取当前 handler 处理器适配器,用来【处理当前的请求】 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 获取发出此次请求的方式 String method = request.getMethod(); // 判断请求是不是 GET 方法 boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } // 拦截器链的前置处理 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 执行处理方法,返回的是 ModelAndView 对象,封装了所有的返回值数据 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } // 设置视图名字 applyDefaultViewName(processedRequest, mv); // 执行拦截器链中的后置处理方法 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } // 处理程序调用的结果,进行结果派发 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } //.... } ``` 笔记参考视频:https://www.bilibili.com/video/BV19K4y1L7MT *** ### 请求映射 #### 映射器 doDispatch() 中调用 getHandler 方法获取所有的映射器 总体流程: * 所有的请求映射都在 HandlerMapping 中,**RequestMappingHandlerMapping 处理 @RequestMapping 注解的映射规则** * 遍历所有的 HandlerMapping 看是否可以匹配当前请求,匹配成功后返回,匹配失败设置 HTTP 404 响应码 * 用户可以自定义的映射处理,也可以给容器中放入自定义 HandlerMapping 访问 URL:http://localhost:8080/user ```java @GetMapping("/user") public String getUser(){ return "GET"; } @PostMapping("/user") public String postUser(){ return "POST"; } //。。。。。 ``` HandlerMapping 处理器映射器,**保存了所有 `@RequestMapping` 和 `handler` 的映射规则** ```java protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { // 遍历所有的 HandlerMapping for (HandlerMapping mapping : this.handlerMappings) { // 尝试去每个 HandlerMapping 中匹配当前请求的处理 HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; } ``` ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-获取Controller处理器.png) * `mapping.getHandler(request)`:调用 AbstractHandlerMapping#getHandler * `Object handler = getHandlerInternal(request)`:**获取映射器**,底层调用 RequestMappingInfoHandlerMapping 类的方法,又调用 AbstractHandlerMethodMapping#getHandlerInternal * `String lookupPath = initLookupPath(request)`:地址栏的 URI,这里的 lookupPath 为 /user * `this.mappingRegistry.acquireReadLock()`:加读锁防止其他线程并发修改 * `handlerMethod = lookupHandlerMethod(lookupPath, request)`:获取当前 HandlerMapping 中的映射规则 * `directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath)`:获取当前的映射器与当前**请求的 URI 有关的所有映射规则** ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-HandlerMapping的映射规则.png) * `addMatchingMappings(directPathMatches, matches, request)`:**匹配某个映射规则** * `for (T mapping : mappings)`:遍历所有的映射规则 * `match = getMatchingMapping(mapping, request)`:去匹配每一个映射规则,匹配失败返回 null * `matches.add(new Match())`:匹配成功后封装成匹配器添加到匹配集合中 * `matches.sort(comparator)`:匹配集合排序 * `Match bestMatch = matches.get(0)`:匹配完成只剩一个,直接获取返回对应的处理方法 * `if (matches.size() > 1)`:当有多个映射规则符合请求时,报错 * `return bestMatch.getHandlerMethod()`:返回匹配器中的处理方法 * `executionChain = getHandlerExecutionChain(handler, request)`:**为当前请求和映射器的构建一个拦截器链** * `for (HandlerInterceptor interceptor : this.adaptedInterceptors)`:遍历所有的拦截器 * `chain.addInterceptor(interceptor)`:把所有的拦截器添加到 HandlerExecutionChain 中,形成拦截器链 * `return executionChain`:**返回拦截器链,HandlerMapping 是链的 handler 成员属性** **** #### 适配器 doDispatch() 中调用 `HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())` ```java protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { if (this.handlerAdapters != null) { // 遍历所有的 HandlerAdapter for (HandlerAdapter adapter : this.handlerAdapters) { // 判断当前适配器是否支持当前 handle if (adapter.supports(handler)) { // 返回的是 【RequestMappingHandlerAdapter】 // AbstractHandlerMethodAdapter#supports -> RequestMappingHandlerAdapter return adapter; } } } throw new ServletException(); } ``` *** #### 方法执行 实例代码: ```java @GetMapping("/params") public String param(Map map, Model model, HttpServletRequest request) { map.put("k1", "v1"); // 都可以向请求域中添加数据 model.addAttribute("k2", "v2"); // 它们两个都在数据封装在 【BindingAwareModelMap】,继承自 LinkedHashMap request.setAttribute("m", "HelloWorld"); return "forward:/success"; } ``` ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-Model和Map的数据解析.png) doDispatch() 中调用 `mv = ha.handle(processedRequest, response, mappedHandler.getHandler())` **使用适配器执行方法** `AbstractHandlerMethodAdapter#handle` → `RequestMappingHandlerAdapter#handleInternal` → `invokeHandlerMethod`: ```java protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { // 封装成 SpringMVC 的接口,用于通用 Web 请求拦截器,使能够访问通用请求元数据,而不是用于实际处理请求 ServletWebRequest webRequest = new ServletWebRequest(request, response); try { // WebDataBinder 用于【从 Web 请求参数到 JavaBean 对象的数据绑定】,获取创建该实例的工厂 WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); // 创建 Model 实例,用于向模型添加属性 ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); // 方法执行器 ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); // 参数解析器,有很多 if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } // 返回值处理器,也有很多 if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } // 设置数据绑定器 invocableMethod.setDataBinderFactory(binderFactory); // 设置参数检查器 invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); // 新建一个 ModelAndViewContainer 并进行初始化和一些属性的填充 ModelAndViewContainer mavContainer = new ModelAndViewContainer(); // 设置一些属性 // 【执行目标方法】 invocableMethod.invokeAndHandle(webRequest, mavContainer); // 异步请求 if (asyncManager.isConcurrentHandlingStarted()) { return null; } // 【获取 ModelAndView 对象,封装了 ModelAndViewContainer】 return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } ``` ServletInvocableHandlerMethod#invokeAndHandle:执行目标方法 * `returnValue = invokeForRequest(webRequest, mavContainer, providedArgs)`:**执行自己写的 controller 方法,返回的就是自定义方法中 return 的值** `Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs)`:**参数处理的逻辑**,遍历所有的参数解析器解析参数或者将 URI 中的参数进行绑定,绑定完成后开始执行目标方法 * `parameters = getMethodParameters()`:获取此处理程序方法的方法参数的详细信息 * `Object[] args = new Object[parameters.length]`:存放所有的参数 * `for (int i = 0; i < parameters.length; i++)`:遍历所有的参数 * `args[i] = findProvidedArgument(parameter, providedArgs)`:获取调用方法时提供的参数,一般是空 * `if (!this.resolvers.supportsParameter(parameter))`:**获取可以解析当前参数的参数解析器** `return getArgumentResolver(parameter) != null`:获取参数的解析是否为空 * `for (HandlerMethodArgumentResolver resolver : this.argumentResolvers)`:遍历容器内所有的解析器 `if (resolver.supportsParameter(parameter))`:是否支持当前参数 * `PathVariableMethodArgumentResolver#supportsParameter`:**解析标注 @PathVariable 注解的参数** * `ModelMethodProcessor#supportsParameter`:解析 Map 和 Model 类型的参数,Model 和 Map 的作用一样 * `ExpressionValueMethodArgumentResolver#supportsParameter`:解析标注 @Value 注解的参数 * `RequestParamMapMethodArgumentResolver#supportsParameter`:**解析标注 @RequestParam 注解** * `RequestPartMethodArgumentResolver#supportsParameter`:解析文件上传的信息 * `ModelAttributeMethodProcessor#supportsParameter`:解析标注 @ModelAttribute 注解或者不是简单类型 * 子类 ServletModelAttributeMethodProcessor 是解析自定义类型 JavaBean 的解析器 * 简单类型有 Void、Enum、Number、CharSequence、Date、URI、URL、Locale、Class * `args[i] = this.resolvers.resolveArgument()`:**开始解析参数,每个参数使用的解析器不同** `resolver = getArgumentResolver(parameter)`:获取参数解析器 `return resolver.resolveArgument()`:开始解析 * `PathVariableMapMethodArgumentResolver#resolveArgument`:@PathVariable,包装 URI 中的参数为 Map * `MapMethodProcessor#resolveArgument`:调用 `mavContainer.getModel()` 返回默认 BindingAwareModelMap 对象 * `ModelAttributeMethodProcessor#resolveArgument`:**自定义的 JavaBean 的绑定封装**,下一小节详解 `return doInvoke(args)`:**真正的执行 Controller 方法** * `Method method = getBridgedMethod()`:从 HandlerMethod 获取要反射执行的方法 * `ReflectionUtils.makeAccessible(method)`:破解权限 * `method.invoke(getBean(), args)`:执行方法,getBean 获取的是标记 @Controller 的 Bean 类,其中包含执行方法 * **进行返回值的处理,响应部分详解**,处理完成进入下面的逻辑 RequestMappingHandlerAdapter#getModelAndView:获取 ModelAndView 对象 * `modelFactory.updateModel(webRequest, mavContainer)`:Model 数据升级到会话域(**请求域中的数据在重定向时丢失**) * `updateBindingResult(request, defaultModel)`:把绑定的数据添加到 BindingAwareModelMap 中 * `if (mavContainer.isRequestHandled())`:判断请求是否已经处理完成了 * `ModelMap model = mavContainer.getModel()`:获取**包含 Controller 方法参数**的 BindingAwareModelMap(本节开头) * `mav = new ModelAndView()`:**把 ModelAndViewContainer 和 ModelMap 中的数据封装到 ModelAndView** * `if (!mavContainer.isViewReference())`:是否是通过名称指定视图引用 * `if (model instanceof RedirectAttributes)`:判断 model 是否是重定向数据,如果是进行重定向逻辑 * `return mav`:**任何方法执行都会返回 ModelAndView 对象** *** #### 参数解析 解析自定义的 JavaBean 为例,调用 ModelAttributeMethodProcessor#resolveArgument 处理参数的方法,通过合适的类型转换器把 URL 中的参数转换以后,利用反射获取 set 方法,注入到 JavaBean * Person.java: ```java @Data @Component //加入到容器中 public class Person { private String userName; private Integer age; private Date birth; } ``` * Controller: ```java @RestController //返回的数据不是页面 public class ParameterController { // 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定 @GetMapping("/saveuser") public Person saveuser(Person person){ return person; } } ``` * 访问 URL:http://localhost:8080/saveuser?userName=zhangsan&age=20 进入源码:ModelAttributeMethodProcessor#resolveArgument * `name = ModelFactory.getNameForParameter(parameter)`:获取名字,此例就是 person * `ann = parameter.getParameterAnnotation(ModelAttribute.class)`:是否有 ModelAttribute 注解 * `if (mavContainer.containsAttribute(name))`:ModelAndViewContainer 中是否包含 person 对象 * `attribute = createAttribute()`:**创建一个实例,空的 Person 对象** * `binder = binderFactory.createBinder(webRequest, attribute, name)`:Web 数据绑定器,可以利用 Converters 将请求数据转成指定的数据类型,绑定到 JavaBean 中 * `bindRequestParameters(binder, webRequest)`:**利用反射向目标对象填充数据** `servletBinder = (ServletRequestDataBinder) binder`:类型强转 `servletBinder.bind(servletRequest)`:绑定数据 * `mpvs = new MutablePropertyValues(request.getParameterMap())`:获取请求 URI 参数中的 k-v 键值对 * `addBindValues(mpvs, request)`:子类可以用来为请求添加额外绑定值 * `doBind(mpvs)`:真正的绑定的方法,调用 `applyPropertyValues` 应用参数值,然后调用 `setPropertyValues` 方法 `AbstractPropertyAccessor#setPropertyValues()`: * `List propertyValues`:获取到所有的参数的值,就是 URI 上的所有的参数值 * `for (PropertyValue pv : propertyValues)`:遍历所有的参数值 * `setPropertyValue(pv)`:**填充到空的 Person 实例中** * `nestedPa = getPropertyAccessorForPropertyPath(propertyName)`:获取属性访问器 * `tokens = getPropertyNameTokens()`:获取元数据的信息 * `nestedPa.setPropertyValue(tokens, pv)`:填充数据 * `processLocalProperty(tokens, pv)`:处理属性 * `if (!Boolean.FALSE.equals(pv.conversionNecessary))`:数据是否需要转换了 * `if (pv.isConverted())`:数据已经转换过了,转换了直接赋值,没转换进行转换 * `oldValue = ph.getValue()`:获取未转换的数据 * `valueToApply = convertForProperty()`:进行数据转换 `TypeConverterDelegate#convertIfNecessary`:进入该方法的逻辑 * `if (conversionService.canConvert(sourceTypeDesc, typeDescriptor))`:判断能不能转换 `GenericConverter converter = getConverter(sourceType, targetType)`:**获取类型转换器** * `converter = this.converters.find(sourceType, targetType)`:寻找合适的转换器 * `sourceCandidates = getClassHierarchy(sourceType.getType())`:原数据类型 * `targetCandidates = getClassHierarchy(targetType.getType())`:目标数据类型 ```java for (Class sourceCandidate : sourceCandidates) { //双重循环遍历,寻找合适的转换器 for (Class targetCandidate : targetCandidates) { ``` * `GenericConverter converter = getRegisteredConverter(..)`:匹配类型转换器 * `return converter`:返回转换器 * `conversionService.convert(newValue, sourceTypeDesc, typeDescriptor)`:开始转换 * `converter = getConverter(sourceType, targetType)`:**获取可用的转换器** * `result = ConversionUtils.invokeConverter()`:执行转换方法 * `converter.convert()`:**调用转换器的转换方法**(GenericConverter#convert) * `return handleResult(sourceType, targetType, result)`:返回结果 * `ph.setValue(valueToApply)`:**设置 JavaBean 属性**(BeanWrapperImpl.BeanPropertyHandler) * `Method writeMethod`:获取写数据方法 * `Class cls = getClass0()`:获取 Class 对象 * `writeMethodName = Introspector.SET_PREFIX + getBaseName()`:**set 前缀 + 属性名** * `writeMethod = Introspector.findMethod(cls, writeMethodName, 1, args)`:获取只包含一个参数的 set 方法 * `setWriteMethod(writeMethod)`:加入缓存 * `ReflectionUtils.makeAccessible(writeMethod)`:设置访问权限 * `writeMethod.invoke(getWrappedInstance(), value)`:执行方法 * `bindingResult = binder.getBindingResult()`:获取绑定的结果 * `mavContainer.addAllAttributes(bindingResultModel)`:**把所有填充的参数放入 ModelAndViewContainer** * `return attribute`:返回填充后的 Person 对象 **** ### 响应处理 #### 响应数据 以 Person 为例: ```java @ResponseBody // 利用返回值处理器里面的消息转换器进行处理,而不是视图 @GetMapping(value = "/person") public Person getPerson(){ Person person = new Person(); person.setAge(28); person.setBirth(new Date()); person.setUserName("zhangsan"); return person; } ``` 直接进入方法执行完后的逻辑 ServletInvocableHandlerMethod#invokeAndHandle: ```java public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 【执行目标方法】,return person 对象 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); // 设置状态码 setResponseStatus(webRequest); // 判断方法是否有返回值 if (returnValue == null) { if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) { disableContentCachingIfNecessary(webRequest); mavContainer.setRequestHandled(true); return; } } // 返回值是字符串 else if (StringUtils.hasText(getResponseStatusReason())) { // 设置请求处理完成 mavContainer.setRequestHandled(true); return; // 设置请求没有处理完成,还需要进行返回值的逻辑 mavContainer.setRequestHandled(false); Assert.state(this.returnValueHandlers != null, "No return value handlers"); try { // 【返回值的处理】 this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) {} } ``` * **没有加 @ResponseBody 注解的返回数据按照视图处理的逻辑**,ViewNameMethodReturnValueHandler(视图详解) * 此例是加了注解的,返回的数据不是视图,HandlerMethodReturnValueHandlerComposite#handleReturnValue: ```java public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { // 获取合适的返回值处理器 HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException(); } // 使用处理器处理返回值(详解源码中的这两个函数) handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } ``` HandlerMethodReturnValueHandlerComposite#selectHandler:获取合适的返回值处理器 * `boolean isAsyncValue = isAsyncReturnValue(value, returnType)`:是否是异步请求 * `for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers)`:遍历所有的返回值处理器 * `RequestResponseBodyMethodProcessor#supportsReturnType`:**处理标注 @ResponseBody 注解的返回值** * `ModelAndViewMethodReturnValueHandler#supportsReturnType`:处理返回值类型是 ModelAndView 的处理器 * `ModelAndViewResolverMethodReturnValueHandler#supportsReturnType`:直接返回 true,处理所有数据 RequestResponseBodyMethodProcessor#handleReturnValue:处理返回值,要进行**内容协商** * `mavContainer.setRequestHandled(true)`:设置请求处理完成 * `inputMessage = createInputMessage(webRequest)`:获取输入的数据 * `outputMessage = createOutputMessage(webRequest)`:获取输出的数据 * `writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage)`:使用消息转换器进行写出 * `if (value instanceof CharSequence)`:判断返回的数据是不是字符类型 * `body = value`:把 value 赋值给 body,此时 body 中就是自定义方法执行完后的 Person 对象 * `if (isResourceType(value, returnType))`:当前数据是不是流数据 * `MediaType selectedMediaType`:**内容协商后选择使用的类型,浏览器和服务器都支持的媒体(数据)类型** * `MediaType contentType = outputMessage.getHeaders().getContentType()`:获取响应头的数据 * `if (contentType != null && contentType.isConcrete())`:判断当前响应头中是否已经有确定的媒体类型 `selectedMediaType = contentType`:前置处理已经使用了媒体类型,直接继续使用该类型 * `acceptableTypes = getAcceptableMediaTypes(request)`:**获取浏览器支持的媒体类型,请求头字段** * `this.contentNegotiationManager.resolveMediaTypes()`:调用该方法 * `for(ContentNegotiationStrategy strategy:this.strategies)`:**默认策略是提取请求头的字段的内容**,策略类为HeaderContentNegotiationStrategy,可以配置添加其他类型的策略 * `List mediaTypes = strategy.resolveMediaTypes(request)`:解析 Accept 字段存储为 List * `headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT)`:获取请求头中 Accept 字段 * `List mediaTypes = MediaType.parseMediaTypes(headerValues)`:解析成 List 集合 * `MediaType.sortBySpecificityAndQuality(mediaTypes)`:按照相对品质因数 q 降序排序 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-浏览器支持接收的数据类型.png) * `producibleTypes = getProducibleMediaTypes(request, valueType, targetType)`:**服务器能生成的媒体类型** * `request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)`:从请求域获取默认的媒体类型 * ` for (HttpMessageConverter converter : this.messageConverters)`:遍历所有的消息转换器 * `converter.canWrite(valueClass, null)`:是否支持当前的类型 * ` result.addAll(converter.getSupportedMediaTypes())`:把当前 MessageConverter 支持的所有类型放入 result * `List mediaTypesToUse = new ArrayList<>()`:存储最佳匹配的集合 * **内容协商:** ```java for (MediaType requestedType : acceptableTypes) { // 遍历所有浏览器能接受的媒体类型 for (MediaType producibleType : producibleTypes) { // 遍历所有服务器能产出的 if (requestedType.isCompatibleWith(producibleType)) { // 判断类型是否匹配,最佳匹配 // 数据协商匹配成功,一般有多种 mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } ``` * `MediaType.sortBySpecificityAndQuality(mediaTypesToUse)`:按照相对品质因数 q 排序,降序排序,越大的越好 * `for (MediaType mediaType : mediaTypesToUse)`:**遍历所有的最佳匹配**,选择一种赋值给选择的类型 * `selectedMediaType = selectedMediaType.removeQualityValue()`:媒体类型去除相对品质因数 * `for (HttpMessageConverter converter : this.messageConverters)`:**遍历所有的 HTTP 数据转换器** * `GenericHttpMessageConverter genericConverter`:**MappingJackson2HttpMessageConverter 可以将对象写为 JSON** * `((GenericHttpMessageConverter) converter).canWrite()`:判断转换器是否可以写出给定的类型 `AbstractJackson2HttpMessageConverter#canWrit` * `if (!canWrite(mediaType))`:是否可以写出指定类型 * `MediaType.ALL.equalsTypeAndSubtype(mediaType)`:是不是 `*/*` 类型 * `getSupportedMediaTypes()`:支持 `application/json` 和 `application/*+json` 两种类型 * `return true`:返回 true * `objectMapper = selectObjectMapper(clazz, mediaType)`:选择可以使用的 objectMapper * `causeRef = new AtomicReference<>()`:获取并发安全的引用 * `if (objectMapper.canSerialize(clazz, causeRef))`:objectMapper 可以序列化当前类 * `return true`:返回 true * ` body = getAdvice().beforeBodyWrite()`:**获取要响应的所有数据,就是 Person 对象** * `addContentDispositionHeader(inputMessage, outputMessage)`:检查路径 * `genericConverter.write(body, targetType, selectedMediaType, outputMessage)`:调用消息转换器的 write 方法 `AbstractGenericHttpMessageConverter#write`:该类的方法 * `addDefaultHeaders(headers, t, contentType)`:**设置响应头中的数据类型** ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-服务器设置数据类型.png) * `writeInternal(t, type, outputMessage)`:**数据写出为 JSON 格式** * `Object value = object`:value 引用 Person 对象 * `ObjectWriter objectWriter = objectMapper.writer()`:获取 ObjectWriter 对象 * `objectWriter.writeValue(generator, value)`:使用 ObjectWriter 写出数据为 JSON *** #### 协商策略 开启基于请求参数的内容协商模式:(SpringBoot 方式) ```yaml spring.mvc.contentnegotiation:favor-parameter: true # 开启请求参数内容协商模式 ``` 发请求: http://localhost:8080/person?format=json,解析 format 策略类为 ParameterContentNegotiationStrategy,运行流程如下: * `acceptableTypes = getAcceptableMediaTypes(request)`:获取浏览器支持的媒体类型 `mediaTypes = strategy.resolveMediaTypes(request)`:解析请求 URL 参数中的数据 * `return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest))`: `getMediaTypeKey(webRequest)`: * `request.getParameter(getParameterName())`:获取 URL 中指定的需求的数据类型 * `getParameterName()`:获取参数的属性名 format * `getParameter()`:**获取 URL 中 format 对应的数据** `resolveMediaTypeKey()`:解析媒体类型,封装成集合 自定义内容协商策略: ```java public class WebConfig implements WebMvcConfigurer { @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override //自定义内容协商策略 public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { Map mediaTypes = new HashMap<>(); mediaTypes.put("json", MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); mediaTypes.put("person",MediaType.parseMediaType("application/x-person")); // 指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); // 请求头解析 HeaderContentNegotiationStrategy headStrategy = new HeaderContentNegotiationStrategy(); // 添加到容器中,即可以解析请求头 又可以解析请求参数 configurer.strategies(Arrays.asList(parameterStrategy,headStrategy)); } @Override // 自定义消息转换器 public void extendMessageConverters(List> converters) { converters.add(new GuiguMessageConverter()); } } } } ``` 也可以自定义 HttpMessageConverter,实现 HttpMessageConverter 接口重写方法即可 *** ### 视图解析 #### 返回解析 请求处理: ```java @GetMapping("/params") public String param(){ return "forward:/success"; //return "redirect:/success"; } ``` 进入执行方法逻辑 ServletInvocableHandlerMethod#invokeAndHandle,进入 `this.returnValueHandlers.handleReturnValue`: ```java public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) { // 获取合适的返回值处理器:调用 if (handler.supportsReturnType(returnType))判断是否支持 HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException(); } // 使用处理器处理返回值 handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } ``` * ViewNameMethodReturnValueHandler#supportsReturnType: ```java public boolean supportsReturnType(MethodParameter returnType) { Class paramType = returnType.getParameterType(); // 返回值是否是 void 或者是 CharSequence 字符序列,这里是字符序列 return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType)); } ``` * ViewNameMethodReturnValueHandler#handleReturnValue: ```java public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { // 返回值是字符串,是 return "forward:/success" if (returnValue instanceof CharSequence) { String viewName = returnValue.toString(); // 【把视图名称设置进入 ModelAndViewContainer 中】 mavContainer.setViewName(viewName); // 判断是否是重定向数据 `viewName.startsWith("redirect:")` if (isRedirectViewName(viewName)) { // 如果是重定向,设置是重定向指令 mavContainer.setRedirectModelScenario(true); } } else if (returnValue != null) { // should not happen throw new UnsupportedOperationException(); } } ``` *** #### 结果派发 doDispatch() 中的 processDispatchResult:处理派发结果 ```java private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { } // mv 是 ModelAndValue if (mv != null && !mv.wasCleared()) { // 渲染视图 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else {} } ``` DispatcherServlet#render: * `Locale locale = this.localeResolver.resolveLocale(request)`:国际化相关 * `String viewName = mv.getViewName()`:视图名字,是请求转发 forward:/success(响应数据解析并存入 ModelAndView) * `view = resolveViewName(viewName, mv.getModelInternal(), locale, request)`:解析视图 * `for (ViewResolver viewResolver : this.viewResolvers)`:**遍历所有的视图解析器** `view = viewResolver.resolveViewName(viewName, locale)`:根据视图名字解析视图,调用内容协商视图处理器 ContentNegotiatingViewResolver 的方法 * `attrs = RequestContextHolder.getRequestAttributes()`:获取请求的相关属性信息 * `requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest())`:获取最佳匹配的媒体类型,函数内进行了匹配的逻辑 * `candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes)`:获取候选的视图对象 * `for (ViewResolver viewResolver : this.viewResolvers)`:遍历所有的视图解析器 * `View view = viewResolver.resolveViewName(viewName, locale)`:**解析视图** `AbstractCachingViewResolver#resolveViewName`: * `returnview = createView(viewName, locale)`:UrlBasedViewResolver#createView **请求转发**:实例为 InternalResourceView * `if (viewName.startsWith(FORWARD_URL_PREFIX))`:视图名字是否是 **`forward:`** 的前缀 * `forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length())`:名字截取前缀 * `view = new InternalResourceView(forwardUrl)`:新建 InternalResourceView 对象并返回 * `return applyLifecycleMethods(FORWARD_URL_PREFIX, view)`:Spring 中的初始化操作 **重定向**:实例为 RedirectView * `if (viewName.startsWith(REDIRECT_URL_PREFIX))`:视图名字是否是 **`redirect:`** 的前缀 * `redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length())`:名字截取前缀 * `RedirectView view = new RedirectView()`:新建 RedirectView 对象并返回 * `bestView = getBestView(candidateViews, requestedMediaTypes, attrs)`:选出最佳匹配的视图对象 * `view.render(mv.getModelInternal(), request, response)`:**页面渲染** * `mergedModel = createMergedOutputModel(model, request, response)`:把请求域中的数据封装到 model * `prepareResponse(request, response)`:响应前的准备工作,设置一些响应头 * `renderMergedOutputModel(mergedModel, getRequestToExpose(request), response)`:渲染输出的数据 `getRequestToExpose(request)`:获取 Servlet 原生的方式 **请求转发 InternalResourceView 的逻辑:请求域中的数据不丢失** * `exposeModelAsRequestAttributes(model, request)`:暴露 model 作为请求域的属性 * `model.forEach()`:遍历 Model 中的数据 * `request.setAttribute(name, value)`:**设置到请求域中** * `exposeHelpers(request)`:自定义接口 * `dispatcherPath = prepareForRendering(request, response)`:确定调度分派的路径,此例是 /success * `rd = getRequestDispatcher(request, dispatcherPath)`:**获取 Servlet 原生的 RequestDispatcher 实现转发** * `rd.forward(request, response)`:实现请求转发 **重定向 RedirectView 的逻辑:请求域中的数据会丢失** * `targetUrl = createTargetUrl(model, request)`:获取目标 URL * `enc = request.getCharacterEncoding()`:设置编码 UTF-8 * `appendQueryProperties(targetUrl, model, enc)`:添加一些属性,比如 `url + ?name=123&&age=324` * `sendRedirect(request, response, targetUrl, this.http10Compatible)`:重定向 * `response.sendRedirect(encodedURL)`:**使用 Servlet 原生方法实现重定向** **** ## 异步调用 ### 请求参数 名称:@RequestBody 类型:形参注解 位置:处理器类中的方法形参前方 作用:将异步提交数据**转换**成标准请求参数格式,并赋值给形参 范例: ```java @Controller //控制层 public class AjaxController { @RequestMapping("/ajaxController") public String ajaxController(@RequestBody String message){ System.out.println(message); return "page.jsp"; } } ``` * 注解添加到 POJO 参数前方时,封装的异步提交数据按照 POJO 的属性格式进行关系映射 * POJO 中的属性如果请求数据中没有,属性值为 null * POJO 中没有的属性如果请求数据中有,不进行映射 * 注解添加到集合参数前方时,封装的异步提交数据按照集合的存储结构进行关系映射 ```java @RequestMapping("/ajaxPojoToController") //如果处理参数是POJO,且页面发送的请求数据格式与POJO中的属性对应,@RequestBody注解可以自动映射对应请求数据到POJO中 public String ajaxPojoToController(@RequestBody User user){ System.out.println("controller pojo :"+user); return "page.jsp"; } @RequestMapping("/ajaxListToController") //如果处理参数是List集合且封装了POJO,且页面发送的数据是JSON格式,数据将自动映射到集合参数 public String ajaxListToController(@RequestBody List userList){ System.out.println("controller list :"+userList); return "page.jsp"; } ``` ajax.jsp ```html <%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> 访问springmvc后台controller
传递Json格式POJO
传递Json格式List
``` web.xml配置:请求响应章节请求中的web.xml配置 ```xml CharacterEncodingFilter + DispatcherServlet ``` spring-mvc.xml: ```xml ``` **** ### 响应数据 注解:@ResponseBody 作用:将 Java 对象转为 json 格式的数据 方法返回值为 POJO 时,自动封装数据成 Json 对象数据: ```java @RequestMapping("/ajaxReturnJson") @ResponseBody public User ajaxReturnJson(){ System.out.println("controller return json pojo..."); User user = new User("Jockme",40); return user; } ``` 方法返回值为 List 时,自动封装数据成 json 对象数组数据: ```java @RequestMapping("/ajaxReturnJsonList") @ResponseBody //基于jackon技术,使用@ResponseBody注解可以将返回的保存POJO对象的集合转成json数组格式数据 public List ajaxReturnJsonList(){ System.out.println("controller return json list..."); User user1 = new User("Tom",3); User user2 = new User("Jerry",5); ArrayList al = new ArrayList(); al.add(user1); al.add(user2); return al; } ``` AJAX 文件: ```js //为id="testAjaxReturnString"的组件绑定点击事件 $("#testAjaxReturnString").click(function(){ //发送异步调用 $.ajax({ type:"POST", url:"ajaxReturnString", //回调函数 success:function(data){ //打印返回结果 alert(data); } }); }); //为id="testAjaxReturnJson"的组件绑定点击事件 $("#testAjaxReturnJson").click(function(){ $.ajax({ type:"POST", url:"ajaxReturnJson", success:function(data){ alert(data['name']+" , "+data['age']); } }); }); //为id="testAjaxReturnJsonList"的组件绑定点击事件 $("#testAjaxReturnJsonList").click(function(){ $.ajax({ type:"POST", url:"ajaxReturnJsonList", success:function(data){ alert(data); alert(data[0]["name"]); alert(data[1]["age"]); } }); }); ``` **** ### 跨域访问 跨域访问:当通过域名 A 下的操作访问域名 B 下的资源时,称为跨域访问,跨域访问时,会出现无法访问的现象 环境搭建: * 为当前主机添加备用域名 * 修改 windows 安装目录中的 host 文件 * 格式: ip 域名 * 动态刷新 DNS * 命令: ipconfig /displaydns * 命令: ipconfig /flushdns 跨域访问支持: * 名称:@CrossOrigin * 类型:方法注解 、 类注解 * 位置:处理器类中的方法上方或类上方 * 作用:设置当前处理器方法 / 处理器类中所有方法支持跨域访问 * 范例: ```java @RequestMapping("/cross") @ResponseBody //使用@CrossOrigin开启跨域访问 //标注在处理器方法上方表示该方法支持跨域访问 //标注在处理器类上方表示该处理器类中的所有处理器方法均支持跨域访问 @CrossOrigin public User cross(HttpServletRequest request){ System.out.println("controller cross..." + request.getRequestURL()); User user = new User("Jockme",36); return user; } ``` * jsp 文件 ```html 跨域访问
``` *** ## 拦截器 ### 基本介绍 拦截器(Interceptor)是一种动态拦截方法调用的机制 作用: 1. 在指定的方法调用前后执行预先设定后的的代码 2. 阻止原始方法的执行 核心原理:AOP 思想 拦截器链:多个拦截器按照一定的顺序,对原始被调用功能进行增强 拦截器和过滤器对比: 1. 归属不同: Filter 属于 Servlet 技术, Interceptor 属于 SpringMVC 技术 2. 拦截内容不同: Filter 对所有访问进行增强, Interceptor 仅针对 SpringMVC 的访问进行增强 *** ### 处理方法 #### 前置处理 原始方法之前运行: ```java public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle"); return true; } ``` * 参数: * request:请求对象 * response:响应对象 * handler:被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装 * handler:public String controller.InterceptorController.handleRun * handler.getClass():org.springframework.web.method.HandlerMethod * 返回值: * 返回值为 false,被拦截的处理器将不执行 *** #### 后置处理 原始方法运行后运行,如果原始方法被拦截,则不执行: ```java public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle"); } ``` 参数: * modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整 *** #### 异常处理 拦截器最后执行的方法,无论原始方法是否执行: ```java public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion"); } ``` 参数: * ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理 *** ### 拦截配置 拦截路径: * `/**`:表示拦截所有映射 * `/* `:表示拦截所有/开头的映射 * `/user/*`:表示拦截所有 /user/ 开头的映射 * `/user/add*`:表示拦截所有 /user/ 开头,且具体映射名称以 add 开头的映射 * `/user/*All`:表示拦截所有 /user/ 开头,且具体映射名称以 All 结尾的映射 ```xml ``` *** ### 拦截器链 **责任链模式**:责任链模式是一种行为模式 特点:沿着一条预先设定的任务链顺序执行,每个节点具有独立的工作任务 优势: * 独立性:只关注当前节点的任务,对其他任务直接放行到下一节点 * 隔离性:具备链式传递特征,无需知晓整体链路结构,只需等待请求到达后进行处理即可 * 灵活性:可以任意修改链路结构动态新增或删减整体链路责任 * 解耦:将动态任务与原始任务解耦 缺点: * 链路过长时,处理效率低下 * 可能存在节点上的循环引用现象,造成死循环,导致系统崩溃 *** ### 源码解析 DispatcherServlet#doDispatch 方法中: ```java protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { try { // 获取映射器以及映射器的所有拦截器(运行原理部分详解了源码) mappedHandler = getHandler(processedRequest); // 前置处理,返回 false 代表条件成立 if (!mappedHandler.applyPreHandle(processedRequest, response)) { //请求从这里直接结束 return; } //所有拦截器都返回 true,执行目标方法 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) // 倒序执行所有拦截器的后置处理方法 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { //异常处理机制 triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } } ``` HandlerExecutionChain#applyPreHandle:前置处理 ```java boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { //遍历所有的拦截器 for (int i = 0; i < this.interceptorList.size(); i++) { HandlerInterceptor interceptor = this.interceptorList.get(i); //执行前置处理,如果拦截器返回 false,则条件成立,不在执行其他的拦截器,直接返回 false,请求直接结束 if (!interceptor.preHandle(request, response, this.handler)) { triggerAfterCompletion(request, response, null); return false; } this.interceptorIndex = i; } return true; } ``` HandlerExecutionChain#applyPostHandle:后置处理 ```java 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); interceptor.postHandle(request, response, this.handler, mv); } } ``` DispatcherServlet#triggerAfterCompletion 底层调用 HandlerExecutionChain#triggerAfterCompletion: * 前面的步骤有任何异常都会直接倒序触发 afterCompletion * 页面成功渲染有异常,也会倒序触发 afterCompletion ```java void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { //倒序遍历 for (int i = this.interceptorIndex; i >= 0; i--) { HandlerInterceptor interceptor = this.interceptorList.get(i); try { //执行异常处理的方法 interceptor.afterCompletion(request, response, this.handler, ex); } catch (Throwable ex2) { logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); } } } ``` 拦截器的执行流程: 参考文章:https://www.yuque.com/atguigu/springboot/vgzmgh#wtPLU *** ### 自定义 * Contoller层 ```java @Controller public class InterceptorController { @RequestMapping("/handleRun") public String handleRun() { System.out.println("业务处理器运行------------main"); return "page.jsp"; } } ``` * 自定义拦截器需要实现 HandleInterceptor 接口 ```java //自定义拦截器需要实现HandleInterceptor接口 public class MyInterceptor implements HandlerInterceptor { //处理器运行之前执行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("前置运行----a1"); //返回值为false将拦截原始处理器的运行 //如果配置多拦截器,返回值为false将终止当前拦截器后面配置的拦截器的运行 return true; } //处理器运行之后执行 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("后置运行----b1"); } //所有拦截器的后置执行全部结束后,执行该操作 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("完成运行----c1"); } } ``` 说明:三个方法的运行顺序为 preHandle → postHandle → afterCompletion,如果 preHandle 返回值为 false,三个方法仅运行preHandle * web.xml: ```xml CharacterEncodingFilter + DispatcherServlet ``` * 配置拦截器:spring-mvc.xml ```xml ``` 注意:配置顺序为**先配置执行位置,后配置执行类** *** ## 异常处理 ### 处理器 异常处理器: **HandlerExceptionResolver** 接口 类继承该接口的以后,当开发出现异常后会执行指定的功能 ```java @Component public class ExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { System.out.println("异常处理器正在执行中"); ModelAndView modelAndView = new ModelAndView(); //定义异常现象出现后,反馈给用户查看的信息 modelAndView.addObject("msg","出错啦! "); //定义异常现象出现后,反馈给用户查看的页面 modelAndView.setViewName("error.jsp"); return modelAndView; } } ``` 根据异常的种类不同,进行分门别类的管理,返回不同的信息: ```java public class ExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { System.out.println("my exception is running ...." + ex); ModelAndView modelAndView = new ModelAndView(); if( ex instanceof NullPointerException){ modelAndView.addObject("msg","空指针异常"); }else if ( ex instanceof ArithmeticException){ modelAndView.addObject("msg","算数运算异常"); }else{ modelAndView.addObject("msg","未知的异常"); } modelAndView.setViewName("error.jsp"); return modelAndView; } } ``` 模拟错误: ```java @Controller public class UserController { @RequestMapping("/save") @ResponseBody public String save(@RequestBody String name) { //模拟业务层发起调用产生了异常 // int i = 1/0; // String str = null; // str.length(); return "error.jsp"; } ``` *** ### 注解开发 使用注解实现异常分类管理,开发异常处理器 @ControllerAdvice 注解: * 类型:类注解 * 位置:异常处理器类上方 * 作用:设置当前类为异常处理器类 * 格式: ```java @Component //声明该类是一个Controller的通知类,声明后该类就会被加载成异常处理器 @ControllerAdvice public class ExceptionAdvice { } ``` @ExceptionHandler 注解: * 类型:方法注解 * 位置:异常处理器类中针对指定异常进行处理的方法上方 * 作用:设置指定异常的处理方式 * 说明:处理器方法可以设定多个 * 格式: ```java @Component @ControllerAdvice public class ExceptionAdvice { //类中定义的方法携带@ExceptionHandler注解的会被作为异常处理器,后面添加实际处理的异常类型 @ExceptionHandler(NullPointerException.class) @ResponseBody public String doNullException(Exception ex){ return "空指针异常"; } @ExceptionHandler(Exception.class) @ResponseBody public String doException(Exception ex){ return "all Exception"; } } ``` @ResponseStatus 注解: * 类型:类注解、方法注解 * 位置:异常处理器类、方法上方 * 参数: value:出现错误指定返回状态码 reason:出现错误返回的错误信息 *** ### 解决方案 * web.xml ```java DispatcherServlet + CharacterEncodingFilter ``` * ajax.jsp ```jsp <%@page pageEncoding="UTF-8" language="java" contentType="text/html;UTF-8" %> 点击
``` * spring-mvc.xml ```xml ``` * java / controller / UserController ```java @Controller public class UserController { @RequestMapping("/save") @ResponseBody public List save(@RequestBody User user) { System.out.println("user controller save is running ..."); //对用户的非法操作进行判定,并包装成异常对象进行处理,便于统一管理 if(user.getName().trim().length() < 8){ throw new BusinessException("对不起,用户名长度不满足要求,请重新输入!"); } if(user.getAge() < 0){ throw new BusinessException("对不起,年龄必须是0到100之间的数字!"); } if(user.getAge() > 100){ throw new SystemException("服务器连接失败,请尽快检查处理!"); } User u1 = new User("Tom",3); User u2 = new User("Jerry",5); ArrayList al = new ArrayList(); al.add(u1);al.add(u2); return al; } } ``` * 自定义异常 ```java //自定义异常继承RuntimeException,覆盖父类所有的构造方法 public class BusinessException extends RuntimeException {覆盖父类所有的构造方法} ``` ```java public class SystemException extends RuntimeException {} ``` * 通过自定义异常将所有的异常现象进行分类管理,以统一的格式对外呈现异常消息 ```java @Component @ControllerAdvice public class ProjectExceptionAdvice { @ExceptionHandler(BusinessException.class) public String doBusinessException(Exception ex, Model m){ //使用参数Model将要保存的数据传递到页面上,功能等同于ModelAndView //业务异常出现的消息要发送给用户查看 m.addAttribute("msg",ex.getMessage()); return "error.jsp"; } @ExceptionHandler(SystemException.class) public String doSystemException(Exception ex, Model m){ //系统异常出现的消息不要发送给用户查看,发送统一的信息给用户看 m.addAttribute("msg","服务器出现问题,请联系管理员!"); return "error.jsp"; } @ExceptionHandler(Exception.class) public String doException(Exception ex, Model m){ m.addAttribute("msg",ex.getMessage()); //将ex对象保存起来 return "error.jsp"; } } ``` *** ## 文件传输 ### 上传下载 上传文件过程: ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-上传文件过程分析.png) MultipartResolver接口: * MultipartResolver 接口定义了文件上传过程中的相关操作,并对通用性操作进行了封装 * MultipartResolver 接口底层实现类 CommonsMultipartResovler * CommonsMultipartResovler 并未自主实现文件上传下载对应的功能,而是调用了 apache 文件上传下载组件 文件上传下载实现: * 导入坐标 ```xml commons-fileupload commons-fileupload 1.4 ``` * 页面表单 fileupload.jsp ```html

``` * web.xml ```xml DispatcherServlet + CharacterEncodingFilter ``` * 控制器 ```java @PostMapping("/upload") public String upload(@RequestParam("email") String email, @RequestParam("username") String username, @RequestPart("headerImg") MultipartFile headerImg) throws IOException { if(!headerImg.isEmpty()){ //保存到文件服务器,OSS服务器 String originalFilename = headerImg.getOriginalFilename(); headerImg.transferTo(new File("H:\\cache\\" + originalFilename)); } return "main"; } ``` *** ### 名称问题 MultipartFile 参数中封装了上传的文件的相关信息。 1. 文件命名问题, 获取上传文件名,并解析文件名与扩展名 ```java file.getOriginalFilename(); ``` 2. 文件名过长问题 3. 文件保存路径 ```java ServletContext context = request.getServletContext(); String realPath = context.getRealPath("/uploads"); File file = new File(realPath + "/"); if(!file.exists()) file.mkdirs(); ``` 4. 重名问题 ```java String uuid = UUID.randomUUID.toString().replace("-", "").toUpperCase(); ``` ```java @Controller public class FileUploadController { @RequestMapping(value = "/fileupload") //参数中定义MultipartFile参数,用于接收页面提交的type=file类型的表单,表单名称与参数名相同 public String fileupload(MultipartFile file,MultipartFile file1,MultipartFile file2, HttpServletRequest request) throws IOException { System.out.println("file upload is running ..."+file); // MultipartFile参数中封装了上传的文件的相关信息 // System.out.println(file.getSize()); // System.out.println(file.getBytes().length); // System.out.println(file.getContentType()); // System.out.println(file.getName()); // System.out.println(file.getOriginalFilename()); // System.out.println(file.isEmpty()); //首先判断是否是空文件,也就是存储空间占用为0的文件 if(!file.isEmpty()){ //如果大小在范围要求内正常处理,否则抛出自定义异常告知用户(未实现) //获取原始上传的文件名,可以作为当前文件的真实名称保存到数据库中备用 String fileName = file.getOriginalFilename(); //设置保存的路径 String realPath = request.getServletContext().getRealPath("/images"); //保存文件的方法,通常文件名使用随机生成策略产生,避免文件名冲突问题 file.transferTo(new File(realPath,file.getOriginalFilename())); } //测试一次性上传多个文件 if(!file1.isEmpty()){ String fileName = file1.getOriginalFilename(); //可以根据需要,对不同种类的文件做不同的存储路径的区分,修改对应的保存位置即可 String realPath = request.getServletContext().getRealPath("/images"); file1.transferTo(new File(realPath,file1.getOriginalFilename())); } if(!file2.isEmpty()){ String fileName = file2.getOriginalFilename(); String realPath = request.getServletContext().getRealPath("/images"); file2.transferTo(new File(realPath,file2.getOriginalFilename())); } return "page.jsp"; } } ``` **** ### 源码解析 StandardServletMultipartResolver 是文件上传解析器 DispatcherServlet#doDispatch: ```java protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { // 判断当前请求是不是文件上传请求 processedRequest = checkMultipart(request); // 文件上传请求会对 request 进行包装,导致两者不相等,此处赋值为 true,代表已经被解析 multipartRequestParsed = (processedRequest != request); } ``` DispatcherServlet#checkMultipart: * `if (this.multipartResolver != null && this.multipartResolver.isMultipart(request))`:判断是否是文件请求 * `StandardServletMultipartResolver#isMultipart`:根据开头是否符合 multipart/form-data 或者 multipart/ * `return this.multipartResolver.resolveMultipart(request)`:把请求封装成 StandardMultipartHttpServletRequest 对象 开始执行 ha.handle() 目标方法进行数据的解析 * RequestPartMethodArgumentResolver#supportsParameter:支持解析文件上传数据 ```java public boolean supportsParameter(MethodParameter parameter) { // 参数上有 @RequestPart 注解 if (parameter.hasParameterAnnotation(RequestPart.class)) { return true; } } ``` * RequestPartMethodArgumentResolver#resolveArgument:解析参数数据,封装成 MultipartFile 对象 * `RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class)`:获取注解的相关信息 * `String name = getPartName(parameter, requestPart)`:获取上传文件的名字 * `Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument()`:解析参数 * `List files = multipartRequest.getFiles(name)`:获取文件的所有数据 * `return doInvoke(args)`:解析完成执行自定义的方法,完成上传功能 *** ## 实用技术 ### 校验框架 #### 校验概述 表单校验保障了数据有效性、安全性 校验分类:客户端校验和服务端校验 * 格式校验 * 客户端:使用 js 技术,利用正则表达式校验 * 服务端:使用校验框架 * 逻辑校验 * 客户端:使用ajax发送要校验的数据,在服务端完成逻辑校验,返回校验结果 * 服务端:接收到完整的请求后,在执行业务操作前,完成逻辑校验 表单校验框架: * JSR(Java Specification Requests):Java 规范提案 * 303:提供bean属性相关校验规则 * JCP(Java Community Process):Java社区 * Hibernate框架中包含一套独立的校验框架hibernate-validator * 导入坐标: ```xml javax.validation validation-api 2.0.1.Final org.hibernate hibernate-validator 6.1.0.Final ``` **注意:** * tomcat7:搭配 hibernate-validator 版本 5.*.*.Final * tomcat8.5:搭配 hibernate-validator 版本 6.*.*.Final *** #### 基本使用 ##### 开启校验 名称:@Valid、@Validated 类型:形参注解 位置:处理器类中的实体类类型的方法形参前方 作用:设定对当前实体类类型参数进行校验 范例: ```java @RequestMapping(value = "/addemployee") public String addEmployee(@Valid Employee employee) { System.out.println(employee); } ``` ##### 校验规则 名称:@NotNull 类型:属性注解等 位置:实体类属性上方 作用:设定当前属性校验规则 范例:每个校验规则所携带的参数不同,根据校验规则进行相应的调整,具体的校验规则查看对应的校验框架进行获取 ```java public class Employee{ @NotNull(message = "姓名不能为空") private String name;//员工姓名 } ``` ##### 错误信息 ```java @RequestMapping(value = "/addemployee") //Errors对象用于封装校验结果,如果不满足校验规则,对应的校验结果封装到该对象中,包含校验的属性名和校验不通过返回的消息 public String addEmployee(@Valid Employee employee, Errors errors, Model model){ System.out.println(employee); //判定Errors对象中是否存在未通过校验的字段 if(errors.hasErrors()){ for(FieldError error : errors.getFieldErrors()){ //将校验结果添加到Model对象中,用于页面显示,返回json数据即可 model.addAttribute(error.getField(),error.getDefaultMessage()); } //当出现未通过校验的字段时,跳转页面到原始页面,进行数据回显 return "addemployee.jsp"; } return "success.jsp"; } ``` 通过形参Errors获取校验结果数据,通过Model接口将数据封装后传递到页面显示,页面获取后台封装的校验结果信息 ```html
员工姓名:${name}
员工年龄:${age}

``` **** #### 多规则校验 * 同一个属性可以添加多个校验器 ```java public class Employee{ @NotBlank(message = "姓名不能为空") private String name;//员工姓名 @NotNull(message = "请输入年龄") @Max(value = 60,message = "年龄最大值60") @Min(value = 18,message = "年龄最小值18") private Integer age;//员工年龄 } ``` * 三种判定空校验器的区别 ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringMVC-三种判定空检验器的区别.png) *** #### 嵌套校验 名称:@Valid 类型:属性注解 位置:实体类中的引用类型属性上方 作用:设定当前应用类型属性中的属性开启校验 范例: ```java public class Employee { //实体类中的引用类型通过标注@Valid注解,设定开启当前引用类型字段中的属性参与校验 @Valid private Address address; } ``` 注意:开启嵌套校验后,被校验对象内部需要添加对应的校验规则 ```java //嵌套校验的实体中,对每个属性正常添加校验规则即可 public class Address implements Serializable { @NotBlank(message = "请输入省份名称") private String provinceName;//省份名称 @NotBlank(message = "请输入邮政编码") @Size(max = 6,min = 6,message = "邮政编码由6位组成") private String zipCode;//邮政编码 } ``` *** #### 分组校验 分组校验的介绍 * 同一个模块,根据执行的业务不同,需要校验的属性会有不同 * 新增用户 * 修改用户 * 对不同种类的属性进行分组,在校验时可以指定参与校验的字段所属的组类别 * 定义组(通用) * 为属性设置所属组,可以设置多个 * 开启组校验 domain: ```java //用于设定分组校验中的组名,当前接口仅提供字节码,用于识别 public interface GroupOne { } ``` ```java public class Employee{ @NotBlank(message = "姓名不能为空",groups = {GroupA.class}) private String name;//员工姓名 @NotNull(message = "请输入年龄",groups = {GroupA.class}) @Max(value = 60,message = "年龄最大值60")//不加Group的校验不生效 @Min(value = 18,message = "年龄最小值18") private Integer age;//员工年龄 @Valid private Address address; //...... } ``` controller: ```java @Controller public class EmployeeController { @RequestMapping(value = "/addemployee") public String addEmployee(@Validated({GroupA.class}) Employee employee, Errors errors, Model m){ if(errors.hasErrors()){ List fieldErrors = errors.getFieldErrors(); System.out.println(fieldErrors.size()); for(FieldError error : fieldErrors){ m.addAttribute(error.getField(),error.getDefaultMessage()); } return "addemployee.jsp"; } return "success.jsp"; } } ``` jsp: ```html
<%--页面使用${}获取后台传递的校验信息--%> 员工姓名:${name}
员工年龄:${age}
<%--注意,引用类型的校验未通过信息不是通过对象进行封装的,直接使用对象名.属性名的格式作为整体属性字符串进行保存的,和使用者的属性传递方式有关,不具有通用性,仅适用于本案例--%> 省:${requestScope['address.provinceName']}
/form> ``` **** ### Lombok Lombok 用标签方式代替构造器、getter/setter、toString() 等方法 引入依赖: ```xml org.projectlombok lombok ``` 下载插件:IDEA 中 File → Settings → Plugins,搜索安装 Lombok 插件 常用注解: ```java @NoArgsConstructor // 无参构造 @AllArgsConstructor // 全参构造 @Data // set + get @ToString // toString @EqualsAndHashCode // hashConde + equals ``` 简化日志: ```java @Slf4j @RestController public class HelloController { @RequestMapping("/hello") public String handle01(@RequestParam("name") String name){ log.info("请求进来了...."); return "Hello, Spring!" + "你好:" + name; } } ``` **** # Boot ## 基本介绍 ### Boot介绍 SpringBoot 提供了一种快速使用 Spring 的方式,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率 SpringBoot 功能: * 自动配置,自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素选择使用哪个配置,该过程是SpringBoot 自动完成的 * 起步依赖,起步依赖本质上是一个 Maven 项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能 * 辅助功能,提供了一些大型项目中常见的非功能性特性,如内嵌 web 服务器、安全、指标,健康检测、外部配置等 参考视频:https://www.bilibili.com/video/BV19K4y1L7MT *** ### 构建工程 普通构建: 1. 创建 Maven 项目 2. 导入 SpringBoot 起步依赖 ```xml org.springframework.boot spring-boot-starter-parent 2.1.8.RELEASE org.springframework.boot spring-boot-starter-web ``` 3. 定义 Controller ```java @RestController public class HelloController { @RequestMapping("/hello") public String hello(){ return " hello Spring Boot !"; } } ``` 4. 编写引导类 ```java // 引导类,SpringBoot项目的入口 @SpringBootApplication public class HelloApplication { public static void main(String[] args) { SpringApplication.run(HelloApplication.class, args); } } ``` 快速构建: ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-IDEA构建工程.png) *** ## 自动装配 ### 依赖管理 在 spring-boot-starter-parent 中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。在各种 starter 中,定义了完成该功能需要的坐标合集,其中大部分版本信息来自于父工程。工程继承 parent,引入 starter 后,通过依赖传递,就可以简单方便获得需要的 jar 包,并且不会存在版本冲突,自动版本仲裁机制 *** ### 底层注解 #### SpringBoot @SpringBootApplication:启动注解,实现 SpringBoot 的自动部署 * 参数 scanBasePackages:可以指定扫描范围 * 默认扫描当前引导类所在包及其子包 假如所在包为 com.example.springbootenable,扫描配置包 com.example.config 的信息,三种解决办法: 1. 使用 @ComponentScan 扫描 com.example.config 包 2. 使用 @Import 注解加载类,这些类都会被 Spring 创建并放入 ioc 容器,默认组件的名字就是**全类名** 3. 对 @Import 注解进行封装 ```java //1.@ComponentScan("com.example.config") //2.@Import(UserConfig.class) @EnableUser @SpringBootApplication public class SpringbootEnableApplication { public static void main(String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableApplication.class, args); //获取Bean Object user = context.getBean("user"); System.out.println(user); } } ``` UserConfig: ```java @Configuration public class UserConfig { @Bean public User user() { return new User(); } } ``` EnableUser 注解类: ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(UserConfig.class)//@Import注解实现Bean的动态加载 public @interface EnableUser { } ``` *** #### Configuration @Configuration:设置当前类为 SpringBoot 的配置类 * proxyBeanMethods = true:Full 全模式,每个 @Bean 方法被调用多少次返回的组件都是单实例的,默认值,类组件之间**有依赖关系**,方法会被调用得到之前单实例组件 * proxyBeanMethods = false:Lite 轻量级模式,每个 @Bean 方法被调用多少次返回的组件都是新创建的,类组件之间**无依赖关系**用 Lite 模式加速容器启动过程 ```java @Configuration(proxyBeanMethods = true) public class MyConfig { @Bean //给容器中添加组件。以方法名作为组件的 id。返回类型就是组件类型。返回的值,就是组件在容器中的实例 public User user(){ User user = new User("zhangsan", 18); return user; } } ``` *** #### Condition ##### 条件注解 Condition 是 Spring4.0 后引入的条件化配置接口,通过实现 Condition 接口可以完成有条件的加载相应的 Bean 注解:@Conditional 作用:条件装配,满足 Conditional 指定的条件则进行组件注入,加上方法或者类上,作用范围不同 使用:@Conditional 配合 Condition 的实现类(ClassCondition)进行使用 ConditionContext 类API: | 方法 | 说明 | | ------------------------------------------------- | ----------------------------- | | ConfigurableListableBeanFactory getBeanFactory() | 获取到 IOC 使用的 beanfactory | | ClassLoader getClassLoader() | 获取类加载器 | | Environment getEnvironment() | 获取当前环境信息 | | BeanDefinitionRegistry getRegistry() | 获取到 bean 定义的注册类 | * ClassCondition: ```java public class ClassCondition implements Condition { /** * context 上下文对象。用于获取环境,IOC容器,ClassLoader对象 * metadata 注解元对象。 可以用于获取注解定义的属性值 */ @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { //1.需求: 导入Jedis坐标后创建Bean //思路:判断redis.clients.jedis.Jedis.class文件是否存在 boolean flag = true; try { Class cls = Class.forName("redis.clients.jedis.Jedis"); } catch (ClassNotFoundException e) { flag = false; } return flag; } } ``` * UserConfig: ```java @Configuration public class UserConfig { @Bean @Conditional(ClassCondition.class) public User user(){ return new User(); } } ``` * 启动类: ```java @SpringBootApplication public class SpringbootConditionApplication { public static void main(String[] args) { //启动SpringBoot应用,返回Spring的IOC容器 ConfigurableApplicationContext context = SpringApplication.run(SpringbootConditionApplication.class, args); Object user = context.getBean("user"); System.out.println(user); } } ``` *** ##### 自定义注解 将类的判断定义为动态的,判断哪个字节码文件存在可以动态指定 * 自定义条件注解类 ```java @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(ClassCondition.class) public @interface ConditionOnClass { String[] value(); } ``` * ClassCondition ```java public class ClassCondition implements Condition { @Override public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) { //需求:通过注解属性值value指定坐标后创建bean Map map = metadata.getAnnotationAttributes (ConditionOnClass.class.getName()); //map = {value={属性值}} //获取所有的 String[] value = (String[]) map.get("value"); boolean flag = true; try { for (String className : value) { Class cls = Class.forName(className); } } catch (Exception e) { flag = false; } return flag; } } ``` * UserConfig ```java @Configuration public class UserConfig { @Bean @ConditionOnClass("com.alibaba.fastjson.JSON")//JSON加载了才注册 User 到容器 public User user(){ return new User(); } } ``` * 测试 User 对象的创建 *** ##### 常用注解 SpringBoot 提供的常用条件注解: @ConditionalOnProperty:判断**配置文件**中是否有对应属性和值才初始化 Bean ```java @Configuration public class UserConfig { @Bean @ConditionalOnProperty(name = "it", havingValue = "seazean") public User user() { return new User(); } } ``` ```properties it=seazean ``` @ConditionalOnClass:判断环境中是否有对应类文件才初始化 Bean @ConditionalOnMissingClass:判断环境中是否有对应类文件才初始化 Bean @ConditionalOnMissingBean:判断环境中没有对应Bean才初始化 Bean ***** #### ImportRes 使用 bean.xml 文件生成配置 bean,如果需要继续复用 bean.xml,@ImportResource 导入配置文件即可 ```java @ImportResource("classpath:beans.xml") public class MyConfig { //... } ``` ```xml ``` **** #### Properties @ConfigurationProperties:读取到 properties 文件中的内容,并且封装到 JavaBean 中 配置文件: ```properties mycar.brand=BYD mycar.price=100000 ``` JavaBean 类: ```java @Component //导入到容器内 @ConfigurationProperties(prefix = "mycar")//代表配置文件的前缀 public class Car { private String brand; private Integer price; } ``` *** ### 装配原理 #### 启动流程 应用启动: ```java @SpringBootApplication public class BootApplication { public static void main(String[] args) { // 启动代码 SpringApplication.run(BootApplication.class, args); } } ``` SpringApplication 构造方法: * `this.resourceLoader = resourceLoader`:资源加载器,初始为 null * `this.webApplicationType = WebApplicationType.deduceFromClasspath()`:判断当前应用的类型,是响应式还是 Web 类 * `this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories()`:**获取引导器** * 去 **`META-INF/spring.factories`** 文件中找 org.springframework.boot.Bootstrapper * 寻找的顺序:classpath → spring-beans → boot-devtools → springboot → boot-autoconfigure * `setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class))`:**获取初始化器** * 去 `META-INF/spring.factories` 文件中找 org.springframework.context.ApplicationContextInitializer * `setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))`:**获取监听器** * 去 `META-INF/spring.factories` 文件中找 org.springframework.context.ApplicationListener * `this.mainApplicationClass = deduceMainApplicationClass()`:获取出 main 程序类 SpringApplication#run(String... args):创建 IOC 容器并实现了自动装配 * `StopWatch stopWatch = new StopWatch()`:停止监听器,**监控整个应用的启停** * `stopWatch.start()`:记录应用的启动时间 * `bootstrapContext = createBootstrapContext()`:**创建引导上下文环境** * `bootstrapContext = new DefaultBootstrapContext()`:创建默认的引导类环境 * `this.bootstrapRegistryInitializers.forEach()`:遍历所有的引导器调用 initialize 方法完成初始化设置 * `configureHeadlessProperty()`:让当前应用进入 headless 模式 * `listeners = getRunListeners(args)`:**获取所有 RunListener(运行监听器)** * 去 `META-INF/spring.factories` 文件中找 org.springframework.boot.SpringApplicationRunListener * `listeners.starting(bootstrapContext, this.mainApplicationClass)`:遍历所有的运行监听器调用 starting 方法 * `applicationArguments = new DefaultApplicationArguments(args)`:获取所有的命令行参数 * `environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments)`:**准备环境** * `environment = getOrCreateEnvironment()`:返回或创建基础环境信息对象 * `switch (this.webApplicationType)`:**根据当前应用的类型创建环境** * `case SERVLET`:Web 应用环境对应 ApplicationServletEnvironment * `case REACTIVE`:响应式编程对应 ApplicationReactiveWebEnvironment * `default`:默认为 Spring 环境 ApplicationEnvironment * `configureEnvironment(environment, applicationArguments.getSourceArgs())`:读取所有配置源的属性值配置环境 * `ConfigurationPropertySources.attach(environment)`:属性值绑定环境信息 * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 放入环境的属性信息头部 * `listeners.environmentPrepared(bootstrapContext, environment)`:运行监听器调用 environmentPrepared(),EventPublishingRunListener 发布事件通知所有的监听器当前环境准备完成 * `DefaultPropertiesPropertySource.moveToEnd(environment)`:移动 defaultProperties 属性源到环境中的最后一个源 * `bindToSpringApplication(environment)`:与容器绑定当前环境 * `ConfigurationPropertySources.attach(environment)`:重新将属性值绑定环境信息 * `sources.remove(ATTACHED_PROPERTY_SOURCE_NAME)`:从环境信息中移除 configurationProperties * `sources.addFirst(ATTACHED_PROPERTY_SOURCE_NAME,..)`:把 configurationProperties 重新放入环境信息 * `configureIgnoreBeanInfo(environment)`:**配置忽略的 bean** * `printedBanner = printBanner(environment)`:打印 SpringBoot 标志 * `context = createApplicationContext()`:**创建 IOC 容器** `switch (this.webApplicationType)`:根据当前应用的类型创建 IOC 容器 * `case SERVLET`:Web 应用环境对应 AnnotationConfigServletWebServerApplicationContext * `case REACTIVE`:响应式编程对应 AnnotationConfigReactiveWebServerApplicationContext * `default`:默认为 Spring 环境 AnnotationConfigApplicationContext * `context.setApplicationStartup(this.applicationStartup)`:设置一个启动器 * `prepareContext()`:配置 IOC 容器的基本信息 * `postProcessApplicationContext(context)`:后置处理流程 * `applyInitializers(context)`:获取所有的**初始化器调用 initialize() 方法**进行初始化 * `listeners.contextPrepared(context)`:所有的运行监听器调用 environmentPrepared() 方法,EventPublishingRunListener 发布事件通知 IOC 容器准备完成 * `listeners.contextLoaded(context)`:所有的运行监听器调用 contextLoaded() 方法,通知 IOC 加载完成 * `refreshContext(context)`:**刷新 IOC 容器** * Spring 的容器启动流程 * `invokeBeanFactoryPostProcessors(beanFactory)`:**实现了自动装配** * `onRefresh()`:**创建 WebServer** 使用该接口 * `afterRefresh(context, applicationArguments)`:留给用户自定义容器刷新完成后的处理逻辑 * `stopWatch.stop()`:记录应用启动完成的时间 * `callRunners(context, applicationArguments)`:调用所有 runners * `listeners.started(context)`:所有的运行监听器调用 started() 方法 * `listeners.running(context)`:所有的运行监听器调用 running() 方法 * 获取容器中的 ApplicationRunner、CommandLineRunner * `AnnotationAwareOrderComparator.sort(runners)`:合并所有 runner 并且按照 @Order 进行排序 * `callRunner()`:遍历所有的 runner,调用 run 方法 * `handleRunFailure(context, ex, listeners)`:**处理异常**,出现异常进入该逻辑 * `handleExitCode(context, exception)`:处理错误代码 * `listeners.failed(context, exception)`:运行监听器调用 failed() 方法 * `reportFailure(getExceptionReporters(context), exception)`:通知异常 **** #### 注解分析 SpringBoot 定义了一套接口规范,这套规范规定 SpringBoot 在启动时会扫描外部引用 jar 包中的 `META-INF/spring.factories` 文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作,对于外部的 jar 包,直接引入一个 starter 即可 @SpringBootApplication 注解是 `@SpringBootConfiguration`、`@EnableAutoConfiguration`、`@ComponentScan` 注解的集合 * @SpringBootApplication 注解 ```java @Inherited @SpringBootConfiguration //代表 @SpringBootApplication 拥有了该注解的功能 @EnableAutoConfiguration //同理 @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) // 扫描被 @Component (@Service,@Controller)注解的 bean,容器中将排除TypeExcludeFilter 和 AutoConfigurationExcludeFilter public @interface SpringBootApplication { } ``` * @SpringBootConfiguration 注解: ```java @Configuration // 代表是配置类 @Indexed public @interface SpringBootConfiguration { @AliasFor(annotation = Configuration.class) boolean proxyBeanMethods() default true; } ``` @AliasFor 注解:表示别名,可以注解到自定义注解的两个属性上表示这两个互为别名,两个属性其实是同一个含义相互替代 * @ComponentScan 注解:默认扫描当前类所在包及其子级包下的所有文件 * **@EnableAutoConfiguration 注解:启用 SpringBoot 的自动配置机制** ````java @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class[] exclude() default {}; String[] excludeName() default {}; } ```` * @AutoConfigurationPackage:**将添加该注解的类所在的 package 作为自动配置 package 进行管理**,把启动类所在的包设置一次,为了给各种自动配置的第三方库扫描用,比如带 @Mapper 注解的类,Spring 自身是不能识别的,但自动配置的 Mybatis 需要扫描用到,而 ComponentScan 只是用来扫描注解类,并没有提供接口给三方使用 ```java @Import(AutoConfigurationPackages.Registrar.class) // 利用 Registrar 给容器中导入组件 public @interface AutoConfigurationPackage { String[] basePackages() default {}; //自动配置包,指定了配置类的包 Class[] basePackageClasses() default {}; } ``` `register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]))`:注册 BD * `new PackageImports(metadata).getPackageNames()`:获取添加当前注解的类的所在包 * `registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames))`:存放到容器中 * `new BasePackagesBeanDefinition(packageNames)`:把当前主类所在的包名封装到该对象中 * @Import(AutoConfigurationImportSelector.class):**自动装配的核心类** 容器刷新时执行:**invokeBeanFactoryPostProcessors()** → invokeBeanDefinitionRegistryPostProcessors() → postProcessBeanDefinitionRegistry() → processConfigBeanDefinitions() → parse() → process() → processGroupImports() → getImports() → process() → **AutoConfigurationImportSelector#getAutoConfigurationEntry()** ```java protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } // 获取注解属性,@SpringBootApplication 注解的 exclude 属性和 excludeName 属性 AnnotationAttributes attributes = getAttributes(annotationMetadata); // 获取所有需要自动装配的候选项 List configurations = getCandidateConfigurations(annotationMetadata, attributes); // 去除重复的选项 configurations = removeDuplicates(configurations); // 获取注解配置的排除的自动装配类 Set exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); // 移除所有的配置的不需要自动装配的类 configurations.removeAll(exclusions); // 过滤,条件装配 configurations = getConfigurationClassFilter().filter(configurations); // 获取 AutoConfigurationImportListener 类的监听器调用 onAutoConfigurationImportEvent 方法 fireAutoConfigurationImportEvents(configurations, exclusions); // 包装成 AutoConfigurationEntry 返回 return new AutoConfigurationEntry(configurations, exclusions); } ``` AutoConfigurationImportSelector#getCandidateConfigurations:**获取自动配置的候选项** * `List configurations = SpringFactoriesLoader.loadFactoryNames()`:加载自动配置类 参数一:`getSpringFactoriesLoaderFactoryClass()`:获取 @EnableAutoConfiguration 注解类 参数二:`getBeanClassLoader()`:获取类加载器 * `factoryTypeName = factoryType.getName()`:@EnableAutoConfiguration 注解的全类名 * `return loadSpringFactories(classLoaderToUse).getOrDefault()`:加载资源 * `urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION)`:获取资源类 * `FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"`:**加载的资源的位置** * `return configurations`:返回所有自动装配类的候选项 * 从 spring-boot-autoconfigure-2.5.3.jar/META-INF/spring.factories 文件中寻找 EnableAutoConfiguration 字段,获取自动装配类,**进行条件装配,按需装配** ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-自动装配配置文件.png) *** #### 装配流程 Spring Boot 通过 `@EnableAutoConfiguration` 开启自动装配,通过 SpringFactoriesLoader 加载 `META-INF/spring.factories` 中的自动配置类实现自动装配,自动配置类其实就是通过 `@Conditional` 注解按需加载的配置类,想要其生效必须引入 `spring-boot-starter-xxx` 包实现起步依赖 * SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration * 每个自动配置类进行**条件装配**,默认都会绑定配置文件指定的值(xxxProperties 和配置文件进行了绑定) * SpringBoot 默认会在底层配好所有的组件,如果用户自己配置了**以用户的优先** * **定制化配置:** - 用户可以使用 @Bean 新建自己的组件来替换底层的组件 - 用户可以去看这个组件是获取的配置文件前缀值,在配置文件中修改 以 DispatcherServletAutoConfiguration 为例: ```java @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) // 类中的 Bean 默认不是单例 @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) // 条件装配,环境中有 DispatcherServlet 类才进行自动装配 @ConditionalOnClass(DispatcherServlet.class) @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration { // 注册的 DispatcherServlet 的 BeanName public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet"; @Configuration(proxyBeanMethods = false) @Conditional(DefaultDispatcherServletCondition.class) @ConditionalOnClass(ServletRegistration.class) // 绑定配置文件的属性,从配置文件中获取配置项 @EnableConfigurationProperties(WebMvcProperties.class) protected static class DispatcherServletConfiguration { // 给容器注册一个 DispatcherServlet,起名字为 dispatcherServlet @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { // 新建一个 DispatcherServlet 设置相关属性 DispatcherServlet dispatcherServlet = new DispatcherServlet(); // spring.mvc 中的配置项获取注入,没有就填充默认值 dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); // ...... // 返回该对象注册到容器内 return dispatcherServlet; } @Bean // 容器中有这个类型组件才进行装配 @ConditionalOnBean(MultipartResolver.class) // 容器中没有这个名字 multipartResolver 的组件 @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) // 方法名就是 BeanName public MultipartResolver multipartResolver(MultipartResolver resolver) { // 给 @Bean 标注的方法传入了对象参数,这个参数就会从容器中找,因为用户自定义了该类型,以用户配置的优先 // 但是名字不符合规范,所以获取到该 Bean 并返回到容器一个规范的名称:multipartResolver return resolver; } } } ``` ```java // 将配置文件中的 spring.mvc 前缀的属性与该类绑定 @ConfigurationProperties(prefix = "spring.mvc") public class WebMvcProperties { } ``` *** ### 事件监听 SpringBoot 在项目启动时,会对几个监听器进行回调,可以实现监听器接口,在项目启动时完成一些操作 ApplicationContextInitializer、SpringApplicationRunListener、CommandLineRunner、ApplicationRunner * MyApplicationRunner **自定义监听器的启动时机**:MyApplicationRunner 和 MyCommandLineRunner 都是当项目启动后执行,使用 @Component 放入容器即可使用 ```java //当项目启动后执行run方法 @Component public class MyApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println("ApplicationRunner...run"); System.out.println(Arrays.asList(args.getSourceArgs()));//properties配置信息 } } ``` * MyCommandLineRunner ```java @Component public class MyCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println("CommandLineRunner...run"); System.out.println(Arrays.asList(args)); } } ``` * MyApplicationContextInitializer 的启用要**在 resource 文件夹下添加 META-INF/spring.factories** ```properties org.springframework.context.ApplicationContextInitializer=\ com.example.springbootlistener.listener.MyApplicationContextInitializer ``` ```java @Component public class MyApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext applicationContext) { System.out.println("ApplicationContextInitializer....initialize"); } } ``` * MySpringApplicationRunListener 的使用要添加**构造器** ```java public class MySpringApplicationRunListener implements SpringApplicationRunListener { //构造器 public MySpringApplicationRunListener(SpringApplication sa, String[] args) { } @Override public void starting() { System.out.println("starting...项目启动中");//输出SPRING之前 } @Override public void environmentPrepared(ConfigurableEnvironment environment) { System.out.println("environmentPrepared...环境对象开始准备"); } @Override public void contextPrepared(ConfigurableApplicationContext context) { System.out.println("contextPrepared...上下文对象开始准备"); } @Override public void contextLoaded(ConfigurableApplicationContext context) { System.out.println("contextLoaded...上下文对象开始加载"); } @Override public void started(ConfigurableApplicationContext context) { System.out.println("started...上下文对象加载完成"); } @Override public void running(ConfigurableApplicationContext context) { System.out.println("running...项目启动完成,开始运行"); } @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { System.out.println("failed...项目启动失败"); } } ``` *** ## 配置文件 ### 配置方式 #### 文件类型 SpringBoot 是基于约定的,很多配置都有默认值,如果想使用自己的配置替换默认配置,可以使用 application.properties 或者 application.yml(application.yaml)进行配置 * 默认配置文件名称:application * 在同一级目录下优先级为:properties > yml > yaml 例如配置内置 Tomcat 的端口 * properties: ```properties server.port=8080 ``` * yml: ```yaml server: port: 8080 ``` * yaml: ```yaml server: port: 8080 ``` *** #### 加载顺序 所有位置的配置文件都会被加载,互补配置,**高优先级配置内容会覆盖低优先级配置内容** 扫描配置文件的位置按优先级**从高到底**: - `file:./config/`:**当前项目**下的 /config 目录下 - `file:./`:当前项目的根目录,Project工程目录 - `classpath:/config/`:classpath 的 /config 目录 - `classpath:/`:classpath 的根目录,就是 resoureces 目录 项目外部配置文件加载顺序:外部配置文件的使用是为了对内部文件的配合 * 命令行:在 package 打包后的 target 目录下,使用该命令 ```sh java -jar myproject.jar --server.port=9000 ``` * 指定配置文件位置 ```sh java -jar myproject.jar --spring.config.location=e://application.properties ``` * 按优先级从高到底选择配置文件的加载命令 ```sh java -jar myproject.jar ``` *** ### yaml语法 基本语法: - 大小写敏感 - **数据值前边必须有空格,作为分隔符** - 使用缩进表示层级关系 - 缩进时不允许使用Tab键,只允许使用空格(各个系统 Tab对应空格数目可能不同,导致层次混乱) - 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 - ''#" 表示注释,从这个字符一直到行尾,都会被解析器忽略 ```yaml server: port: 8080 address: 127.0.0.1 ``` 数据格式: * 纯量:单个的、不可再分的值 ```yaml msg1: 'hello \n world' # 单引忽略转义字符 msg2: "hello \n world" # 双引识别转义字符 ``` * 对象:键值对集合,Map、Hash ```yaml person: name: zhangsan age: 20 # 行内写法 person: {name: zhangsan} ``` 注意:不建议使用 JSON,应该使用 yaml 语法 * 数组:一组按次序排列的值,List、Array ```yaml address: - beijing - shanghai # 行内写法 address: [beijing,shanghai] ``` ```yaml allPerson #List - {name:lisi, age:18} - {name:wangwu, age:20} # 行内写法 allPerson: [{name:lisi, age:18}, {name:wangwu, age:20}] ``` * 参数引用: ```yaml name: lisi person: name: ${name} # 引用上边定义的name值 ``` *** ### 获取配置 三种获取配置文件的方式: * 注解 @Value ```java @RestController public class HelloController { @Value("${name}") private String name; @Value("${person.name}") private String name2; @Value("${address[0]}") private String address1; @Value("${msg1}") private String msg1; @Value("${msg2}") private String msg2; @RequestMapping("/hello") public String hello(){ System.out.println("所有的数据"); return " hello Spring Boot !"; } } ``` * Evironment 对象 ```java @Autowired private Environment env; @RequestMapping("/hello") public String hello() { System.out.println(env.getProperty("person.name")); System.out.println(env.getProperty("address[0]")); return " hello Spring Boot !"; } ``` * 注解 @ConfigurationProperties 配合 @Component 使用 **注意**:参数 prefix 一定要指定 ```java @Component //不扫描该组件到容器内,无法完成自动装配 @ConfigurationProperties(prefix = "person") public class Person { private String name; private int age; private String[] address; } ``` ```java @Autowired private Person person; @RequestMapping("/hello") public String hello() { System.out.println(person); //Person{name='zhangsan', age=20, address=[beijing, shanghai]} return " hello Spring Boot !"; } ``` *** ### 配置提示 自定义的类和配置文件绑定一般没有提示,添加如下依赖可以使用提示: ```xml org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-maven-plugin org.springframework.boot spring-boot-configuration-processor ``` *** ### Profile @Profile:指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件 * 加了环境标识的 bean,只有这个环境被激活的时候才能注册到容器中,默认是 default 环境 * 写在配置类上,只有是指定的环境的时候,整个配置类里面的所有配置才能开始生效 * 没有标注环境标识的 bean 在,任何环境下都是加载的 Profile 的配置: * **profile 是用来完成不同环境下,配置动态切换功能** * **profile 配置方式**:多 profile 文件方式,提供多个配置文件,每个代表一种环境 * application-dev.properties/yml 开发环境 * application-test.properties/yml 测试环境 * sapplication-pro.properties/yml 生产环境 * yml 多文档方式:在 yml 中使用 --- 分隔不同配置 ```yacas --- server: port: 8081 spring: profiles:dev --- server: port: 8082 spring: profiles:test --- server: port: 8083 spring: profiles:pro --- ``` * **profile 激活方式** * 配置文件:在配置文件中配置:spring.profiles.active=dev ```properties spring.profiles.active=dev ``` * 虚拟机参数:在VM options 指定:`-Dspring.profiles.active=dev` ![](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/SpringBoot-profile激活方式虚拟机参数.png) * 命令行参数:`java –jar xxx.jar --spring.profiles.active=dev` 在 Program arguments 里输入,也可以先 package *** ## Web开发 ### 功能支持 SpringBoot 自动配置了很多约定,大多场景都无需自定义配置 * 内容协商视图解析器 ContentNegotiatingViewResolver 和 BeanName 视图解析器 BeanNameViewResolver * 支持静态资源(包括 webjars)和静态 index.html 页支持 * 自动注册相关类:Converter、GenericConverter、Formatter * 内容协商处理器:HttpMessageConverters * 国际化:MessageCodesResolver 开发规范: * 使用 `@Configuration` + `WebMvcConfigurer` 自定义规则,不使用 `@EnableWebMvc` 注解 * 声明 `WebMvcRegistrations` 的实现类改变默认底层组件 * 使用 `@EnableWebMvc` + `@Configuration` + `DelegatingWebMvcConfiguration` 全面接管 SpringMVC **** ### 静态资源 #### 访问规则 默认的静态资源路径是 classpath 下的,优先级由高到低为:/META-INF/resources、/resources、 /static、/public 的包内,`/` 表示当前项目的根路径 静态映射 `/**` ,表示请求 `/ + 静态资源名` 就直接去默认的资源路径寻找请求的资源 处理原理:静请求去寻找 Controller 处理,不能处理的请求就会交给静态资源处理器,静态资源也找不到就响应 404 页面 * 修改默认资源路径: ```yaml spring: web: resources: static-locations:: [classpath:/haha/] ``` * 修改静态资源访问前缀,默认是 `/**`: ```yaml spring: mvc: static-path-pattern: /resources/** ``` 访问 URL:http://localhost:8080/resources/ + 静态资源名,将所有资源**重定位**到 `/resources/` * webjar 访问资源: ```xml org.webjars jquery 3.5.1 ``` 访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js,后面地址要按照依赖里面的包路径 **** #### 欢迎页面 静态资源路径下 index.html 默认作为欢迎页面,访问 http://localhost:8080 出现该页面,使用 welcome page 功能不能修改前缀 网页标签上的小图标可以自定义规则,把资源重命名为 favicon.ico 放在静态资源目录下即可 *** #### 源码分析 SpringMVC 功能的自动配置类 WebMvcAutoConfiguration: ```java public class WebMvcAutoConfiguration { //当前项目的根路径 private static final String SERVLET_LOCATION = "/"; } ``` * 内部类 WebMvcAutoConfigurationAdapter: ```java @Import(EnableWebMvcConfiguration.class) // 绑定 spring.mvc、spring.web、spring.resources 相关的配置属性 @EnableConfigurationProperties({ WebMvcProperties.class,ResourceProperties.class, WebProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware { //有参构造器所有参数的值都会从容器中确定 public WebMvcAutoConfigurationAdapter(/*参数*/) { this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources(); this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.dispatcherServletPath = dispatcherServletPath; this.servletRegistrations = servletRegistrations; this.mvcProperties.checkConfiguration(); } } ``` * ResourceProperties resourceProperties:获取和 spring.resources 绑定的所有的值的对象 * WebMvcProperties mvcProperties:获取和 spring.mvc 绑定的所有的值的对象 * ListableBeanFactory beanFactory:Spring 的 beanFactory * HttpMessageConverters:找到所有的 HttpMessageConverters * ResourceHandlerRegistrationCustomizer:找到 资源处理器的自定义器。 * DispatcherServletPath:项目路径 * ServletRegistrationBean:给应用注册 Servlet、Filter * WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.addResourceHandler():两种静态资源映射规则 ```java public void addResourceHandlers(ResourceHandlerRegistry registry) { //配置文件设置 spring.resources.add-mappings: false,禁用所有静态资源 if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled");//被禁用 return; } //注册webjars静态资源的映射规则 映射 路径 addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); //注册静态资源路径的映射规则 默认映射 staticPathPattern = "/**" addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { //staticLocations = CLASSPATH_RESOURCE_LOCATIONS registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (this.servletContext != null) { ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION); registration.addResourceLocations(resource); } }); } ``` ```java @ConfigurationProperties("spring.web") public class WebProperties { public static class Resources { //默认资源路径,优先级从高到低 static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" } private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; //可以进行规则重写 public void setStaticLocations(String[] staticLocations) { this.staticLocations = appendSlashIfNecessary(staticLocations); this.customized = true; } } } ``` * WebMvcAutoConfiguration.EnableWebMvcConfiguration.welcomePageHandlerMapping():欢迎页 ```java //spring.web 属性 @EnableConfigurationProperties(WebProperties.class) public static class EnableWebMvcConfiguration { @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(/*参数*/) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), //staticPathPattern = "/**" this.mvcProperties.getStaticPathPattern()); return welcomePageHandlerMapping; } } WelcomePageHandlerMapping(/*参数*/) { //所以限制 staticPathPattern 必须为 /** 才能启用该功能 if (welcomePage != null && "/**".equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage); //重定向 setRootViewName("forward:index.html"); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { logger.info("Adding welcome page template: index"); setRootViewName("index"); } } ``` WelcomePageHandlerMapping,访问 / 能访问到 index.html *** ### Rest映射 开启 Rest 功能 ```yaml spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能 ``` 源码分析,注入了 HiddenHttpMethodFilte 解析 Rest 风格的访问: ```java public class WebMvcAutoConfiguration { @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled") public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } } ``` 详细源码解析:SpringMVC → 基本操作 → Restful → 识别原理 Web 部分源码详解:SpringMVC → 运行原理 **** ### 内嵌容器 SpringBoot 嵌入式 Servlet 容器,默认支持的 WebServe:Tomcat、Jetty、Undertow 配置方式: ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat org.springframework.boot spring-boot-starter-jetty ``` Web 应用启动,SpringBoot 导入 Web 场景包 tomcat,创建一个 Web 版的 IOC 容器: * `SpringApplication.run(BootApplication.class, args)`:应用启动 * `ConfigurableApplicationContext.run()`: * `context = createApplicationContext()`:**创建容器** * `applicationContextFactory = ApplicationContextFactory.DEFAULT` ```java ApplicationContextFactory DEFAULT = (webApplicationType) -> { try { switch (webApplicationType) { case SERVLET: // Servlet 容器,继承自 ServletWebServerApplicationContext return new AnnotationConfigServletWebServerApplicationContext(); case REACTIVE: // 响应式编程 return new AnnotationConfigReactiveWebServerApplicationContext(); default: // 普通 Spring 容器 return new AnnotationConfigApplicationContext(); } } catch (Exception ex) { throw new IllegalStateException(); } } ``` * `applicationContextFactory.create(this.webApplicationType)`:根据应用类型创建容器 * `refreshContext(context)`:容器启动刷新 内嵌容器工作流程: - Spring 容器启动逻辑中,在实例化非懒加载的单例 Bean 之前有一个方法 **onRefresh()**,留给子类去扩展,Web 容器就是重写这个方法创建 WebServer ```java protected void onRefresh() { //省略.... createWebServer(); } private void createWebServer() { ServletWebServerFactory factory = getWebServerFactory(); this.webServer = factory.getWebServer(getSelfInitializer()); createWebServer.end(); } ``` 获取 WebServer 工厂 ServletWebServerFactory,并且获取的数量不等于 1 会报错,Spring 底层有三种: `TomcatServletWebServerFactory`、`JettyServletWebServerFactory`、`UndertowServletWebServerFactory` - **自动配置类 ServletWebServerFactoryAutoConfiguration** 导入了 ServletWebServerFactoryConfiguration(配置类),根据条件装配判断系统中到底导入了哪个 Web 服务器的包,创建出服务器并启动 - 默认是 web-starter 导入 tomcat 包,容器中就有 TomcatServletWebServerFactory,创建出 Tomcat 服务器并启动, ```java public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { // 初始化 initialize(); } ``` 初始化方法 initialize 中有启动方法:`this.tomcat.start()` *** ### 自定义 #### 定制规则 ```java @Configuration public class MyWebMvcConfigurer implements WebMvcConfigurer { @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { //进行一些方法重写,来实现自定义的规则 //比如添加一些解析器和拦截器,就是对原始容器功能的增加 } } //也可以不加 @Bean,直接从这里重写方法进行功能增加 } ``` *** #### 定制容器 @EnableWebMvc:全面接管 SpringMVC,所有规则全部自己重新配置 - @EnableWebMvc + WebMvcConfigurer + @Bean 全面接管SpringMVC - @Import(DelegatingWebMvcConfiguration.**class**),该类继承 WebMvcConfigurationSupport,自动配置了一些非常底层的组件,只能保证 SpringMVC 最基本的使用 原理:自动配置类 **WebMvcAutoConfiguration** 里面的配置要能生效,WebMvcConfigurationSupport 类不能被加载,所以 @EnableWebMvc 导致配置类失效,从而接管了 SpringMVC ```java @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) public class WebMvcAutoConfiguration {} ``` 注意:一般不适用此注解 *** ## 数据访问 ### JDBC #### 基本使用 导入 starter: ```xml org.springframework.boot spring-boot-starter-data-jdbc mysql mysql-connector-java ``` 单独导入 MySQL 驱动是因为不确定用户使用的什么数据库 配置文件: ```yaml spring: datasource: url: jdbc:mysql://192.168.0.107:3306/db1?useSSL=false # 不加 useSSL 会警告 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver ``` 测试文件: ```java @Slf4j @SpringBootTest class Boot05WebAdminApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test void contextLoads() { Long res = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class); log.info("记录总数:{}", res); } } ``` **** #### 自动配置 DataSourceAutoConfiguration:数据源的自动配置 ```java @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class}) protected static class PooledDataSourceConfiguration {} } // 配置项 @ConfigurationProperties(prefix = "spring.datasource") public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {} ``` - 底层默认配置好的连接池是:**HikariDataSource** - 数据库连接池的配置,是容器中没有 DataSource 才自动配置的 - 修改数据源相关的配置:spring.datasource 相关配置: - DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置 - JdbcTemplateAutoConfiguration: JdbcTemplate 的自动配置 - 可以修改这个配置项 @ConfigurationProperties(prefix = **"spring.jdbc"**) 来修改JdbcTemplate - `@AutoConfigureAfter(DataSourceAutoConfiguration.class)`:在 DataSource 装配后装配 - JndiDataSourceAutoConfiguration: jndi 的自动配置 - XADataSourceAutoConfiguration: 分布式事务相关 **** ### Druid 导入坐标: ```xml com.alibaba druid-spring-boot-starter 1.1.17 ``` ```java @Configuration @ConditionalOnClass(DruidDataSource.class) @AutoConfigureBefore(DataSourceAutoConfiguration.class) @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class}) @Import({DruidSpringAopConfiguration.class, DruidStatViewServletConfiguration.class, DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class}) public class DruidDataSourceAutoConfigure {} ``` 自动配置: - 扩展配置项 **spring.datasource.druid** - DruidSpringAopConfiguration: 监控 SpringBean,配置项为 `spring.datasource.druid.aop-patterns` - DruidStatViewServletConfiguration:监控页的配置项为 `spring.datasource.druid.stat-view-servlet`,默认开启 - DruidWebStatFilterConfiguration:Web 监控配置项为 `spring.datasource.druid.web-stat-filter`,默认开启 - DruidFilterConfiguration:所有 Druid 自己 filter 的配置 配置示例: ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* #监控SpringBean filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin #项目启动访问:http://localhost:8080/druid ,账号和密码是 admin login-password: admin resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: # 对上面filters里面的stat的详细配置 slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false ``` 配置示例:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter 配置项列表:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8 **** ### MyBatis #### 基本使用 导入坐标: ```xml org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.4 ``` * 编写 MyBatis 相关配置:application.yml ```yaml # 配置mybatis规则 mybatis: # config-location: classpath:mybatis/mybatis-config.xml 建议不写 mapper-locations: classpath:mybatis/mapper/*.xml configuration: map-underscore-to-camel-case: true #可以不写全局配置文件,所有全局配置文件的配置都放在 configuration 配置项中即可 ``` * 定义表和实体类 ```java public class User { private int id; private String username; private String password; } ``` * 编写 dao 和 mapper 文件/纯注解开发 dao:**@Mapper 注解必须加,使用自动装配的 package,否则在启动类指定 @MapperScan() 扫描路径(不建议)** ```java @Mapper //必须加Mapper @Repository public interface UserXmlMapper { public List findAll(); } ``` mapper.xml ```xml ``` * 纯注解开发 ```java @Mapper @Repository public interface UserMapper { @Select("select * from t_user") public List findAll(); } ``` **** #### 自动配置 MybatisAutoConfiguration: ```java @EnableConfigurationProperties(MybatisProperties.class) //MyBatis配置项绑定类。 @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); return factory.getObject(); } @org.springframework.context.annotation.Configuration @Import(AutoConfiguredMapperScannerRegistrar.class) @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class }) public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {} } @ConfigurationProperties(prefix = "mybatis") public class MybatisProperties {} ``` * 配置文件:`mybatis` * 自动配置了 SqlSessionFactory * 导入 `AutoConfiguredMapperScannerRegistra` 实现 @Mapper 的扫描 **** #### MyBatis-Plus ```xml com.baomidou mybatis-plus-boot-starter 3.4.1 ``` 自动配置类:MybatisPlusAutoConfiguration 只需要 Mapper 继承 **BaseMapper** 就可以拥有 CRUD 功能 *** ### Redis #### 基本使用 ```xml org.springframework.boot spring-boot-starter-data-redis ``` * 配置redis相关属性 ```yaml spring: redis: host: 127.0.0.1 # redis的主机ip port: 6379 ``` * 注入 RedisTemplate 模板 ```java @RunWith(SpringRunner.class) @SpringBootTest public class SpringbootRedisApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test public void testSet() { //存入数据 redisTemplate.boundValueOps("name").set("zhangsan"); } @Test public void testGet() { //获取数据 Object name = redisTemplate.boundValueOps("name").get(); System.out.println(name); } } ``` **** #### 自动配置 RedisAutoConfiguration 自动配置类 ```java @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean @ConditionalOnSingleCandidate(RedisConnectionFactory.class) public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } } ``` - 配置项:`spring.redis` - 自动导入了连接工厂配置类:LettuceConnectionConfiguration、JedisConnectionConfiguration - 自动注入了模板类:RedisTemplate 、StringRedisTemplate,k v 都是 String 类型 - 使用 @Autowired 注入模板类就可以操作 redis **** ## 单元测试 ### Junit5 Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,由三个不同的子模块组成: * JUnit Platform:在 JVM 上启动测试框架的基础,不仅支持 Junit 自制的测试引擎,其他测试引擎也可以接入 * JUnit Jupiter:提供了 JUnit5 的新的编程模型,是 JUnit5 新特性的核心,内部包含了一个测试引擎,用于在 Junit Platform 上运行 * JUnit Vintage:JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的测试引擎 注意:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖,如果需要兼容 Junit4 需要自行引入 ```java @SpringBootTest class Boot05WebAdminApplicationTests { @Test void contextLoads() { } } ``` *** ### 常用注解 JUnit5 的注解如下: - @Test:表示方法是测试方法,但是与 JUnit4 的 @Test 不同,它的职责非常单一不能声明任何属性,拓展的测试将会由 Jupiter 提供额外测试,包是 `org.junit.jupiter.api.Test` - @ParameterizedTest:表示方法是参数化测试 - @RepeatedTest:表示方法可重复执行 - @DisplayName:为测试类或者测试方法设置展示名称 - @BeforeEach:表示在每个单元测试之前执行 - @AfterEach:表示在每个单元测试之后执行 - @BeforeAll:表示在所有单元测试之前执行 - @AfterAll:表示在所有单元测试之后执行 - @Tag:表示单元测试类别,类似于 JUnit4 中的 @Categories - @Disabled:表示测试类或测试方法不执行,类似于 JUnit4 中的 @Ignore - @Timeout:表示测试方法运行如果超过了指定时间将会返回错误 - @ExtendWith:为测试类或测试方法提供扩展类引用 **** ### 断言机制 #### 简单断言 断言(assertions)是测试方法中的核心,用来对测试需要满足的条件进行验证,断言方法都是 org.junit.jupiter.api.Assertions 的静态方法 用来对单个值进行简单的验证: | 方法 | 说明 | | --------------- | ------------------------------------ | | assertEquals | 判断两个对象或两个原始类型是否相等 | | assertNotEquals | 判断两个对象或两个原始类型是否不相等 | | assertSame | 判断两个对象引用是否指向同一个对象 | | assertNotSame | 判断两个对象引用是否指向不同的对象 | | assertTrue | 判断给定的布尔值是否为 true | | assertFalse | 判断给定的布尔值是否为 false | | assertNull | 判断给定的对象引用是否为 null | | assertNotNull | 判断给定的对象引用是否不为 null | ```java @Test @DisplayName("simple assertion") public void simple() { assertEquals(3, 1 + 2, "simple math"); assertNull(null); assertNotNull(new Object()); } ``` **** #### 数组断言 通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等 ```java @Test @DisplayName("array assertion") public void array() { assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); } ``` *** #### 组合断言 assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为验证的断言,可以通过 lambda 表达式提供这些断言 ```java @Test @DisplayName("assert all") public void all() { assertAll("Math", () -> assertEquals(2, 1 + 1), () -> assertTrue(1 > 0) ); } ``` *** #### 异常断言 Assertions.assertThrows(),配合函数式编程就可以进行使用 ```java @Test @DisplayName("异常测试") public void exceptionTest() { ArithmeticException exception = Assertions.assertThrows( //扔出断言异常 ArithmeticException.class, () -> System.out.println(1 / 0) ); } ``` **** #### 超时断言 Assertions.assertTimeout() 为测试方法设置了超时时间 ```java @Test @DisplayName("超时测试") public void timeoutTest() { //如果测试方法时间超过1s将会异常 Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); } ``` **** #### 快速失败 通过 fail 方法直接使得测试失败 ```java @Test @DisplayName("fail") public void shouldFail() { fail("This should fail"); } ``` *** ### 前置条件 JUnit 5 中的前置条件(assumptions)类似于断言,不同之处在于**不满足的断言会使得测试方法失败**,而不满足的**前置条件只会使得测试方法的执行终止**,前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要 ```java @DisplayName("测试前置条件") @Test void testassumptions(){ Assumptions.assumeTrue(false,"结果不是true"); System.out.println("111111"); } ``` *** ### 嵌套测试 JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起,在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制 ```java @DisplayName("A stack") class TestingAStackDemo { Stack stack; @Test @DisplayName("is instantiated with new Stack()") void isInstantiatedWithNew() { assertNull(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); } } } ``` **** ### 参数测试 参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能 利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。 * @ValueSource:为参数化测试指定入参来源,支持八大基础类以及 String 类型、Class 类型 * @NullSource:表示为参数化测试提供一个 null 的入参 * @EnumSource:表示为参数化测试提供一个枚举入参 * @CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参 * @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流) *** ## 指标监控 ### Actuator 每一个微服务在云上部署以后,都需要对其进行监控、追踪、审计、控制等,SpringBoot 抽取了 Actuator 场景,使得每个微服务快速引用即可获得生产级别的应用监控、审计等功能 ```xml org.springframework.boot spring-boot-starter-actuator ``` 暴露所有监控信息为 HTTP: ```yaml management: endpoints: enabled-by-default: true #暴露所有端点信息 web: exposure: include: '*' #以web方式暴露 ``` 访问 http://localhost:8080/actuator/[beans/health/metrics/] 可视化界面:https://github.com/codecentric/spring-boot-admin **** ### Endpoint 默认所有的 Endpoint 除过 shutdown 都是开启的 ```yaml management: endpoints: enabled-by-default: false #禁用所有的 endpoint: #手动开启一部分 beans: enabled: true health: enabled: true ``` 端点: | ID | 描述 | | ------------------ | ------------------------------------------------------------ | | `auditevents` | 暴露当前应用程序的审核事件信息。需要一个 `AuditEventRepository` 组件 | | `beans` | 显示应用程序中所有 Spring Bean 的完整列表 | | `caches` | 暴露可用的缓存 | | `conditions` | 显示自动配置的所有条件信息,包括匹配或不匹配的原因 | | `configprops` | 显示所有 `@ConfigurationProperties` | | `env` | 暴露 Spring 的属性 `ConfigurableEnvironment` | | `flyway` | 显示已应用的所有 Flyway 数据库迁移。 需要一个或多个 Flyway 组件。 | | `health` | 显示应用程序运行状况信息 | | `httptrace` | 显示 HTTP 跟踪信息,默认情况下 100 个 HTTP 请求-响应需要一个 `HttpTraceRepository` 组件 | | `info` | 显示应用程序信息 | | `integrationgraph` | 显示 Spring integrationgraph,需要依赖 `spring-integration-core` | | `loggers` | 显示和修改应用程序中日志的配置 | | `liquibase` | 显示已应用的所有 Liquibase 数据库迁移,需要一个或多个 Liquibase 组件 | | `metrics` | 显示当前应用程序的指标信息。 | | `mappings` | 显示所有 `@RequestMapping` 路径列表 | | `scheduledtasks` | 显示应用程序中的计划任务 | | `sessions` | 允许从 Spring Session 支持的会话存储中检索和删除用户会话,需要使用 Spring Session 的基于 Servlet 的 Web 应用程序 | | `shutdown` | 使应用程序正常关闭,默认禁用 | | `startup` | 显示由 `ApplicationStartup` 收集的启动步骤数据。需要使用 `SpringApplication` 进行配置 `BufferingApplicationStartup` | | `threaddump` | 执行线程转储 | 应用程序是 Web 应用程序(Spring MVC,Spring WebFlux 或 Jersey),则可以使用以下附加端点: | ID | 描述 | | ------------ | ------------------------------------------------------------ | | `heapdump` | 返回 `hprof` 堆转储文件。 | | `jolokia` | 通过 HTTP 暴露 JMX bean(需要引入 Jolokia,不适用于 WebFlux),需要引入依赖 `jolokia-core` | | `logfile` | 返回日志文件的内容(如果已设置 `logging.file.name` 或 `logging.file.path` 属性),支持使用 HTTP Range标头来检索部分日志文件的内容。 | | `prometheus` | 以 Prometheus 服务器可以抓取的格式公开指标,需要依赖 `micrometer-registry-prometheus` | 常用 Endpoint: - Health:监控状况 - Metrics:运行时指标 - Loggers:日志记录 *** ## 项目部署 SpringBoot 项目开发完毕后,支持两种方式部署到服务器: * jar 包 (官方推荐,默认) * war 包 **更改 pom 文件中的打包方式为 war** * 修改启动类 ```java @SpringBootApplication public class SpringbootDeployApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(SpringbootDeployApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder b) { return b.sources(SpringbootDeployApplication.class); } } ``` * 指定打包的名称 ```xml war springboot org.springframework.boot spring-boot-maven-plugin ``` *** # Cloud ## 基本介绍 SpringCloud 是分布式微服务的一站式解决方案,是多种微服务落地技术的集合体,俗称微服务全家桶 ![Cloud-组件概览](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-组件概览.png) 参考文档:https://www.yuque.com/mrlinxi/pxvr4g/wcwd39 *** ## 服务注册 ### Eureka #### 基本介绍 Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理。Eureka 采用了 CS(Client-Server) 的设计架构,Eureka Server 是服务注册中心,系统中的其他微服务使用 Eureka 的客户端连接到 Eureka Server 并维持心跳连接 ![Cloud-Eureka和Dubbo对比](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka和Dubbo对比.png) * Eureka Server 提供服务注册服务:各个微服务节点通过配置启动后,会在 EurekaServer 中进行注册,EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,并且具有可视化界面 * Eureka Client 通过注册中心进行访问:用于简化 Eureka Server的交互,客户端也具备一个内置的、使用轮询 (round-robin) 负载算法的负载均衡器。在应用启动后将会向 Eureka Server 发送心跳(默认周期为30秒),如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,将会从服务注册表中把这个服务节点移除(默认 90 秒) **** #### 服务端 服务器端主启动类增加 @EnableEurekaServer 注解,指定该模块作为 Eureka 注册中心的服务器 构建流程如下: * 主启动类 ```java @SpringBootApplication @EnableEurekaServer // 表示当前是Eureka的服务注册中心 public class EurekaMain7001 { public static void main(String[] args) { SpringApplication.run(EurekaMain7001.class, args); } } ``` * 修改 pom 文件 ```xml 1.x: server跟client合在一起 org.springframework.cloud spring-cloud-starter-eureka 2.x: server跟client分开 org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` * 修改 application.yml 文件 ```yaml server: port: 7001 eureka: instance: hostname: localhost # eureka服务端的实例名称 client: # false表示不向注册中心注册自己。 register-with-eureka: false # false表示自己端就是注册中心,职责就是维护服务实例,并不需要去检索服务 fetch-registry: false service-url: # 设置与 Eureka Server 交互的地址查询服务和注册服务都需要依赖这个地址。 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ ``` * 游览器访问 http://localhost:7001 *** #### 客户端 ##### 生产者 服务器端主启动类需要增加 @EnableEurekaClient 注解,表示这是一个 Eureka 客户端,要注册进 EurekaServer 中 * 主启动类:PaymentMain8001 ```java @SpringBootApplication @EnableEurekaClient public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class, args); } } ``` * 修改 pom 文件:添加一个 Eureka-Client 依赖 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` * 写 yml 文件 ```yaml server: port: 8001 eureka: client: # 表示将自己注册进EurekaServer默认为true register-with-eureka: true # 表示可以从Eureka抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka instance: instance-id: payment8001 # 只暴露服务名,不带有主机名 prefer-ip-address: true # 访问信息有 IP 信息提示(鼠标停留在服务名称上时) ``` * 游览器访问 http://localhost:7001 *** ##### 消费者 * 主启动类:PaymentMain8001 ```java @SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class, args); } } ``` * pom 文件同生产者 * 写 yml 文件 ```yaml server: port: 80 # 微服务名称 spring: application: name: cloud-order-service eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka ``` * 浏览器访问 http://localhost:7001 ![Cloud-Eureka可视化界面](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka可视化界面.png) *** #### 集群构建 ##### 服务端 Server 端高可用集群原理:实现负载均衡和故障容错,互相注册,相互守望 ![Cloud-Eureka集群原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka集群原理.png) 多台 Eureka 服务器,每一台 Eureka 服务器需要有自己的主机名,同时各服务器需要相互注册 * Eureka1: ```yaml server: port: 7001 eureka: instance: hostname: eureka7001.com client: register-with-eureka: false fetch-registry: false service-url: # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 # 单机就是自己 # defaultZone: http://eureka7001.com:7001/eureka/ # 集群指向其他eureka #defaultZone: http://eureka7002.com:7002/eureka/ # 写成这样可以直接通过可视化页面跳转到7002 defaultZone: http://eureka7002.com:7002/ ``` * Eureka2: ```yaml server: port: 7002 eureka: instance: hostname: eureka7002.com client: register-with-eureka: false fetch-registry: false service-url: #写成这样可以直接通过可视化页面跳转到7001 defaultZone: http://eureka7001.com:7001/ ``` * 主启动类: ```java @SpringBootApplication @EnableEurekaServer public class EurekaMain7002 { public static void main(String[] args) { SpringApplication.run(EurekaMain7002.class, args); } } ``` * 访问 http://eureka7001.com:7001 和 http://eureka7002.com:7002: ![Cloud-EurekaServer集群构建成功](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-EurekaServer集群构建成功.png) * RPC 调用:controller.OrderController ```java @RestController @Slf4j public class OrderController { public static final String PAYMENT_URL = "http://localhost:8001"; @Autowired private RestTemplate restTemplate; // CommonResult 是一个公共的返回类型 @GetMapping("/consumer/payment/get/{id}") public CommonResult getPayment(@PathVariable("id") long id) { // 返回对象为响应体中数据转化成的对象,基本上可以理解为JSON return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); } } ``` *** ##### 生产者 构建 PaymentMain8001 的服务集群 * 主启动类 ```java @SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient public class PaymentMain8002 { public static void main(String[] args) { SpringApplication.run(PaymentMain8002.class, args); } } ``` * 写 yml 文件:端口修改,并且 spring.application.name 均为 cloud-payment-service ```yaml server: port: 8002 spring: application: name: cloud-payment-service eureka: client: # 表示将自己注册进EurekaServer默认为true register-with-eureka: true # 表示可以从Eureka抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka ``` *** ##### 负载均衡 消费者端的 Controller ```java // public static final String PAYMENT_URL = "http://localhost:8001"; public static final String PAYMENT_URL = "http://localhost:8002"; ``` 由于已经建立了生产者集群,所以可以进行负载均衡的操作: * Controller:只修改 PAYMENT_URL 会报错,因为 CLOUD-PAYMENT-SERVICE 对应多个微服务,需要规则来判断调用哪个端口 ```java public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE"; ``` * 使用 @LoadBlanced 注解赋予 RestTemplate 负载均衡的能力,增加 config.ApplicationContextConfig 文件: ```java @Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` **** #### 服务发现 服务发现:对于注册进 Eureka 里面的微服务,可以通过服务发现来获得该服务的信息 * 主启动类增加注解 @EnableDiscoveryClient: ```java @SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class, args); } } ``` * 修改生产者的 Controller ```java @RestController @Slf4j public class PaymentController { @Autowired private DiscoveryClient discoveryClient; @GetMapping(value = "/payment/discovery") public Object discovery() { List services = discoveryClient.getServices(); for (String service : services) { log.info("**** element:" + service); } List instances = discoveryClient.getInstances("PAYMENT-SERVICE"); for (ServiceInstance instance : instances) { log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort()); } return this.discoveryClient; } } ``` *** #### 自我保护 保护模式用于客户端和 EurekaServer 之间存在网络分区场景下的保护,一旦进入保护模式 EurekaServer 将会尝试保护其服务注册表中的信息,不在删除服务注册表中的数据,属于 CAP 里面的 AP 思想(可用性和分区容错性) ![Cloud-Eureka自我保护机制](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Eureka自我保护机制.png) 如果一定时间内丢失大量该微服务的实例,这时 Eureka 就会开启自我保护机制,不会剔除该服务。 因为这个现象可能是因为网络暂时不通,出现了 Eureka 的假死、拥堵、卡顿,客户端恢复后还能正常发送心跳 禁止自我保护: * Server: ```yaml eureka: server: # 关闭自我保护机制,不可用的服务直接删除 enable-self-preservation: false eviction-interval-timer-in-ms: 2000 ``` * Client: ```yaml eureka: instance: # Eureka客户端向服务端发送心跳的时间间隔默认30秒 lease-renewal-interval-in-seconds: 1 # Eureka服务端在收到最后一次心跳后,90s没有收到心跳,剔除服务 lease-expiration-duration-in-seconds: 2 ``` **** ### Consul #### 基本介绍 Consul 是开源的分布式服务发现和配置管理系统,采用 Go 语言开发,官网:https://developer.hashicorp.com/consul * 提供了微服务系统中心的服务治理,配置中心,控制总线等功能 * 基于 Raft 协议,支持健康检查,同时支持 HTTP 和 DNS 协议支持跨数据中心的 WAN 集群 * 提供图形界面 下载 Consul 后,运行指令:`consul -version` ```bash D:\Program Files\Java>consul -version Consul v1.15.1 Revision 7c04b6a0 Build Date 2023-03-07T20:35:33Z Protocol 2 spoken by default, understands 2 to 3 (.....) ``` 启动命令: ```bash consul agent -dev ``` 访问浏览器:http://localhost:8500/ 中文文档:https://www.springcloud.cc/spring-cloud-consul.html *** #### 基本使用 无需 Server 端代码的编写 生产者: * 引入 pom 依赖: ```xml org.springframework.cloud spring-cloud-starter-consul-discovery ``` * application.yml: ```yaml ###consul 服务端口号 server: port: 8006 spring: application: name: consul-provider-payment ####consul注册中心地址 cloud: consul: host: localhost port: 8500 discovery: service-name: ${spring.application.name} ``` * 主启动类: ```java @SpringBootApplication @EnableDiscoveryClient public class PaymentMain8006 { public static void main(String[] args) { SpringApplication.run(PaymentMain8006.class, args); } } ``` 消费者: * application.yml: ```yaml ###consul服务端口号 server: port: 80 spring: application: name: cloud-consumer-order ####consul注册中心地址 cloud: consul: host: localhost port: 8500 discovery: #hostname: 127.0.0.1 service-name: ${spring.application.name} ``` * 主启动类:同生产者 * 配置类: ```java @Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` * 业务类 Controller: ```java @RestController @Slf4j public class OrderConsulController { public static final String INVOKE_URL = "http://cloud-provider-pament"; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/payment/consul") public String paymentInfo() { return restTemplate.getForObject(INVOKE_URL, String.class); } } ``` **** ## 服务调用 ### Ribbon #### 基本介绍 SpringCloud Ribbon 是基于 Netflix Ribbon 实现的一套负载均衡工具,提供客户端的软件负载均衡算法和服务调用,Ribbon 客户端组件提供一系列完善的配置项如连接超时,重试等 官网: https://github.com/Netflix/ribbon/wiki/Getting-Started (已进入维护模式,未来替换为 Load Banlancer) 负载均衡 Load Balance (LB) 就是将用户的请求平摊的分配到多个服务上,从而达到系统的 HA(高可用) **常见的负载均衡算法:** - 轮询:为请求选择健康池中的第一个后端服务器,然后按顺序往后依次选择 - 最小连接:优先选择连接数最少,即压力最小的后端服务器,在会话较长的情况下可以采取这种方式 - 散列:根据请求源的 IP 的散列(hash)来选择要转发的服务器,可以一定程度上保证特定用户能连接到相同的服务器,如果应用需要处理状态而要求用户能连接到和之前相同的服务器,可以采取这种方式 Ribbon 本地负载均衡客户端与 Nginx 服务端负载均衡区别: - Nginx 是服务器负载均衡,客户端所有请求都会交给 Nginx,然后由 Nginx 实现转发请求,即负载均衡是由服务端实现的 - Ribbon 本地负载均衡,在调用微服务接口时会在注册中心上获取注册信息服务列表,然后缓存到 JVM 本地,从而在本地实现 RPC 远程服务调用技术 集中式 LB 和进程内 LB 的对比: * 集中式 LB:在服务的消费方和提供方之间使用独立的 LB 设施(如 Nginx),由该设施把访问请求通过某种策略转发至服务的提供方 * 进程内 LB:将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些服务可用,然后从中选择出一个服务器,Ribbon 属于该类 *** #### 工作流程 Ribbon 是一个软负载均衡的客户端组件 ![Cloud-Ribbon架构原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Ribbon架构原理.png) - 第一步先选择 EurekaServer,优先选择在同一个区域内负载较少的 Server - 第二步根据用户指定的策略,再从 Server 取到的服务注册列表中选择一个地址 *** #### 核心组件 Ribbon 核心组件 IRule 接口,主要实现类: - RoundRobinRule:轮询 - RandomRule:随机 - RetryRule:先按照 RoundRobinRule 的策略获取服务,如果获取服务失败则在指定时间内会进行重试 - WeightedResponseTimeRule:对 RoundRobinRule 的扩展,响应速度越快的实例选择权重越大,越容易被选择 - BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 - AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例 - ZoneAvoidanceRule:默认规则,复合判断 Server 所在区域的性能和 Server 的可用性选择服务器 ![Cloud-IRule类图](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-IRule类图.png) 注意:官方文档明确给出了警告,自定义负载均衡配置类不能放在 @ComponentScan 所扫描的当前包下以及子包下 更换负载均衡算法方式: * 自定义负载均衡配置类 MySelfRule: ```java @Configuration public class MySelfRule { @Bean public IRule myRule() { return new RandomRule();//定义为随机负载均衡算法 } } ``` * 主启动类添加 @RibbonCilent 注解 ```java @SpringBootApplication @EnableEurekaClient // 指明访问的服务CLOUD-PAYMENT-SERVICE,以及指定负载均衡策略 @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration= MySelfRule.class) public class OrderMain80 { public static void main(String[] args) { SpringApplication.run(OrderMain80.class, args); } } ``` **** ### OpenFeign #### 基本介绍 Feign 是一个声明式 WebService 客户端,能让编写 Web 客户端更加简单,只要创建一个接口并添加注解 @Feign 即可,可以与 Eureka 和 Ribbon 组合使用支持负载均衡,所以一般**用在消费者端** OpenFeign 在 Feign 的基础上支持了 SpringMVC 注解,并且 @FeignClient 注解可以解析 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,在实现类中做负载均衡和服务调用 优点:利用 RestTemplate 对 HTTP 请求的封装处理,形成了一套模版化的调用方法。但是对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以一个微服务接口上面标注一个 @Feign 注解,就可以完成包装依赖服务的调用 **** #### 基本使用 @FeignClient("provider name") 注解使用规则: * 声明的方法签名必须和 provider 微服务中的 controller 中的方法签名一致 * 如果需要传递参数,那么 `@RequestParam` 、`@RequestBody` 、`@PathVariable` 也需要加上 改造消费者服务 * 引入 pom 依赖:OpenFeign 整合了 Ribbon,具有负载均衡的功能 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` * application.yml:不将其注册到 Eureka 作为微服务 ```yaml server: port: 80 eureka: client: # 表示不将其注入Eureka作为微服务,不作为Eureak客户端了,而是作为Feign客户端 register-with-eureka: false service-url: # 集群版 defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka ``` * 主启动类:开启 Feign ```java @SpringBootApplication @EnableFeignClients //不作为Eureak客户端了,而是作为Feign客户端 public class OrderOpenFeignMain80 { public static void main(String[] args) { SpringApplication.run(OrderOpenFeignMain80.class, args); } } ``` * 新建 Service 接口:PaymentFeignService 接口和 @FeignClient 注解,完成 Feign 的包装调用 ```java @Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 作为一个Feign功能绑定的的接口 public interface PaymentFeignService { @GetMapping(value = "/payment/get/{id}") public CommonResult getPaymentById(@PathVariable("id") long id); @GetMapping("/payment/feign/timeout") public String paymentFeignTimeout(); } ``` * Controller: ```java @RestController @Slf4j public class OrderFeignController { @Autowired private PaymentFeignService paymentFeignService; @GetMapping("/consumer/payment/get/{id}") public CommonResult getPayment(@PathVariable("id") long id) { // 返回对象为响应体中数据转化成的对象,基本上可以理解为JSON return paymentFeignService.getPaymentById(id); } @GetMapping("/consumer/payment/feign/timeout") public String paymentFeignTimeout() { // openfeign-ribbon,客户端一般默认等待1s return paymentFeignService.paymentFeignTimeout(); } } ``` *** #### 超时问题 Feign 默认是支持 Ribbon,Feign 客户端的负载均衡和超时控制都由 Ribbon 控制 设置 Feign 客户端的超时等待时间: ```yaml ribbon: #指的是建立连接后从服务器读取到可用资源所用的时间 ReadTimeout: 5000 #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 ConnectTimeout: 5000 ``` 演示超时现象:OpenFeign 默认等待时间为 1 秒钟,超过后会报错 * 服务提供方 Controller: ```java @GetMapping("/payment/feign/timeout") public String paymentFeignTimeout() { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } return serverPort; } ``` * 消费者 PaymentFeignService 和 OrderFeignController 参考上一小节代码 * 测试报错: ![Cloud-OpenFeign超时错误](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-OpenFeign超时错误.png)!](C:\Users\Seazean\Desktop\123\Cloud-OpenFeign超时错误.png) *** #### 日志级别 Feign 提供了日志打印功能,可以通过配置来调整日志级别,从而了解 Feign 中 HTTP 请求的细节 | NONE | 默认的,不显示任何日志 | | ------- | --------------------------------------------------------- | | BASIC | 仅记录请求方法、URL、响应状态码及执行时间 | | HEADERS | 除了 BASIC 中定义的信息之外,还有请求和响应的头信息 | | FULL | 除了 HEADERS 中定义的信息外,还有请求和响应的正文及元数据 | 配置在消费者端 * 新建 config.FeignConfig 文件:配置日志 Bean ```java @Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } } ``` * application.yml: ```yaml logging: level: # feign 日志以什么级别监控哪个接口 com.atguigu.springcloud.service.PaymentFeignService: debug ``` * Debug 后查看后台日志 **** ## 服务熔断 ### Hystrix #### 基本介绍 Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖会出现调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性 断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间地占用,避免了故障在分布式系统中的蔓延,乃至雪崩 * 服务降级 Fallback:系统不可用时需要一个兜底的解决方案或备选响应,向调用方返回一个可处理的响应 * 服务熔断 Break:达到最大服务访问后,直接拒绝访问 * 服务限流 Flowlimit:高并发操作时严禁所有请求一次性过来拥挤,一秒钟 N 个,有序排队进行 官方文档:https://github.com/Netflix/Hystrix/wiki/How-To-Use **** #### 服务降级 ##### 案例构建 生产者模块: * 引入 pom 依赖: ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` * 主启动类:开启 Feign ```java @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker // 降级使用 public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } } ``` * Controller: ```java @RestController @Slf4j public class PaymentController { @Resource private PaymentService paymentService; @Value("${server.port}") private String serverPort; // 正常访问 @GetMapping("/payment/hystrix/ok/{id}") private String paymentInfo_Ok(@PathVariable("id") Integer id) { return paymentService.paymentInfo_Ok(id); } // 超时 @GetMapping("/payment/hystrix/timeout/{id}") private String paymentInfo_Timeout(@PathVariable("id") Integer id) { // service 层有 Thread.sleep() 操作,保证超时 return paymentService.paymentInfo_Timeout(id); } } ``` * Service: ```java @Service public class PaymentService { public String paymentInfo_Ok(Integer id) { return "线程池: " + Thread.currentThread().getName() + "paymentInfo_OK, id: " + id"; } public String paymentInfo_Timeout(Integer id) { int timeNumber = 3; try { TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池: " + Thread.currentThread().getName() + " payment_Timeout, id: " + id; } } ``` * jmeter 压测两个接口,发现接口 paymentInfo_Ok 也变的卡顿 消费者模块: * Service 接口: ```java @Component @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_Ok(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_Timeout(@PathVariable("id") Integer id); } ``` * Controller: ```java @RestController @Slf4j public class OrderHystirxController { @Resource PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_Ok(@PathVariable("id") Integer id) { return paymentHystrixService.paymentInfo_Ok(id); } @GetMapping("/consumer/payment/hystrix/timeout/{id}") public String paymentInfo_Timeout(@PathVariable("id") Integer id) { return paymentHystrixService.paymentInfo_Timeout(id); } } ``` * 测试:使用的是 Feign 作为客户端,默认 1s 没有得到响应就会报超时错误,进行并发压测 * 解决: * 超时导致服务器变慢(转圈):超时不再等待 * 出错(宕机或程序运行出错):出错要有兜底 **** ##### 降级操作 生产者端和消费者端都可以进行服务降级,使用 @HystrixCommand 注解指定降级后的方法 生产者端:主启动类添加新注解 @EnableCircuitBreaker,业务类(Service)方法进行如下修改, ```java // 模拟拥堵的情况 @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler", commandProperties = { //规定这个线程的超时时间是3s,3s后就由fallbackMethod指定的方法“兜底”(服务降级) @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value = "3000") }) public String paymentInfo_Timeout(Integer id) { // 超时或者出错 } public String paymentInfo_TimeoutHandler(Integer id) { return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeoutHandler, id: " + id"; } ``` 服务降级的方法和业务处理的方法混杂在了一块,耦合度很高,并且每个方法配置一个服务降级方法 - 在业务类Controller上加 @DefaultProperties(defaultFallback = "method_name") 注解 - 在需要服务降级的方法上标注 @HystrixCommand 注解,如果 @HystrixCommand 里没有指明 fallbackMethod,就默认使用 @DefaultProperties 中指明的降级服务 ```java @RestController @Slf4j @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") public class OrderHystrixController { @Resource PaymentHystrixService paymentHystrixService; @GetMapping("/consumer/payment/hystrix/ok/{id}") public String paymentInfo_Ok(@PathVariable("id") Integer id) { return paymentHystrixService.paymentInfo_OK(id); } @HystrixCommand public String paymentInfo_Timeout(@PathVariable("id") Integer id) { return paymentHystrixService.paymentInfo_Timeout(id); } public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) { return "fallback"; } // 下面是全局fallback方法 public String payment_Global_FallbackMethod() { return "Global fallback"; } } ``` 客户端调用服务端,遇到服务端宕机或关闭等极端情况,为 Feign 客户端定义的接口添加一个服务降级实现类即可实现解耦 * application.yml:配置文件中开启了 Hystrix ```yaml # 用于服务降级 在注解 @FeignClient中添加fallbackFactory属性值 feign: hystrix: enabled: true #在Feign中开启Hystrix ``` * Service:统一为接口里面的方法进行异常处理,服务异常找 PaymentFallbackService,来统一进行服务降级的处理 ```java @Component @FeignClient(value = "PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class) public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_Timeout(@PathVariable("id") Integer id); } ``` * PaymentFallbackService: ```java @Component public class PaymentFallbackService implements PaymentHystrixService { @Override public String paymentInfo_OK(Integer id) { return "------PaymentFallbackService-paymentInfo_Ok, fallback"; } @Override public String paymentInfo_Timeout(Integer id) { return "------PaymentFallbackService-paymentInfo_Timeout, fallback"; } } ``` *** #### 服务熔断 ##### 熔断类型 熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息 Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省时 5 秒内 20 次调用失败,就会启动熔断机制;当检测到该节点微服务调用响应正常后(检测方式是尝试性放开请求),自动恢复调用链路 - 熔断打开:请求不再进行调用当前服务,再有请求调用时将不会调用主逻辑,而是直接调用降级 fallback。实现了自动的发现错误并将降级逻辑切换为主逻辑,减少响应延迟效果。内部设置时钟一般为 MTTR(Mean time to repair,平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态 - 熔断关闭:熔断关闭不会对服务进行熔断,服务正常调用 - 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断,反之继续熔断 ![Cloud-Hystrix熔断机制](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix熔断机制.png) **** ##### 熔断操作 涉及到断路器的四个重要参数:**快照时间窗、请求总数阀值、窗口睡眠时间、错误百分比阀值** - circuitBreaker.enabled:是否开启断路器 - metrics.rollingStats.timeInMilliseconds:快照时间窗口,断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 秒 - circuitBreaker.requestVolumeThreshold:请求总数阀值,该属性设置在快照时间窗内(默认 10s)使断路器跳闸的最小请求数量(默认是 20),如果 10s 内请求数小于设定值,就算请求全部失败也不会触发断路器 - circuitBreaker.sleepWindowInMilliseconds:窗口睡眠时间,短路多久以后开始尝试是否恢复进入半开状态,默认 5s - circuitBreaker.errorThresholdPercentage:错误百分比阀值,失败率达到多少后将断路器打开 ```java //总的意思就是在n(10)毫秒内的时间窗口期内,m次请求中有p% (60%)的请求失败了,那么断路器启动 @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = { @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") }) public String paymentCircuitBreaker(@PathVariable("id") Integer id) { if (id < 0) { throw new RuntimeException("******id 不能负数"); } String serialNumber = IdUtil.simpleUUID(); // 等价于UUID.randomUUID().toString() return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber; } ``` * 开启:满足一定的阈值(默认 10 秒内超过 20 个请求次数)、失败率达到阈值(默认 10 秒内超过 50% 的请求失败) * 关闭:一段时间之后(默认是 5 秒),断路器是半开状态,会让其中一个请求进行转发,如果成功断路器会关闭,反之继续开启 *** #### 工作流程 具体工作流程: 1. 创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象 2. 命令执行,其中 HystrixComand 实现了下面前两种执行方式,而 HystrixObservableCommand 实现了后两种执行方式 * execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常 * queue():异步执行, 直接返回 一个 Future 对象, 其中包含了服务执行结束时要返回的单一结果对象 * observe():返回 Observable 对象,代表了操作的多个结果,它是一个 Hot Obserable(不论事件源是否有订阅者,都会在创建后对事件进行发布,所以对于 Hot Observable 的每个订阅者都有可能是从事件源的中途开始的,并可能只是看到了整个操作的局部过程) * toObservable():同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个 Cold Observable(没有订阅者的时候并不会发布事件,而是进行等待,直到有订阅者之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程) 3. 若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以 Observable 对象的形式返回 4. 检查断路器是否为打开状态,如果断路器是打开的,那么 Hystrix 不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步) 5. 线程池/请求队列/信号量是否占满,如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池时)已经被占满, 那么 Hystrix 也不会执行命令, 而是转接到 fallback 处理逻辑(第 8 步) 6. Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务 * HystrixCommand.run():返回一个单一的结果,或者抛出异常 * HystrixObservableCommand.construct():返回一个Observable 对象来发射多个结果,或通过 onError 发送错误通知 7. Hystrix会将"成功"、"失败"、"拒绝"、"超时"等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行"熔断/短路" 8. 当命令执行失败的时候,Hystrix 会进入 fallback 尝试回退处理,通常也称该操作为"服务降级",而能够引起服务降级情况: * 第 4 步:当前命令处于"熔断/短路"状态,断路器是打开的时候 * 第 5 步:当前命令的线程池、请求队列或 者信号量被占满的时候 * 第 6 步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候 9. 当 Hystrix 命令执行成功之后, 它会将处理结果直接返回或是以 Observable 的形式返回 注意:如果、没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据,而是通过 onError 方法通知命令立即中断请求,并通过 onError() 方法将引起命令失败的异常发送给调用者 ![Cloud-Hystrix工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix工作流程.png) 官方文档:https://github.com/Netflix/Hystrix/wiki/How-it-Works **** #### 服务监控 Hystrix 提供了准实时的调用监控(Hystrix Dashboard),Hystrix 会持续的记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等,Netflix 通过 `hystrix-metrics-event-stream` 项目实现了对以上指标的监控,Spring Cloud 提供了 Hystrix Dashboard 的整合,对监控内容转化成可视化页面 * 引入 pom 依赖: ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard ``` * application.yml:只需要端口即可 ```yaml server: port: 9001 ``` * 主启动类: ```java @SpringBootApplication @EnableHystrixDashboard // 开启Hystrix仪表盘 public class HystrixDashboardMain9001 { public static void main(String[] args) { SpringApplication.run(HystrixDashboardMain9001.class, args); } } ``` * 所有微服务(生产者)提供类 8001/8002/8003 都需要监控依赖配置 ```xml org.springframework.boot spring-boot-starter-actuator ``` * 启动测试:http://localhost:9001/hystrix Cloud-Hystrix可视化界面 * 新版本 Hystrix 需要在需要监控的微服务端的主启动类中指定监控路径,不然会报错 ```java @SpringBootApplication @EnableEurekaClient // 本服务启动后会自动注册进eureka服务中 @EnableCircuitBreaker // 对hystrixR熔断机制的支持 public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class, args); } /** ======================================需要添加的代码================== *此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑 *ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream", *只要在自己的项目里配置上下面的servlet就可以了 */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } } ``` * 指标说明: ![Cloud-Hystrix界面图示说明](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Hystrix界面图示说明.png) **** ## 服务网关 ### Zuul SpringCloud 中所集成的 Zuul 版本,采用的是 Tomcat 容器,基于 Servlet 之上的一个阻塞式处理模型,不支持任何长连接,用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得 Zuul 的性能相对较差 官网: https://github.com/Netflix/zuul/wiki **** ### Gateway #### 基本介绍 SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。 * 基于 WebFlux 框架实现,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty(异步非阻塞响应式的框架) * 基于 Filter 链的方式提供了网关基本的功能,例如:安全、监控/指标、限流等 Gateway 的三个核心组件: * Route:路由是构建网关的基本模块,由 ID、目标 URI、一系列的断言和过滤器组成,如果断言为 true 则匹配该路由 * Predicate:断言,可以匹配 HTTP 请求中的所有内容(例如请求头或请求参数),如果请求参数与断言相匹配则进行路由 * Filter:指 Spring 框架中的 GatewayFilter实例,使用过滤器可以在请求被路由前或之后(拦截)对请求进行修改 ![Cloud-Gateway工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Gateway工作流程.png) 核心逻辑:路由转发 + 执行过滤器链 - 客户端向 Spring Cloud Gateway 发出请求,然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler - Handler 通过指定的过滤器链来将请求发送到际的服务执行业务逻辑,然后返回 - 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑 - Filter 在 pre 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在 post 类型的过滤器中可以做响应内容、响应头的修改、日志的输出、流量监控等 *** #### 网关使用 ##### 配置方式 Gateway 网关路由有两种配置方式,分别为通过 yml 配置和注入 Bean * 引入 pom 依赖:Gateway 不需要 spring-boot-starter-web 依赖,否在会报错,原因是底层使用的是 WebFlux 与 Web 冲突 ```xml org.springframework.cloud spring-cloud-starter-gateway ``` * application.yml: ```yaml server: port: 9527 spring: application: name: cloud-gateway eureka: instance: hostname: cloud-gateway-service client: #服务提供者provider注册进eureka服务列表内 service-url: register-with-eureka: true fetch-registry: true defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka #集群版 ``` * 主启动类(网关不需要业务类): ```java @SpringBootApplication @EnableEurekaClient public class GateWayMain9527 { public static void main(String[] args) { SpringApplication.run(GateWayMain9527.class, args); } } ``` * 以前访问 provider-payment8001 中的 Controller 方法,通过 localhost:8001/payment/get/id 和 localhost:8001/payment/lb,项目不想暴露 8001 端口号,希望在 8001 外面套一层 9527 端口: ```yaml server: port: 9527 spring: application: name: cloud-gateway ## =====================新增==================== cloud: gateway: routes: - id: payment_routh # payment_route #路由的ID,没有固定规则但要求【唯一】,建议配合服务名 uri: http://localhost:8001 #匹配后提供服务的路由地址 predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 - id: payment_routh2 # payment_route#路由的ID,没有固定规则但要求【唯一】,建议配合服务名 uri: http://localhost:8001 #匹配后提供服务的路由地址 predicates: - Path=/payment/lb/** # 断言,路径相匹配的进行路由 ``` * uri + predicate 拼接就是具体的接口请求路径,通过 localhost:9527 映射的地址 * predicate 断言 http://localhost:8001下面有一个 /payment/get/** 的地址,如果找到了该地址就返回 true,可以用 9527 端口访问,进行端口的适配 * `**` 表示通配符,因为这是一个不确定的参数 **** ##### 注入Bean 通过 9527 网关访问到百度的网址 https://www.baidu.com/,在 config 包下创建一个配置类,路由规则是访问 /baidu 跳转到百度 ```java @Configuration public class GatewayConfig { // 配置了一个 id 为 path_route_cloud 的路由规则 @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){ // 构建一个路由器,这个routes相当于yml配置文件中的routes RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes(); // 路由器的id是:path_route_cloud,规则是访问/baidu ,将会转发到 https://www.baidu.com/ routes.route("path_route_cloud", r -> r.path("/baidu").uri(" https://www.baidu.com")).build(); return routes.build(); } } ``` *** ##### 动态路由 Gateway 会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由和负载均衡,避免出现一个路由规则仅对应一个接口方法,当请求地址很多时需要很大的配置文件 application.yml 开启动态路由功能 ```yaml spring: application: name: cloud-gateway cloud: gateway: discovery: locator: enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由 routes: - id: payment_routh1 # 路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: lb://cloud-payment-service # 匹配后提供服务的路由地址 predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 - id: payment_routh2 #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: lb://cloud-payment-service #匹配后提供服务的路由地址 predicates: - Path=/payment/lb/** # 断言,路径相匹配的进行路由 - After=2021-09-28T19:14:51.514+08:00[Asia/Shanghai] ``` lb:// 开头代表从注册中心中获取服务,后面是需要转发到的服务名称 ***** #### 断言类型 After Route Predicate:匹配该断言时间之后的 URI 请求 * 获取时间: ```java public class TimeTest { public static void main(String[] args) { ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区 System.out.println(zbj); //2023-01-10T16:31:44.106+08:00[Asia/Shanghai] } } ``` * 配置 yml:动态路由小结有配置 * 测试:正常访问成功,将时间修改到 2023-01-10T16:31:44.106+08:00[Asia/Shanghai] 之后访问失败 ![Cloud-Gateway时间断言](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Gateway时间断言.png) 常见断言类型: * Before Route Predicate:匹配该断言时间之前的 URI 请求 * Between Route Predicate:匹配该断言时间之间的 URI 请求 ```yaml - Between=2022-02-02T17:45:06.206+08:00[Asia/Shanghai],2022-03-25T18:59:06.206+08:00[Asia/Shanghai] ``` * Cookie Route Predicate:Cookie 断言,两个参数分别是 Cookie name 和正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由 ```yaml - Cookie=username, seazean # 只有发送的请求有 cookie,而且有username=seazean这个数据才能访问,反之404 ``` * Header Route Predicate:请求头断言 ```yaml - Header=X-Request-Id, \d+ # 请求头要有 X-Request-Id 属性,并且值为整数的正则表达式 ``` * Host Route Predicate:指定主机可以访问,可以指定多个用 `,` 分隔开 ```yaml - Host=**.seazean.com ``` * Method Route Predicate:请求类型断言 ```yaml - Method=GET # 只有 Get 请求才能访问 ``` * Path Route Predicate:路径匹配断言 * Query Route Predicate:请求参数断言 ```yaml - Query=username, \d+ # 要有参数名 username 并且值还要是整数才能路由 ``` **** #### Filter使用 Filter 链是同时满足一系列的过滤链,路由过滤器可用于修改进入的 HTTP 请求和返回的 HTTP 响应,路由过滤器只能指定路由进行使用,Spring Cloud Gateway 内置了多种路由过滤器,都由 GatewayFilter 的工厂类来产生 配置文件:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gatewayfilter-factories 自定义全局过滤器:实现两个主要接口 GlobalFilter, Ordered ```java @Component @Slf4j public class MyLogGateWayFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("*********************come in MyLogGateWayFilter: "+ new Date()); // 取出请求参数的uname对应的值 String uname = exchange.getRequest().getQueryParams().getFirst("uname"); // 如果 uname 为空,就直接过滤掉,不走路由 if(uname == null){ log.info("************* 用户名为 NULL 非法用户 o(╥﹏╥)o"); // 判断该请求不通过时:给一个回应,返回 exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); return exchange.getResponse().setComplete(); } // 反之,调用下一个过滤器,也就是放行:在该环节判断通过的 exchange 放行,交给下一个 filter 判断 return chain.filter(exchange); } // 设置这个过滤器在Filter链中的加载顺序,数字越小,优先级越高 @Override public int getOrder() { return 0; } } ``` *** ## 服务配置 ### config #### 基本介绍 SpringCloud Config 为微服务架构中的微服务提供集中化的外部配置支持(Git/GitHub),为各个不同微服务应用的所有环境提供了一个中心化的外部配置(Config Server) ![Cloud-Config工作原理](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Config工作原理.png) SpringCloud Config 分为服务端和客户端两部分 * 服务端也称为分布式配置中心,是一个独立的微服务应用,连接配置服务器并为客户端提供获取配置信息,加密/解密等信息访问接口 * 客户端通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动时从配置中心获取和加载配置信息,配置服务器默认采用 Git 来存储配置信息,这样既有助于对环境配置进行版本管理,也可以通过 Git 客户端来方便的管理和访问配置内容 优点: * 集中管理配置文件 * 不同环境不同配置,动态化的配置更新,分环境部署比如 dev/test/prod/beta/release * 运行期间动态调整配置,服务向配置中心统一拉取配置的信息,**服务不需要重启即可感知到配置的变化并应用新的配置** * 将配置信息以 Rest 接口的形式暴露 官网: https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/ **** #### 服务端 构建 Config Server 模块 * 引入 pom 依赖: ```xml org.springframework.cloud spring-cloud-config-server ``` * application.yml: ```yaml server: port: 3344 spring: application: name: cloud-config-center #注册进Eureka服务器的微服务名 cloud: config: server: git: # GitHub上面的git仓库名字 这里可以写https地址跟ssh地址,https地址需要配置username和 password uri: git@github.com:seazean/springcloud-config.git default-label: main search-paths: - springcloud-config # 搜索目录 # username: # password: label: main # 读取分支,以前是master #服务注册到eureka地址 eureka: client: service-url: defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka #集群版 ``` search-paths 表示远程仓库下有一个叫做 springcloud-config 的,label 则表示读取 main分支里面的内容 * 主启动类: ```java @SpringBootApplication @EnableEurekaClient @EnableConfigServer //开启SpringCloud的 public class ConfigCenterMain3344 { public static void main(String[] args) { SpringApplication.run(ConfigCenterMain3344.class, args); } } ``` 配置读取规则: ```yaml /{application}/{profile}[/{label}] /{application}-{profile}.yml /{label}/{application}-{profile}.yml /{application}-{profile}.properties /{label}/{application}-{profile}.properties ``` * label:分支 * name:服务名 * profile:环境(dev/test/prod) 比如:http://localhost:3344/master/config-dev.yaml *** #### 客户端 ##### 基本配置 配置客户端 Config Client,客户端从配置中心(Config Server)获取配置信息 * 引入 pom 依赖: ```xml org.springframework.cloud spring-cloud-starter-config ``` * bootstrap.yml:系统级配置,优先级更高,application.yml 是用户级的资源配置项 Spring Cloud 会创建一个 Bootstrap Context 作为 Spring 应用的 Application Context 的父上下文,初始化的时候 Bootstrap Context 负责从外部源加载配置属性并解析配置,这两个上下文共享一个从外部获取的 Environment,为了配置文件的加载顺序和分级管理,这里使用 bootstrap.yml ```yaml server: port: 3355 # 构建多个微服务,3366 3377 等 spring: application: name: config-client cloud: #Config客户端配置 config: label: main #分支名称 以前是master name: config #配置文件名称 profile: dev #读取后缀名称 # main分支上config-dev.yml的配置文件被读取 http://config-3344.com:3344/master/config-dev.yml uri: http://localhost:3344 # 配置中心地址k #服务注册到eureka地址 eureka: client: service-url: defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka ``` * 主启动类: ```java @SpringBootApplication @EnableEurekaClient public class ConfigClientMain3355 { public static void main(String[] args) { SpringApplication.run(ConfigClientMain3355.class, args); } } ``` * 业务类:将配置信息以 REST 窗口的形式暴露 ```java @RestController public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/configInfo") public String getConfigInfo() { return configInfo; } } ``` *** ##### 动态刷新 分布式配置的动态刷新问题,修改 GitHub 上的配置文件,Config Server 配置中心立刻响应,但是 Config Client 客户端没有任何响应,需要重启客户端 * 引入 pom 依赖: ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator ``` * 修改 yml,暴露监控端口:SpringBoot 的 actuator 启动端点监控 Web 端默认加载默认只有两个 info,health 可见的页面节点 ```yaml management: endpoints: web: exposure: include: "*" # 表示包含所有节点页面 exclude: env,beans # 表示排除env、beans ``` * 业务类:加 @RefreshScope 注解 ```java @RestController @RefreshScope public class ConfigClientController { // 从配置文件中取前缀为server.port的值 @Value("${config.info}") private String configInfo; // config-{profile}.yml @GetMapping("/configInfo") public String getConfigInfo() { return configInfo; } } ``` 此时客户端还是没有刷新,需要发送 POST 请求刷新 3355:`curl -X POST "http://localhost:3355/actuator/refresh` 引出问题: * 在微服务多的情况下,每个微服务都需要执行一个 POST 请求,手动刷新成本太大 * 可否广播,一次通知,处处生效,大范围的实现自动刷新 解决方法:Bus 总线 *** ## 服务消息 ### Bus #### 基本介绍 Spring Cloud Bus 能管理和传播分布式系统间的消息,就像分布式执行器,可用于广播状态更改、事件推送、微服务间的通信通道等 消息总线:在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称为消息总线 基本原理:ConfigClient 实例都监听 MQ 中同一个 Topic(默认 springCloudBus),当一个服务刷新数据时,会把信息放入 Topic 中,这样其它监听同一 Topic 的服务就能得到通知,然后去更新自身的配置 **** #### 全局广播 利用消息总线接触一个服务端 ConfigServer 的 `/bus/refresh` 断点,从而刷新所有客户端的配置 Cloud-Bus全局广播架构 改造 ConfigClient: * 引入 MQ 的依赖: ```xml org.springframework.cloud spring-cloud-starter-bus-amqp ``` * yml 文件添加 MQ 信息: ```yaml server: port: 3344 spring: application: name: config-client #注册进Eureka服务器的微服务名 cloud: # rabbitmq相关配置 rabbitmq: host: localhost port: 5672 username: guest password: guest # rabbitmq相关配置,暴露bus刷新配置的端点 management: endpoints: # 暴露bus刷新配置的端点 web: exposure: include: 'bus-refresh' ``` * 只需要调用一次 `curl -X POST "http://localhost:3344/actuator/bus-refresh`,可以实现全局广播 *** #### 定点通知 动态刷新情况下,只通知指定的微服务,比如只通知 3355 服务,不通知 3366,指定具体某一个实例生效,而不是全部 公式:`http://localhost:port/actuator/bus-refresh/{destination}` /bus/refresh 请求不再发送到具体的服务实例上,而是发给 Config Server 并通过 destination 参数类指定需要更新配置的服务或实例 ![Cloud-Bus工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Bus工作流程.png) **** ### Stream #### 基本介绍 Spring Cloud Stream 是一个构建消息驱动微服务的框架,通过定义绑定器 Binder 作为中间层,实现了应用程序与消息中间件细节之间的隔离,屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型 Stream 中的消息通信方式遵循了发布订阅模式,Binder 可以生成 Binding 用来绑定消息容器的生产者和消费者,Binding 有两种类型 Input 和 Output,Input 对应于消费者(消费者从 Stream 接收消息),Output 对应于生产者(生产者从 Stream 发布消息) ![Cloud-Stream工作流程](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Stream工作流程.png) - Binder:连接中间件 - Channel:通道,是队列 Queue 的一种抽象,在消息通讯系统中实现存储和转发的媒介,通过 Channel 对队列进行配置 - Source、Sink:生产者和消费者 中文手册:https://m.wang1314.com/doc/webapp/topic/20971999.html **** #### 基本使用 Binder 是应用与消息中间件之间的封装,目前实现了 Kafka 和 RabbitMQ 的 Binder,可以动态的改变消息类型(Kafka 的 Topic 和 RabbitMQ 的 Exchange),可以通过配置文件实现,常用注解如下: * @Input:标识输入通道,接收的消息通过该通道进入应用程序 * @Output:标识输出通道,发布的消息通过该通道离开应用程序 * @StreamListener:监听队列,用于消费者队列的消息接收 * @EnableBinding:信道 Channel 和 Exchange 绑定 生产者发消息模块: * 引入 pom 依赖:RabbitMQ ```xml org.springframework.cloud spring-cloud-starter-stream-rabbit ``` * application.yml: ```yaml server: port: 8801 spring: application: name: cloud-stream-provider cloud: stream: binders: # 在此处配置要绑定的rabbitmq的服务信息; defaultRabbit: # 表示定义的名称,用于于binding整合 type: rabbit # 消息组件类型 environment: # 设置rabbitmq的相关的环境配置 spring: rabbitmq: host: localhost port: 5672 username: guest password: guest bindings: # 服务的整合处理 output: # 这个名字是一个通道的名称 destination: studyExchange # 表示要使用的Exchange名称定义 content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain” binder: defaultRabbit # 设置要绑定的消息服务的具体设置 eureka: client: # 客户端进行Eureka注册的配置 service-url: defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka instance: lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒) lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒) instance-id: send-8801.com # 在信息列表时显示主机名称 prefer-ip-address: true # 访问的路径变为IP地址 ``` * 主启动类: ```java @SpringBootApplication @EnableEurekaClient public class StreamMQMain8801 { public static void main(String[] args) { SpringApplication.run(StreamMQMain8801.class, args); } } ``` * 业务类:MessageChannel 的实例名必须是 output,否则无法启动 ```java // 可以理解为定义消息的发送管道Source对应output(生产者),Sink对应input(消费者) @EnableBinding(Source.class) // @Service:这里不需要,不是传统的controller调用service。这个service是和rabbitMQ打交道的 // IMessageProvider 只有一个 send 方法的接口 public class MessageProviderImpl implements IMessageProvider { @Resource private MessageChannel output; // 消息的发送管道 @Override public String send() { String serial = UUID.randomUUID().toString(); //创建消息,通过output这个管道向消息中间件发消息 this.output.send(MessageBuilder.withPayload(serial).build()); System.out.println("***serial: " + serial); return serial; } } ``` * Controller: ```java @RestController public class SendMessageController { @Resource private IMessageProvider messageProvider; @GetMapping(value = "/sendMessage") public String sendMessage() { return messageProvider.send(); } } ``` 消费者模块:8802 和 8803 两个消费者 * application.yml:只标注出与生产者不同的地方 ```yaml server: port: 8802 spring: application: name: cloud-stream-consumer cloud: stream: # ... bindings: # 服务的整合处理 input: # 这个名字是一个通道的名称 # ... binder: { defaultRabbit } # 设置要绑定的消息服务的具体设置 eureka: # ... instance: # ... instance-id: receive-8802.com # 在信息列表时显示主机名称 ``` * Controller: ```java @Component @EnableBinding(Sink.class) // 理解为定义一个消息消费者的接收管道 public class ReceiveMessageListener { @Value("${server.port}") private String serverPort; @StreamListener(Sink.INPUT) //输入源:作为一个消息监听者 public void input(Message message) { // 获取到消息 String messageStr = message.getPayload(); System.out.println("消费者1号,------->接收到的消息:" + messageStr + "\t port: " + serverPort); } } ``` **** #### 高级特性 重复消费问题:生产者 8801 发送一条消息后,8802 和 8803 会同时收到 8801 的消息 解决方法:微服务应用放置于同一个 group 中,能够保证消息只会被其中一个应用消费一次。不同的组是可以全面消费的(重复消费),同一个组内的多个消费者会发生竞争关系,只有其中一个可以消费 ```yaml bindings: input: destination: studyExchange content-type: application/json binder: { defaultRabbit } group: seazean # 设置分组 ``` 消息持久化问题: * 停止 8802/8803 并去除掉 8802 的分组 group: seazean,8801 先发送 4 条消息到 MQ * 先启动 8802,无分组属性配置,后台没有打出来消息,消息丢失 * 再启动 8803,有分组属性配置,后台打印出来了 MQ 上的消息 ***** ### Sleuth #### 基本介绍 Spring Cloud Sleuth 提供了一套完整的分布式请求链路跟踪的解决方案,并且兼容支持了 zipkin 在微服务框架中,一个客户端发起的请求在后端系统中会经过多次不同的服务节点调用来协同产生最后的请求结果,形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败,所以需要链路追踪 Sleuth 官网:https://github.com/spring-cloud/spring-cloud-sleuth zipkin 下载地址:https://repo1.maven.org/maven2/io/zipkin/java/zipkin-server/ *** #### 链路监控 Sleuth 负责跟踪整理,zipkin 负责可视化展示 ```bash java -jar zipkin-server-2.12.9-exec.jar # 启动 zipkin ``` 访问 http://localhost:9411/zipkin/ 展示交互界面 一条请求链路通过 Trace ID 唯一标识,Span 标识发起的请求信息 * Trace:类似于树结构的 Span 集合,表示一条调用链路,存在唯一 ID 标识 * Span:表示调用链路来源,通俗的理解 Span 就是一次请求信息,各个 Span 通过 ParentID 关联起来 服务生产者模块: * 引入 pom 依赖: ```xml org.springframework.cloud spring-cloud-starter-zipkin ``` * application.yml: ```yaml server: port: 8001 spring: application: name: cloud-payment-service zipkin: base-url: http://localhost:9411 sleuth: sampler: #采样率值介于 0 到 1 之间,1 则表示全部采集 probability: 1 ``` * 业务类: ```java @GetMapping("/payment/zipkin") public String paymentZipkin() { return "hi ,i'am paymentzipkin server fall back,welcome to seazean"; } ``` 服务消费者模块: * application.yml: ```yaml server: port: 80 # 微服务名称 spring: application: name: cloud-order-service zipkin: base-url: http://localhost:9411 sleuth: sampler: probability: 1 ``` * 业务类: ```java @GetMapping("/comsumer/payment/zipkin") public String paymentZipKin() { String result = restTemplate.getForObject("http://localhost:8001" + "/payment/zipkin/", String.class); return result; } ``` **** ## Alibaba Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案,此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务 - 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Zuul、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。 - 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持 - 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新 - 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力 - 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题 - 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务 - 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行 官方文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md 官方手册:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html ### Nacos #### 基本介绍 Nacos 全称 Dynamic Naming and Configuration Service,一个更易于构建云原生应用的动态服务发现、配置管理和服务的管理平台,Nacos = Eureka + Config + Bus 下载地址:https://github.com/alibaba/nacos/releases 启动命令:命令运行成功后直接访问 http://localhost:8848/nacos,默认账号密码都是 nacos ```bash startup.cmd -m standalone # standalone 代表着单机模式运行,非集群模式 ``` 关闭命令: ```bash shutdown.cmd ``` 注册中心对比:C 一致性,A 可用性,P 分区容错性 | 注册中心 | CAP 模型 | 控制台管理 | | :-------: | :------: | :--------: | | Eureka | AP | 支持 | | Zookeeper | CP | 不支持 | | Consul | CP | 支持 | | Nacos | AP | 支持 | 切换模式:`curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP` 官网:https://nacos.io **** #### 注册中心 Nacos 作为服务注册中心 服务提供者: * 引入 pom 依赖: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` * application.yml: ```yaml server: port: 9001 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: localhost:8848 #配置Nacos地址,注册到Nacos # 做监控需要把这个全部暴露出来 management: endpoints: web: exposure: include: '*' ``` * 主启动类:注解是 EnableDiscoveryClient ```java @EnableDiscoveryClient @SpringBootApplication public class PaymentMain9001 { public static void main(String[] args) { SpringApplication.run(PaymentMain9001.class, args); } } ``` * Controller: ```java @RestController public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping(value = "/payment/nacos/{id}") public String getPayment(@PathVariable("id") Integer id) { return "nacos registry, serverPort: " + serverPort + "\t id" + id; } } ``` * 管理后台服务: ![Cloud-Nacos服务列表](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos服务列表.png) * 新建一个模块端口是 9002,其他与 9001 服务一样,nacos-payment-provider 的实例数就变为 2 服务消费者: * application.yml: ```yaml server: port: 83 spring: application: name: nacos-order-consumer cloud: nacos: discovery: server-addr: localhost:8848 # 消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者) service-url: nacos-user-service: http://nacos-payment-provider ``` * 主启动类: ```java @SpringBootApplication @EnableDiscoveryClient public class OrderNacosMain83 { public static void main(String[] args) { SpringApplication.run(OrderNacosMain83.class, args); } } ``` * 业务类: ```java @Configuration public class ApplicationContextBean { @Bean @LoadBalanced // 生产者集群状态下,必须添加,防止找不到实例 public RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` ```java @RestController @Slf4j public class OrderNacosController { @Resource private RestTemplate restTemplate; // 从配置文件中读取 URL @Value("${service-url.nacos-user-service}") private String serverURL; @GetMapping("/consumer/payment/nacos/{id}") public String paymentInfo(@PathVariable("id") Long id) { String result = restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class); return result; } } ``` *** #### 配置中心 ##### 基础配置 把配置文件写进 Nacos,然后再用 Nacos 做 config 这样的功能,直接从 Nacos 上抓取服务的配置信息 在 Nacos 中,dataId 的完整格式如下 `${prefix}-${spring.profiles.active}.${file-extension}` * `prefix`:默认为 `spring.application.name` 的值,也可以通过配置项 `spring.cloud.nacos.config.prefix` 来配置 * `spring.profiles.active`:当前环境对应的 profile,当该值为空时,dataId 的拼接格式变成 `${prefix}.${file-extension}` * `file-exetension`:配置内容的数据格式,可以通过配置项 `spring.cloud.nacos.config.file-extension` 来配置,目前只支持 properties 和 yaml 类型(不是 yml) 构建流程: * 引入 pom 依赖: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config ``` * 配置两个 yml 文件:配置文件的加载是存在优先级顺序的,bootstrap 优先级高于 application bootstrap.yml:全局配置 ```yaml # nacos配置 server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension} ``` application.yml:服务独立配置,表示服务要去配置中心找名为 nacos-config-client-dev.yaml 的文件 ```yaml spring: profiles: active: dev # 表示开发环境 ``` * 主启动类: ```java @SpringBootApplication @EnableDiscoveryClient public class NacosConfigClientMain3377 { public static void main(String[] args) { SpringApplication.run(NacosConfigClientMain3377.class, args); } } ``` * 业务类:@RefreshScope 注解使当前类下的配置支持 Nacos 的动态刷新功能 ```java @RestController @RefreshScope public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/config/info") public String getConfigInfo() { return configInfo; } } ``` * 新增配置,然后访问 http://localhost:3377/config/info ![Cloud-Nacos新增配置](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos新增配置.png) *** ##### 分类配置 分布式开发中的多环境多项目管理问题,Namespace 用于区分部署环境,Group 和 DataID 逻辑上区分两个目标对象 ![Cloud-Nacos配置说明](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos配置说明.png) Namespace 默认 public,主要用来实现隔离,图示三个开发环境 Group 默认是 DEFAULT_GROUP,Group 可以把不同的微服务划分到同一个分组里面去 *** ##### 加载配置 DataID 方案:指定 `spring.profile.active` 和配置文件的 DataID 来使不同环境下读取不同的配置 Group 方案:通过 Group 实现环境分区,在 config 下增加一条 Group 的配置即可 Namespace 方案: ```yaml server: port: 3377 spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 config: server-addr: localhost:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 group: DEV_GROUP namespace: 95d44530-a4a6-4ead-98c6-23d192cee298 ``` ![Cloud-Nacos命名空间](https://seazean.oss-cn-beijing.aliyuncs.com/img/Frame/Cloud-Nacos命名空间.png) **** #### 集群架构 集群部署参考官方文档,Nacos 支持的三种部署模式: - 单机模式:用于测试和单机使用 - 集群模式:用于生产环境,确保高可用 - 多集群模式:用于多数据中心场景 集群部署文档:https://nacos.io/zh-cn/docs/v2/guide/admin/cluster-mode-quick-start.html 默认 Nacos 使用嵌入式数据库 derby 实现数据的存储,重启 Nacos 后配置文件不会消失,但是多个 Nacos 节点数据存储存在一致性问题,每个 Nacos 都有独立的嵌入式数据库,所以 Nacos 采用了集中式存储的方式来支持集群化部署,目前只支持 MySQL 的存储 Windows 下 Nacos 切换 MySQL 存储: * 在 Nacos 安装目录的 conf 目录下找到一个名为 `nacos-mysql.sql` 的脚本并执行 * 在 conf 目录下找到 `application.properties`,增加如下数据 ```properties spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=username db.password=password ``` * 重新启动 Nacos,可以看到是个全新的空记录界面 Linux 参考:https://www.yuque.com/mrlinxi/pxvr4g/rnahsn#dPvMy **** ### Sentinel #### 基本介绍 Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件 Sentinel 分为两个部分: - 核心库(Java 客户端)不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境 - 控制台(Dashboard)主要负责管理推送规则、监控、管理机器信息等 下载到本地,运行命令:`java -jar sentinel-dashboard-1.8.2.jar` (要求 Java8,且 8080 端口不能被占用),访问 http://localhost:8080/,账号密码均为 sentinel 官网:https://sentinelguard.io 下载地址:https://github.com/alibaba/Sentinel/releases **** #### 基本使用 构建演示工程: * 引入 pom 依赖: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` * application.yml:sentinel.transport.port 端口配置会在应用对应的机器上启动一个 HTTP Server,该 Server 与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了 1 个限流规则,会把规则数据 Push 给 Server 接收,Server 再将规则注册到 Sentinel 中 ```yaml server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 # Nacos 服务注册中心地址【需要启动Nacos8848】 sentinel: transport: # 配置Sentinel dashboard地址 dashboard: localhost:8080 # 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719 management: endpoints: web: exposure: include: '*' ``` * 主启动类: ```java @EnableDiscoveryClient @SpringBootApplication public class SentinelMainApp8401 { public static void main(String[] args) { SpringApplication.run(SentinelMainApp8401.class, args); } } ``` * 流量控制 Controller: ```java @RestController @Slf4j public class FlowLimitController { @GetMapping("/testA") public String testA() { return "------testA"; } @GetMapping("/testB") public String testB() { return "------testB"; } } ``` * Sentinel 采用懒加载机制,需要先访问 http://localhost:8401/testA,控制台才能看到 *** #### 流控规则 流量控制规则 FlowRule:同一个资源可以同时有多个限流规则 * 资源名 resource:限流规则的作用对象,Demo 中为 testA * 针对资源 limitApp:针对调用者进行限流,默认为 default 代表不区分调用来源 * 阈值类型 grade:QPS 或线程数模式 * 单机阈值 count:限流阈值 * 流控模式 strategy:调用关系限流策略 * 直接:资源本身达到限流条件直接限流 * 关联:当关联的资源达到阈值时,限流自身 * 链路:只记录指定链路上的流量,从入口资源进来的流量 * 流控效果 controlBehavior: * 快速失败:直接失败,抛出异常 * Warm Up:冷启动,根据 codeFactory(冷加载因子,默认 3)的值,从 count/codeFactory 开始缓慢增加,给系统预热时间 * 排队等待:匀速排队,让请求以匀速的方式通过,阈值类型必须设置为 QPS,否则无效 Cloud-Sentinel增加流控规则 通过调用 `SystemRuleManager.loadRules()` 方法来用硬编码的方式定义流量控制规则: ```java private void initSystemProtectionRule() { List rules = new ArrayList<>(); SystemRule rule = new SystemRule(); rule.setHighestSystemLoad(10); rules.add(rule); SystemRuleManager.loadRules(rules); } ``` 详细内容参考文档:https://sentinelguard.io/zh-cn/docs/flow-control.html **** #### 降级熔断 Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时,对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException) Sentinel 提供以下几种熔断策略: * 资源名 resource:限流规则的作用对象,Demo 中为 testA * 熔断策略 grade: * 慢调用比例(SLOW_REQUEST_RATIO):以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断 * 异常比例(ERROR_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 `[0.0, 1.0]`,代表 0% - 100% * 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断 * 单机阈值 count:慢调用比例模式下为慢调用临界 RT;异常比例/异常数模式下为对应的阈值 * 熔断时长 timeWindow:单位为 s * 最小请求数 minRequestAmount:熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断,默认 5 * 统计时长 statIntervalMs:单位统计时长 * 慢调用比例阈值 slowRatioThreshold:仅慢调用比例模式有效 Cloud-Sentinel增加熔断规则 注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常 BlockException 不生效,为了统计异常比例或异常数,需要通过 `Tracer.trace(ex)` 记录业务异常或者通过`@SentinelResource` 注解会自动统计业务异常 ```java Entry entry = null; try { entry = SphU.entry(resource); // Write your biz code here. // <> } catch (Throwable t) { if (!BlockException.isBlockException(t)) { Tracer.trace(t); } } finally { if (entry != null) { entry.exit(); } } ``` 详细内容参考文档:https://sentinelguard.io/zh-cn/docs/circuit-breaking.html **** #### 热点限流 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流,Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控 Cloud-Sentinel热点参数限流 引入 @SentinelResource 注解:https://sentinelguard.io/zh-cn/docs/annotation-support.html - value:Sentinel 资源名,默认为请求路径,这里 value 的值可以任意写,但是约定与 Restful 地址一致 - blockHandler:表示触发了 Sentinel 中配置的流控规则,就会调用兜底方法 `del_testHotKey` - blockHandlerClass:如果设置了该值,就会去该类中寻找 blockHandler 方法 - fallback:用于在抛出异常的时候提供 fallback 处理逻辑 说明:fallback 对应服务降级(服务出错了需要有个兜底方法),blockHandler 对应服务熔断(服务不可用的兜底方法) ```java @GetMapping("/testHotKey") @SentinelResource(value = "testHotKey", blockHandler = "del_testHotKey") public String testHotkey(@RequestParam(value = "p1", required = false) String p1, @RequestParam(value = "p1", required = false) String p2) { return "------testHotkey"; } // 自定义的兜底方法,必须是 BlockException public String del_testHotKey(String p1, String p2, BlockException e) { return "不用默认的兜底提示 Blocked by Sentinel(flow limiting),自定义提示:del_testHotKeyo."; } ``` 图示设置 p1 参数限流,规则是 1s 访问 1 次,当 p1=5 时 QPS > 100,只访问 p2 不会出现限流 `http://localhost:8401/testHotKey?p2=b` Cloud-Sentinel增加热点规则 * 参数索引 paramIdx:热点参数的索引,图中索引 0 对应方法中的 p1 参数 * 参数例外项 paramFlowItemList:针对指定的参数值单独设置限流阈值,不受 count 阈值的限制,**仅支持基本类型和字符串类型** 说明:@SentinelResource 只管控制台配置规则问题,出现运行时异常依然会报错 详细内容参考文档:https://sentinelguard.io/zh-cn/docs/parameter-flow-control.html *** #### 系统规则 Sentinel 系统自适应保护从整体维度对**应用入口流量**进行控制,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性 系统规则支持以下的阈值类型: - Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护,系统容量由系统的 `maxQps * minRt` 计算得出,设定参考值一般是 `CPU cores * 2.5` - RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒 - 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护 - 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护 - CPU usage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0) Cloud-Sentinel增加系统规则 详细内容参考文档:https://sentinelguard.io/zh-cn/docs/system-adaptive-protection.html **** #### 服务调用 消费者需要进行服务调用 * 引入 pom 依赖: ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` * application.yml:激活 Sentinel 对 Feign 的支持 ```yaml feign: sentinel: enabled: true ``` * 主启动类:加上 @EnableFeignClient 注解开启 OpenFeign * 业务类: ```java // 指明调用失败的兜底方法在PaymentFallbackService,使用 fallback 方式是无法获取异常信息的, // 如果想要获取异常信息,可以使用 fallbackFactory 参数 @FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class) public interface PaymentFeignService { // 去生产则服务中找相应的接口,方法签名一定要和生产者中 controller 的一致 @GetMapping(value = "/paymentSQL/{id}") public CommonResult paymentSQL(@PathVariable("id") Long id); } ``` ```java @Component //不要忘记注解,降级方法 public class PaymentFallbackService implements PaymentFeignService { @Override public CommonResult paymentSQL(Long id) { return new CommonResult<>(444,"服务降级返回,没有该流水信息-------PaymentFallbackSe ``` **** #### 持久化 配置持久化: * 引入 pom 依赖: ```xml com.alibaba.csp sentinel-datasource-nacos ``` * 添加 Nacos 数据源配置: ```yaml server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服务注册中心地址 sentinel: transport: dashboard: localhost:8080 port: 8719 # 关闭默认收敛所有URL的入口context,不然链路限流不生效 web-context-unify: false # filter: # enabled: false # 关闭自动收敛 #持久化配置 datasource: ds1: nacos: server-addr: localhost:8848 dataId: cloudalibaba-sentinel-service groupId: DEFAULT_GROUP data-type: json rule-type: flow ``` **** ### Seata #### 分布事物 一个分布式事务过程,可以用分布式处理过程的一 ID + 三组件模型来描述: * XID (Transaction ID):全局唯一的事务 ID,在这个事务ID下的所有事务会被统一控制 * TC (Transaction Coordinator):事务协调者,维护全局和分支事务的状态,驱动全局事务提交或回滚 * TM (Transaction Manager):事务管理器,定义全局事务的范围,开始全局事务、提交或回滚全局事务 * RM (Resource Manager):资源管理器,管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚 典型的分布式事务流程: 1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID 2. XID 在微服务调用链路的上下文中传播(也就是在多个 TM,RM 中传播) 3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖 4. TM 向 TC 发起针对 XID 的全局提交或回滚决议 5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求 Cloud-分布式事务流程 *** #### 基本配置 Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案 下载 seata-server 文件修改 conf 目录下的配置文件 * file.conf:自定义事务组名称、事务日志存储模式为 db、数据库连接信息 **事务分组**:seata 的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字 Cloud-Seata配置文件 * 数据库新建库 seata,建表 db_store.sql 在 https://github.com/seata/seata/tree/2.x/script/server/db 目录里面 * registry.conf:指明注册中心为 Nacos,及修改 Nacos 连接信息 Cloud-Seata注册中心配置 启动 Nacos 和 Seata,如果 DB 报错,需要把将 lib 文件夹下 mysql-connector-java-5.1.30.jar 删除,替换为自己 MySQL 连接器版本 Cloud-Seata配置成功 官网:https://seata.io 下载地址:https://github.com/seata/seata/releases 基本介绍:https://seata.io/zh-cn/docs/overview/what-is-seata.html *** #### 基本使用 两个注解: * Spring 提供的本地事务:@Transactional * Seata 提供的全局事务:**@GlobalTransactional** 搭建简单 Demo: * 创建 UNDO_LOG 表:SEATA AT 模式需要 `UNDO_LOG` 表,如果一个模块的事务提交了,Seata 会把提交了哪些数据记录到 undo_log 表中,如果这时 TC 通知全局事务回滚,那么 RM 就从 undo_log 表中获取之前修改了哪些资源,并根据这个表回滚 ```sql -- 注意此处0.3.0+ 增加唯一索引 ux_undo_log CREATE TABLE `undo_log` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `branch_id` bigint(20) NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int(11) NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; ``` * 引入 pom 依赖: ```xml com.alibaba.cloud spring-cloud-starter-alibaba-seata ${spring-cloud-alibaba.version} ``` * application.yml: ```yaml spring: application: name: seata-order-service cloud: alibaba: seata: # 自定义事务组名称需要与seata-server中file.conf中配置的事务组ID对应 # vgroup_mapping.my_test_tx_group = "my_group" tx-service-group: my_group nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC username: root password: 123456 ``` * 构建三个服务: ```java // 仓储服务 public interface StorageService { // 扣除存储数量 void deduct(String commodityCode, int count); } // 订单服务 public interface OrderService { // 创建订单 Order create(String userId, String commodityCode, int orderCount); } // 帐户服务 public interface AccountService { // 从用户账户中借出 void debit(String userId, int money); } ``` * 业务逻辑:增加 @GlobalTransactional 注解 ```java public class OrderServiceImpl implements OrderService { @Resource private OrderDAO orderDAO; @Resource private AccountService accountService; @Transactional(rollbackFor = Exception.class) public Order create(String userId, String commodityCode, int orderCount) { int orderMoney = calculate(commodityCode, orderCount); // 账户扣钱 accountService.debit(userId, orderMoney); Order order = new Order(); order.userId = userId; order.commodityCode = commodityCode; order.count = orderCount; order.money = orderMoney; return orderDAO.insert(order); } } ``` ```java public class BusinessServiceImpl implements BusinessService { @Resource private StorageService storageService; @Resource private OrderService orderService; // 采购,涉及多服务的分布式事务问题 @GlobalTransactional @Transactional(rollbackFor = Exception.class) public void purchase(String userId, String commodityCode, int orderCount) { storageService.deduct(commodityCode, orderCount); orderService.create(userId, commodityCode, orderCount); } } ``` 详细示例参考:https://github.com/seata/seata-samples/tree/master/springcloud-nacos-seata

Links:

访问springmvc后台controllerjavascript:void(0);
传递Json格式POJOjavascript:void(0);
传递Json格式Listjavascript:void(0);
跨域访问javascript:void(0);
点击javascript:void(0);

URLs of crawlers that visited me.