首页 文章 精选 留言 我的

精选列表

搜索[指标分析],共10000篇文章
优秀的个人博客,低调大师

Spring MVC请求处理流程分析

一、简介 Spring MVC框架在工作中经常用到,配置简单,使用起来也很方便,很多书籍和博客都有介绍其处理流程,但是,对于其原理,总是似懂非懂的样子。我们做技术,需要做到知其然,还要知其所以然。今天我们结合源码来深入了解一下Spring MVC的处理流程。 以上流程图是Spring MVC的处理流程(参考:spring-mvc-flow-with-example),原作者对流程的解释如下: Step 1: First request will be received by DispatcherServlet. Step 2: DispatcherServlet will take the help of HandlerMapping and get to know the Controller class name associated with the given request. Step 3: So request transfer to the Controller, and then controller will process the request by executing appropriate methods and returns ModelAndView object (contains Model data and View name) back to the DispatcherServlet. Step 4: Now DispatcherServlet send the model object to the ViewResolver to get the actual view page. Step 5: Finally DispatcherServlet will pass the Model object to the View page to display the result. 针对以上流程,这里需要更加详细一点: 1、请求被web 容器接收,并且根据contextPath将请求发送给DispatcherServlet 2、DispatcherServlet接收到请求后,会设置一些属性(localeResolver、themeResolver等等),在根据request在handlerMappings中查找对应的HandlerExecutionChain;然后根据HandlerExecutionChain中的handler来找到HandlerAdapter,然后通过反射来调用handler中的对应方法(RequestMapping对应的方法) 3、handler就是对应的controller,调用controller中的对应方法来进行业务逻辑处理,返回ModelAndView(或者逻辑视图名称) 4、ViewResolver根据逻辑视图名称、视图前后缀,来获取实际的逻辑视图 5、获取实际视图之后,就会使用model来渲染视图,得到用户实际看到的视图,然后返回给客户端。 二、Demo样例 我们运行一个小样例(github地址:https://github.com/yangjianzhou/spring-mvc-demo)来了解Spring MVC处理流程,项目结构如下: web.xml配置如下: <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>smart</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>smart</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app> smart-servlet.xml的内容如下: <context:component-scan base-package="com.iwill.mvc"/> <!-- 在使用Excel PDF的视图时,请先把这个视图解析器注释掉,否则产生视图解析问题--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:order="100" p:viewClass="org.springframework.web.servlet.view.JstlView" p:prefix="/WEB-INF/views/" p:suffix=".jsp"/> UserController.java的代码如下: package com.iwill.mvc; import org.apache.log4j.Logger; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping("/user") public class UserController { Logger logger = Logger.getLogger(UserController.class); @RequestMapping("register") public String register() { logger.info("invoke register"); return "user/register"; } @RequestMapping(method = RequestMethod.POST) public ModelAndView createUser(User user) { ModelAndView mav = new ModelAndView(); mav.setViewName("user/createSuccess"); mav.addObject("user", user); return mav; } } 三、请求接收 DispatcherServlet的类继承关系如下: 可以看出,DispatcherServlet是一个HttpServlet,因此,它可以处理http请求。 在浏览器中输入http://localhost:8080/spring-mvc-demo/user/register,因为在web服务器上配置了spring-mvc-demo的contextPath为spring-mvc-demo,所以/spring-mvc-demo/user/register的请求就会被DispatcherServlet处理,请求处理路径如下: 请求由tomcat传递给了DispatcherServlet了,DispatcherServlet接收后,就开始自己的特殊处理了。 红框中是Spring MVC自己特有的逻辑,主要是与视图、主题有关。 接下来的主要处理逻辑在org.springframework.web.servlet.DispatcherServlet#doDispatch中: 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; // 根据request在handlerMappings中获取HandlerExecutionChain mappedHandler = getHandler(processedRequest, false); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } //根据handler在handlerAdapters中获取HandlerAdapter HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { String requestUri = urlPathHelper.getRequestUri(request); logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } try { //适配器调用实际的handler mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); } finally { if (asyncManager.isConcurrentHandlingStarted()) { return; } } applyDefaultViewName(request, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } //逻辑视图名转换为物理视图名,并进行视图渲染 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Error err) { triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); return; } // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } 首先获取HandlerExecutionChain(入口:mappedHandler = getHandler(processedRequest, false);),方法如下: protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null; } 之后就是根据handler获取HandlerAdapter(入口:HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler())): protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { for (HandlerAdapter ha : this.handlerAdapters) { if (logger.isTraceEnabled()) { logger.trace("Testing handler adapter [" + ha + "]"); } if (ha.supports(handler)) { return ha; } } throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); } handler适配器调用handler的方法(入口:mv = ha.handle(processedRequest, response, mappedHandler.getHandler())): protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ServletHandlerMethodResolver methodResolver = getMethodResolver(handler); Method handlerMethod = methodResolver.resolveHandlerMethod(request); ServletHandlerMethodInvoker methodInvoker = new ServletHandlerMethodInvoker(methodResolver); ServletWebRequest webRequest = new ServletWebRequest(request, response); ExtendedModelMap implicitModel = new BindingAwareModelMap(); Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest); methodInvoker.updateModelAttributes(handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest); return mav; } 通过Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel)会调用底层的方法: 红框中,通过反射调用UserController的register方法。这样请求就被传递到了实际的controller方法了。 四、响应返回 UserController#register处理后,就返回逻辑视图名:user/register。在org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.ServletHandlerMethodInvoker#getModelAndView中,就会将String转化为ModelAndView: 在org.springframework.web.servlet.DispatcherServlet#render中,就会将逻辑视图ModelAndView转化物理视图。 resolveViewName就是使用ViewResolver来获取物理视图名: protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception { for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } return null; } 物理视图名会被缓存,不需要重复解析,提高性能。 这里就是prefix和suffix的用途了,用于定位实际视图。 获取到了物理视图之后,就进行视图渲染了。 针对jsp格式的视图,我们配置的view是org.springframework.web.servlet.view.JstlView,渲染过程就是将model中的值set到request的attribute中,之后就是使用jsp自己的规则来显示jsp文件就好。

优秀的个人博客,低调大师

缓存穿透问题分析压测

缓存穿透 缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。 本篇讨论缓存击穿的其中一个表现: 对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑另外一个问题:缓存被“击穿”的问题。 概念:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。 如何解决:使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。类似下面的代码: public String get(key) { String value = redis.get(key); if (value == null) { //代表缓存值过期 //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex); } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可 sleep(50); get(key); //重试 } } else { return value; } } 接下来,进行并发压力测试和优化: 首先是不使用setNX进行并发压力测试 代码如下: package cn.chinotan.controller; import lombok.extern.java.Log; import org.apache.catalina.servlet4preview.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; /** * @program: test * @description: redis测试 * @author: xingcheng * @create: 2019-03-09 16:26 **/ @RestController @RequestMapping("/redis") @Log public class RedisController { @Autowired StringRedisTemplate redisTemplate; public static final String KEY = "chinotan:redis:pass"; public static final String VALUE = "redis-pass-value"; /** * 模拟耗时操作 3秒 */ public static final Long TIME_CONSUMING = 3 * 1000L; /** * VALUE缓存时间 5秒 */ public static final Long VALUE_TIME = 5 * 1000L; @GetMapping(value = "/pass") public Object hello(HttpServletRequest request) throws Exception { long cacheStart = System.currentTimeMillis(); String value = redisTemplate.opsForValue().get(KEY); long cacheEnd = System.currentTimeMillis(); if (StringUtils.isBlank(value)) { // 模拟耗时操作,从数据库获取 long start = System.currentTimeMillis(); TimeUnit.MILLISECONDS.sleep(TIME_CONSUMING); redisTemplate.opsForValue().set(KEY, VALUE, VALUE_TIME, TimeUnit.MILLISECONDS); long end = System.currentTimeMillis(); log.info("从数据库中获取耗时: " + (end - start) + "ms"); return VALUE; } else { log.info("从缓存中获取耗时:" + (cacheEnd - cacheStart) + "ms"); return value; } } } 很简单的一个get请求,先从缓存中获取数据,如果数据不存在,则从数据库获取,这里用 TimeUnit.MILLISECONDS.sleep(TIME_CONSUMING); 来模拟一个复杂的从数据库获取数据的操作,耗时设定为3秒钟 本次测试采用的是springBoot2.0以上进行部署,jmeter进行压力并发测试 在压力测试之前,进行springboot自带的tomcat并发数和连接数调整以及redis连接池的调整 redis的连接池调整如下: spring: redis: database: 0 host: 127.0.0.1 jedis: pool: #最大连接数据库连接数 max-active: 5000 #最大等待连接中的数量 max-idle: 5000 #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 max-wait: -1 #最小等待连接中的数量,设 0 为没有限制 min-idle: 10 # lettuce: # pool: # max-active: 5000 # max-idle: 5000 # max-wait: -1 # min-idle: 10 # shutdown-timeout: 5000ms password: port: 6379 timeout: 5000 tomcat的调整如下: server: port: 11111 tomcat: uri-encoding: UTF-8 max-threads: 500 max-connections: 10000 这样redis和tomcat可以支持大并发请求 设置完成后查看设置是否生效: redis连接池不生效举例如下: 必须和配置项相同才正确 之后进行压测准备:下载jmeter,之后步骤如下 启动后控制台打印如下: 可以看到大量并发过来后,会有多次的查看操作,并没有走到缓存,缓存命中率低,缓存的意义就少很多 下面进行优化: package cn.chinotan.controller; import lombok.extern.java.Log; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.JedisCommands; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * @program: test * @description: redis测试 * @author: xingcheng * @create: 2019-03-09 16:26 **/ @RestController @RequestMapping("/redis") @Log public class RedisController { @Autowired StringRedisTemplate redisTemplate; public static final String KEY = "chinotan:redis:pass"; public static final String NX_KEY = "chinotan:redis:nx"; public static final String VALUE = "redis-pass-value"; /** * 间隔时间 3秒 */ public static final Long NX_SLEEP_TIME = 50L; /** * 模拟耗时操作 3秒 */ public static final Long TIME_CONSUMING = 1 * 1000L; /** * VALUE缓存时间 5秒 */ public static final Long VALUE_TIME = 5 * 1000L; /** * 锁缓存时间 5分钟 */ public static final Long NX_TIME = 5 * 60L; @GetMapping(value = "/pass") public Object hello() throws Exception { long cacheStart = System.currentTimeMillis(); String value = redisTemplate.opsForValue().get(KEY); long cacheEnd = System.currentTimeMillis(); if (StringUtils.isBlank(value)) { long start = System.currentTimeMillis(); if (setNX(NX_KEY, NX_KEY)) { // 模拟耗时操作,从数据库获取 TimeUnit.MILLISECONDS.sleep(TIME_CONSUMING); redisTemplate.opsForValue().set(KEY, VALUE, VALUE_TIME, TimeUnit.MILLISECONDS); long end = System.currentTimeMillis(); redisTemplate.delete(NX_KEY); log.info("从数据库中获取耗时: " + (end - start) + "ms"); return VALUE; } else { TimeUnit.MILLISECONDS.sleep(NX_SLEEP_TIME); log.info("缓存穿透递归"); return hello(); } } else { log.info("从缓存中获取耗时:" + (cacheEnd - cacheStart) + "ms"); return value; } } private boolean setNX(String key, String value) { Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, value); redisTemplate.expire(key, NX_TIME, TimeUnit.SECONDS); return aBoolean; } } 通过进行setNX命令操作,这个命令在缓存存在时不会进行覆盖更新写入操作,并返回false,缓存不存在才会进行写入并返回true,通常会被用来分布式锁的设计实现 进行优化后,大量的并发请求不会打到数据库上,而是每隔50ms进行递归重试,这样只有一个请求会请求数据库,其他请求只能从缓存中取数,大大增加了缓存的命中率 下面是压测结果: 可以看到从数据库取数的操作日志只有一条,从而避免了缓存击穿的一个表现问题 下一步优化方向: RedisTemplate提供的setNX操作并不是原子操作(一个是保存数据操作,一个是设置缓存时间操作,是两个请求),在并发环境下可能会有问题,该如何解决呢,欢迎大家留言

优秀的个人博客,低调大师

sync.mutex 源代码分析

sync.Mutex是Go标准库中常用的一个排外锁。当一个 goroutine 获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放。sync.Mutex的实现也是经过多次的演化,功能逐步加强,增加了公平的处理和饥饿机制。 初版的 Mutex首先我们来看看Russ Cox在2008提交的第一版的Mutex实现。 1type Mutex struct { 2 key int32; 3 sema int32; 4} 5 6func xadd(val *int32, delta int32) (new int32) { 7 for { 8 v := *val; 9 if cas(val, v, v+delta) { 10

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册