(ps:前几个星期发生的事情)之前同事跟我说不要使用@Autowired方式注入HttpServletRequest(ps:我们的代码之前用的是第2种方式)。同事的意思大概是注入的HttpServletRequest对象是同一个而且存在线程安全问题。我保持质疑的态度,看了下源码,证明了@Autowired方式不存在线程安全问题,而@ModelAttribute方式存在线程安全问题。
public abstract class BaseController {
@Autowired
protected HttpSession httpSession;
@Autowired
protected HttpServletRequest request;
}
复制代码
public abstract class BaseController1 {
protected HttpServletRequest request;
protected HttpServletResponse response;
protected HttpSession httpSession;
@ModelAttribute
public void init(HttpServletRequest request,
HttpServletResponse response,
HttpSession httpSession) {
this.request = request;
this.response = response;
this.httpSession = httpSession;
}
}
复制代码
线程安全测试
@RequestMapping("/test")
@RestController
public class TestController extends BaseController {
@GetMapping("/1")
public void test1() throws InterruptedException {
// System.out.println("thread.id=" + Thread.currentThread().getId());
// System.out.println("thread.name=" + Thread.currentThread().getName());
// ServletRequestAttributes servletRequestAttributes =
// ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
//
// HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
TimeUnit.SECONDS.sleep(10);
// System.out.println("base.request=" + request);
System.out.println("base.request.name=" + request.getParameter("name"));
}
@GetMapping("/2")
public void test2() throws InterruptedException {
// System.out.println("thread.id=" + Thread.currentThread().getId());
// System.out.println("thread.name=" + Thread.currentThread().getName());
// ServletRequestAttributes servletRequestAttributes =
// ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
//
// HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
// System.out.println("base.request=" + request);
System.out.println("base.request.name=" + request.getParameter("name"));
}
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
binder.registerCustomEditor(Date.class, new CustomDateEditor(sdf, true));
}
}
复制代码
通过JUC的CountDownLatch,模拟同一时刻100个并发请求。
public class Test {
public static void main(String[] args) {
CountDownLatch start = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(100);
CustomThreadPoolExecutor customThreadPoolExecutor = new CustomThreadPoolExecutor(
100, 100, 0L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(100)
);
for (int i = 0; i < 100; i++) {
final int finalName = i;
CustomThreadPoolExecutor.CustomTask task = new CustomThreadPoolExecutor.CustomTask(
new Runnable() {
@Override
public void run() {
try {
start.await();
HttpUtil.get("http://localhost:8081/test/2?name=" + finalName);
} catch (Exception ex) {
ex.printStackTrace();
} finally {
end.countDown();
}
}
}
, "success");
customThreadPoolExecutor.submit(task);
}
start.countDown();
try {
end.await();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
customThreadPoolExecutor.shutdown();
}
}
复制代码
通过观看base.request.name的值并没有null值和存在值重复的现象,很肯定的说@Autowired注入的HttpServletRequest不存在线程安全问题。
base.request.name=78
base.request.name=20
base.request.name=76
base.request.name=49
base.request.name=82
base.request.name=12
base.request.name=80
base.request.name=91
base.request.name=92
base.request.name=30
base.request.name=28
base.request.name=36
base.request.name=41
base.request.name=73
base.request.name=29
base.request.name=2
base.request.name=81
base.request.name=43
base.request.name=35
base.request.name=22
base.request.name=6
base.request.name=27
base.request.name=17
base.request.name=70
base.request.name=65
base.request.name=84
base.request.name=14
base.request.name=54
base.request.name=67
base.request.name=19
base.request.name=21
base.request.name=66
base.request.name=11
base.request.name=53
base.request.name=9
base.request.name=72
base.request.name=64
base.request.name=0
base.request.name=44
base.request.name=89
base.request.name=77
base.request.name=48
base.request.name=1
base.request.name=8
base.request.name=74
base.request.name=46
base.request.name=88
base.request.name=26
base.request.name=24
base.request.name=62
base.request.name=61
base.request.name=51
base.request.name=96
base.request.name=33
base.request.name=45
base.request.name=5
base.request.name=95
base.request.name=68
base.request.name=60
base.request.name=56
base.request.name=42
base.request.name=57
base.request.name=10
base.request.name=55
base.request.name=90
base.request.name=47
base.request.name=97
base.request.name=40
base.request.name=85
base.request.name=86
base.request.name=69
base.request.name=98
base.request.name=13
base.request.name=32
base.request.name=37
base.request.name=4
base.request.name=23
base.request.name=50
base.request.name=38
base.request.name=59
base.request.name=99
base.request.name=71
base.request.name=25
base.request.name=58
base.request.name=34
base.request.name=7
base.request.name=93
base.request.name=31
base.request.name=3
base.request.name=39
base.request.name=75
base.request.name=94
base.request.name=83
base.request.name=63
base.request.name=79
base.request.name=16
base.request.name=52
base.request.name=15
base.request.name=87
base.request.name=18
复制代码
很明显发现base.request.name的值存在null或者重复的现象,说明通过@ModelAttribute注入的HttpServletRequest存在线程安全问题。
base.request.name=97
base.request.name=59
base.request.name=63
base.request.name=14
base.request.name=82
base.request.name=49
base.request.name=86
base.request.name=13
base.request.name=99
base.request.name=29
base.request.name=45
base.request.name=85
base.request.name=8
base.request.name=35
base.request.name=69
base.request.name=70
base.request.name=16
base.request.name=21
base.request.name=74
base.request.name=20
base.request.name=34
base.request.name=23
base.request.name=96
base.request.name=19
base.request.name=67
base.request.name=15
base.request.name=27
base.request.name=43
base.request.name=39
base.request.name=47
base.request.name=87
base.request.name=71
base.request.name=41
base.request.name=38
base.request.name=null
base.request.name=31
base.request.name=32
base.request.name=76
base.request.name=55
base.request.name=75
base.request.name=93
base.request.name=null
base.request.name=56
base.request.name=1
base.request.name=18
base.request.name=89
base.request.name=65
base.request.name=10
base.request.name=78
base.request.name=null
base.request.name=80
base.request.name=24
base.request.name=88
base.request.name=88
base.request.name=44
base.request.name=53
base.request.name=58
base.request.name=61
base.request.name=60
base.request.name=37
base.request.name=92
base.request.name=42
base.request.name=11
base.request.name=68
base.request.name=72
base.request.name=91
base.request.name=79
base.request.name=33
base.request.name=66
base.request.name=54
base.request.name=40
base.request.name=94
base.request.name=46
base.request.name=83
base.request.name=17
base.request.name=64
base.request.name=26
base.request.name=90
base.request.name=7
base.request.name=62
base.request.name=57
base.request.name=73
base.request.name=98
base.request.name=30
base.request.name=6
base.request.name=2
base.request.name=28
base.request.name=5
base.request.name=95
base.request.name=9
base.request.name=3
base.request.name=51
base.request.name=4
base.request.name=52
base.request.name=12
base.request.name=25
base.request.name=36
base.request.name=84
base.request.name=81
base.request.name=50
复制代码
源码分析
1.在Spring容器初始化中,refresh()方法会调用postProcessBeanFactory(beanFactory);
。它是个模板方法,在BeanDefinition被装载后(所有BeanDefinition被加载,但是没有bean被实例化),提供一个修改beanFactory容器的入口。这里还是贴下AbstractApplicationContext中的refresh()方法吧。
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 1.Prepare this context for refreshing.
prepareRefresh();
// 2.Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3.Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// 4.Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// 5.Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// 6.Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// 7.Initialize message source for this context.
initMessageSource();
// 8.Initialize event multicaster for this context.
initApplicationEventMulticaster();
// 9.Initialize other special beans in specific context subclasses.
onRefresh();
//10. Check for listener beans and register them.
registerListeners();
// 11.Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
//12. Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
复制代码
2.由于postProcessBeanFactory是模板方法,它会被子类AbstractRefreshableWebApplicationContext重写。在AbstractRefreshableWebApplicationContext的postProcessBeanFactory()做以下几件事情。
1.注册ServletContextAwareProcessor。 2.注册需要忽略的依赖接口ServletContextAware
、ServletConfigAware
。 3.注册Web应用的作用域和环境配置信息。
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext, this.servletConfig));
beanFactory.ignoreDependencyInterface(ServletContextAware.class);
beanFactory.ignoreDependencyInterface(ServletConfigAware.class);
WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext, this.servletConfig);
}
复制代码
- WebApplicationContextUtils中的registerWebApplicationScopes(),beanFactory注册了request,application,session,globalSession作用域,也注册了需要解决的依赖:ServletRequest、ServletResponse、HttpSession、WebRequest。
public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory, ServletContext sc) {
beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope(false));
beanFactory.registerScope(WebApplicationContext.SCOPE_GLOBAL_SESSION, new SessionScope(true));
if (sc != null) {
ServletContextScope appScope = new ServletContextScope(sc);
beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
// Register as ServletContext attribute, for ContextCleanupListener to detect it.
sc.setAttribute(ServletContextScope.class.getName(), appScope);
}
beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
if (jsfPresent) {
FacesDependencyRegistrar.registerFacesDependencies(beanFactory);
}
}
复制代码
4.RequestObjectFactory, ResponseObjectFactory, SessionObjectFactory都实现了ObjectFactory的接口,注入的值其实是getObject()的值。
/**
* Factory that exposes the current request object on demand.
*/
@SuppressWarnings("serial")
private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {
@Override
public ServletRequest getObject() {
return currentRequestAttributes().getRequest();
}
@Override
public String toString() {
return "Current HttpServletRequest";
}
}
/**
* Factory that exposes the current response object on demand.
*/
@SuppressWarnings("serial")
private static class ResponseObjectFactory implements ObjectFactory<ServletResponse>, Serializable {
@Override
public ServletResponse getObject() {
ServletResponse response = currentRequestAttributes().getResponse();
if (response == null) {
throw new IllegalStateException("Current servlet response not available - " +
"consider using RequestContextFilter instead of RequestContextListener");
}
return response;
}
@Override
public String toString() {
return "Current HttpServletResponse";
}
}
/**
* Factory that exposes the current session object on demand.
*/
@SuppressWarnings("serial")
private static class SessionObjectFactory implements ObjectFactory<HttpSession>, Serializable {
@Override
public HttpSession getObject() {
return currentRequestAttributes().getRequest().getSession();
}
@Override
public String toString() {
return "Current HttpSession";
}
}
/**
* Factory that exposes the current WebRequest object on demand.
*/
@SuppressWarnings("serial")
private static class WebRequestObjectFactory implements ObjectFactory<WebRequest>, Serializable {
@Override
public WebRequest getObject() {
ServletRequestAttributes requestAttr = currentRequestAttributes();
return new ServletWebRequest(requestAttr.getRequest(), requestAttr.getResponse());
}
@Override
public String toString() {
return "Current ServletWebRequest";
}
}
复制代码
5.很明显,我们从getObject()中获取的值是从绑定当前线程的RequestAttribute中获取的,内部实现是通过ThreadLocal去完成的。看到这里,你应该明白了一点点。
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<RequestAttributes>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<RequestAttributes>("Request context");
复制代码
private static ServletRequestAttributes currentRequestAttributes() {
RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();
if (!(requestAttr instanceof ServletRequestAttributes)) {
throw new IllegalStateException("Current request is not a servlet request");
}
return (ServletRequestAttributes) requestAttr;
}
复制代码
public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
RequestAttributes attributes = getRequestAttributes();
if (attributes == null) {
if (jsfPresent) {
attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
}
if (attributes == null) {
throw new IllegalStateException("No thread-bound request found: " +
"Are you referring to request attributes outside of an actual web request, " +
"or processing a request outside of the originally receiving thread? " +
"If you are actually operating within a web request and still receive this message, " +
"your code is probably running outside of DispatcherServlet/DispatcherPortlet: " +
"In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
}
}
return attributes;
}
复制代码
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
复制代码
6.我们再来捋一捋@Autowired注入HttpServletRequest对象的过程。这里以HttpServletRequest对象注入举例。首先调用DefaultListableBeanFactory中的findAutowireCandidates()方法,判断autowiringType类型是否和requiredType类型一致或者是autowiringType是否是requiredType的父接口(父类)。如果满足条件的话,我们会从resolvableDependencies中通过autowiringType(对应着上文的ServletRequest)拿到autowiringValue(对应着上文的RequestObjectFactory)。然后调用AutowireUtils.resolveAutowiringValue()
对我们的ObjectFactory进行处理。
protected Map<String, Object> findAutowireCandidates(
String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {
String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this, requiredType, true, descriptor.isEager());
Map<String, Object> result = new LinkedHashMap<String, Object>(candidateNames.length);
for (Class<?> autowiringType : this.resolvableDependencies.keySet()) {
if (autowiringType.isAssignableFrom(requiredType)) {
Object autowiringValue = this.resolvableDependencies.get(autowiringType);
autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
if (requiredType.isInstance(autowiringValue)) {
result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
break;
}
}
}
for (String candidate : candidateNames) {
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {
// Consider fallback matches if the first pass failed to find anything...
DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
for (String candidate : candidateNames) {
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
if (result.isEmpty()) {
// Consider self references as a final pass...
// but in the case of a dependency collection, not the very same bean itself.
for (String candidate : candidateNames) {
if (isSelfReference(beanName, candidate) &&
(!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
isAutowireCandidate(candidate, fallbackDescriptor)) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
}
}
return result;
}
复制代码
- 很明显,对我们的RequestObjectFactory进行了JDK动态代理。原来我们通过@Autowired注入拿到的HttpServletRequest对象是代理对象。
public static Object resolveAutowiringValue(Object autowiringValue, Class<?> requiredType) {
if (autowiringValue instanceof ObjectFactory && !requiredType.isInstance(autowiringValue)) {
ObjectFactory<?> factory = (ObjectFactory<?>) autowiringValue;
if (autowiringValue instanceof Serializable && requiredType.isInterface()) {
autowiringValue = Proxy.newProxyInstance(requiredType.getClassLoader(),
new Class<?>[] {requiredType}, new ObjectFactoryDelegatingInvocationHandler(factory));
}
else {
return factory.getObject();
}
}
return autowiringValue;
}
复制代码
private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {
private final ObjectFactory<?> objectFactory;
public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
this.objectFactory = objectFactory;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("equals")) {
// Only consider equal when proxies are identical.
return (proxy == args[0]);
}
else if (methodName.equals("hashCode")) {
// Use hashCode of proxy.
return System.identityHashCode(proxy);
}
else if (methodName.equals("toString")) {
return this.objectFactory.toString();
}
try {
return method.invoke(this.objectFactory.getObject(), args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
复制代码
8.我们再来看SpringMVC是怎么把HttpServletRequest对象放入到ThreadLocal中。当用户发出请求后,会经过FrameworkServlet中的processRequest()方法做了一些骚操作,然后再交给子类DispatcherServlet中的doService()去处理这个请求。这些骚操作就包括把request,response对象包装成ServletRequestAttributes对象,然后放入到ThreadLocal中。
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
doService(request, response);
}
catch (ServletException ex) {
failureCause = ex;
throw ex;
}
catch (IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
if (logger.isDebugEnabled()) {
if (failureCause != null) {
this.logger.debug("Could not complete request", failureCause);
}
else {
if (asyncManager.isConcurrentHandlingStarted()) {
logger.debug("Leaving response open for concurrent processing");
}
else {
this.logger.debug("Successfully completed request");
}
}
}
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
复制代码
- buildRequestAttributes()方法将当前request和response对象包装成ServletRequestAttributes对象。initContextHolders()负责把RequestAttributes对象放入到requestAttributesHolder(ThreadLocal)中。一切真相大白。
protected ServletRequestAttributes buildRequestAttributes(
HttpServletRequest request, HttpServletResponse response, RequestAttributes previousAttributes) {
if (previousAttributes == null || previousAttributes instanceof ServletRequestAttributes) {
return new ServletRequestAttributes(request, response);
}
else {
return null; // preserve the pre-bound RequestAttributes instance
}
}
复制代码
private void initContextHolders(
HttpServletRequest request, LocaleContext localeContext, RequestAttributes requestAttributes) {
if (localeContext != null) {
LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
}
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
if (logger.isTraceEnabled()) {
logger.trace("Bound request context to thread: " + request);
}
}
复制代码
public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}
复制代码
private static final boolean jsfPresent =
ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<RequestAttributes>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<RequestAttributes>("Request context");
复制代码
- SpringMVC会优先执行被@ModelAttribute注解的方法。也就是说我们每一次请求,都会去调用init()方法,对request,response,httpSession进行赋值操作,并发问题也由此产生。
private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container)
throws Exception {
while (!this.modelMethods.isEmpty()) {
InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();
ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class);
if (container.containsAttribute(ann.name())) {
if (!ann.binding()) {
container.setBindingDisabled(ann.name());
}
continue;
}
Object returnValue = modelMethod.invokeForRequest(request, container);
if (!modelMethod.isVoid()){
String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
if (!ann.binding()) {
container.setBindingDisabled(returnValueName);
}
if (!container.containsAttribute(returnValueName)) {
container.addAttribute(returnValueName, returnValue);
}
}
}
}
复制代码
public abstract class BaseController1 {
protected HttpServletRequest request;
protected HttpServletResponse response;
protected HttpSession httpSession;
@ModelAttribute
public void init(HttpServletRequest request,
HttpServletResponse response,
HttpSession httpSession) {
this.request = request;
this.response = response;
this.httpSession = httpSession;
}
}
复制代码
尾言
大家好,我是cmazxiaoma(寓意是沉梦昂志的小马),希望和你们一起成长进步,感谢各位阅读本文章。