SpringMVC請求分發的簡單實現

簡介


    以前用了下SpringMVC感覺挺不錯了,前段事件也簡單了寫了一些代碼來實現了SpringMVC簡單的請求分發功能,實現的主要思想如下:
  • 將處理請求的類在系統啓動的時候加載起來,相當於SpringMVC中的Controller
  • 讀取Controller中的配置並對應其處理的URL
  • 通過調度Servlet進行攔截請求,並找到相應的Controller進行處理

主要代碼


首先得標識出來哪些類是Controller類,這裏我自己定義的是ServletHandler,通過Annotation的方式進行標識,並配置每個類和方法處理的URL:
  1. package com.meet58.base.servlet.annotation;  
  2.   
  3. public @interface ServletHandler {  
  4.       
  5. }  
package com.meet58.base.servlet.annotation;

public @interface ServletHandler {
	
}

這裏註解主要是聲明這個類是一個ServletHandler類,用於處理請求的類,系統啓動的時候就會加載這些類。

  1. package com.meet58.base.servlet.annotation;  
  2.   
  3. import java.lang.annotation.Documented;  
  4. import java.lang.annotation.ElementType;  
  5. import java.lang.annotation.Retention;  
  6. import java.lang.annotation.RetentionPolicy;  
  7. import java.lang.annotation.Target;  
  8.   
  9. import com.meet58.base.servlet.types.RequestMethod;  
  10. import com.meet58.base.servlet.types.ResponseType;  
  11.   
  12. @Target({ElementType.TYPE, ElementType.METHOD})  
  13. @Retention(RetentionPolicy.RUNTIME)  
  14. @Documented  
  15. public @interface HandlerMapping {  
  16.     String value();   
  17.       
  18. }  
package com.meet58.base.servlet.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.meet58.base.servlet.types.RequestMethod;
import com.meet58.base.servlet.types.ResponseType;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HandlerMapping {
	String value();	
	
}

這個註解是配置處理請求的註解,定義了要處理的路徑。

定義了註解之後就是要在系統啓動的時候掃描並加載這些類,下面是如何進行掃描的代碼:
  1. package com.meet58.base.servlet.mapping;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. import org.springframework.core.io.Resource;  
  6. import org.springframework.core.io.support.PathMatchingResourcePatternResolver;  
  7. import org.springframework.core.io.support.ResourcePatternResolver;  
  8. import org.springframework.core.io.support.ResourcePatternUtils;  
  9. import org.springframework.core.type.classreading.CachingMetadataReaderFactory;  
  10. import org.springframework.core.type.classreading.MetadataReader;  
  11. import org.springframework.core.type.classreading.MetadataReaderFactory;  
  12. import org.springframework.core.type.filter.AnnotationTypeFilter;  
  13. import org.springframework.core.type.filter.TypeFilter;  
  14. import org.springframework.util.ClassUtils;  
  15.   
  16. import com.meet58.base.servlet.annotation.ServletHandler;  
  17.   
  18. public class ServletHandlerMappingResolver {  
  19.       
  20.     private static final String RESOURCE_PATTERN = "/**/*.class";  
  21.       
  22.     private String[] packagesToScan;  
  23.   
  24.     private ResourcePatternResolver resourcePatternResolver;   
  25.       
  26.       
  27.     private static final TypeFilter[] ENTITY_TYPE_FILTERS = new TypeFilter[] {  
  28.         new AnnotationTypeFilter(ServletHandler.classfalse)};  
  29.   
  30.       
  31.     public ServletHandlerMappingResolver(){  
  32.         this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(new PathMatchingResourcePatternResolver());  
  33.     }  
  34.       
  35.     public ServletHandlerMappingResolver scanPackages(String[] packagesToScan){  
  36.         try {  
  37.             for (String pkg : packagesToScan) {  
  38.                 String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +  
  39.                         ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN;  
  40.                 Resource[] resources;  
  41.                   
  42.                     resources = this.resourcePatternResolver.getResources(pattern);  
  43.                   
  44.                 MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);  
  45.                 for (Resource resource : resources) {  
  46.                     if (resource.isReadable()) {  
  47.                         MetadataReader reader = readerFactory.getMetadataReader(resource);  
  48.                         String className = reader.getClassMetadata().getClassName();  
  49.                         if (matchesFilter(reader, readerFactory)) {  
  50.                             ServletHandlerMappingFactory.addClassMapping(this.resourcePatternResolver.getClassLoader().loadClass(className));  
  51.                         }  
  52.                     }  
  53.                 }  
  54.             }  
  55.         } catch (IOException e) {  
  56.             // TODO Auto-generated catch block  
  57.             e.printStackTrace();  
  58.         } catch (ClassNotFoundException e) {  
  59.             // TODO Auto-generated catch block  
  60.             e.printStackTrace();  
  61.         }  
  62.         return this;  
  63.     }  
  64.       
  65.     public String[] getPackagesToScan() {  
  66.         return packagesToScan;  
  67.     }  
  68.   
  69.     public void setPackagesToScan(String[] packagesToScan) {  
  70.         this.packagesToScan = packagesToScan;  
  71.         this.scanPackages(packagesToScan);  
  72.     }  
  73.   
  74.     private boolean matchesFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {  
  75.         for (TypeFilter filter : ENTITY_TYPE_FILTERS) {  
  76.             if (filter.match(reader, readerFactory)) {  
  77.                 return true;  
  78.             }  
  79.         }  
  80.         return false;  
  81.     }  
  82.   
  83. }  
package com.meet58.base.servlet.mapping;

import java.io.IOException;

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.ClassUtils;

import com.meet58.base.servlet.annotation.ServletHandler;

public class ServletHandlerMappingResolver {
	
	private static final String RESOURCE_PATTERN = "/**/*.class";
	
	private String[] packagesToScan;

	private ResourcePatternResolver resourcePatternResolver; 
	
	
	private static final TypeFilter[] ENTITY_TYPE_FILTERS = new TypeFilter[] {
		new AnnotationTypeFilter(ServletHandler.class, false)};

	
	public ServletHandlerMappingResolver(){
		this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(new PathMatchingResourcePatternResolver());
	}
	
	public ServletHandlerMappingResolver scanPackages(String[] packagesToScan){
		try {
			for (String pkg : packagesToScan) {
				String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
						ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN;
				Resource[] resources;
				
					resources = this.resourcePatternResolver.getResources(pattern);
				
				MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(this.resourcePatternResolver);
				for (Resource resource : resources) {
					if (resource.isReadable()) {
						MetadataReader reader = readerFactory.getMetadataReader(resource);
						String className = reader.getClassMetadata().getClassName();
						if (matchesFilter(reader, readerFactory)) {
							ServletHandlerMappingFactory.addClassMapping(this.resourcePatternResolver.getClassLoader().loadClass(className));
						}
					}
				}
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return this;
	}
	
	public String[] getPackagesToScan() {
		return packagesToScan;
	}

	public void setPackagesToScan(String[] packagesToScan) {
		this.packagesToScan = packagesToScan;
		this.scanPackages(packagesToScan);
	}

	private boolean matchesFilter(MetadataReader reader, MetadataReaderFactory readerFactory) throws IOException {
		for (TypeFilter filter : ENTITY_TYPE_FILTERS) {
			if (filter.match(reader, readerFactory)) {
				return true;
			}
		}
		return false;
	}

}

這段代碼是Spring中如何掃描Hibernate持久化對象的代碼,拿過來借鑑了一下,下面要處理的就是把要處理的URL和相對應的ServletHandler進行匹配:
  1. package com.meet58.base.servlet.mapping;  
  2.   
  3. import java.lang.reflect.Method;  
  4. import java.util.HashMap;  
  5. import java.util.Map;  
  6.   
  7. import org.apache.log4j.Logger;  
  8.   
  9. import com.meet58.base.servlet.annotation.HandlerMapping;  
  10. import com.meet58.base.servlet.context.ServletHandlerFactory;  
  11.   
  12. public class ServletHandlerMappingFactory {  
  13.       
  14.     private static Logger logger = Logger.getLogger(ServletHandlerMappingFactory.class);  
  15.   
  16.     private static Map<String, Method> servletHandlerMapping = new HashMap<String, Method>();  
  17.   
  18.   
  19.     public static void addClassMapping(Class<?> clazz) {  
  20.         String url = null;  
  21.         HandlerMapping handlerMapping = clazz.getAnnotation(HandlerMapping.class);  
  22.         if (handlerMapping != null) {  
  23.             url = handlerMapping.value();  
  24.         } else {  
  25.             String classSimpleName = clazz.getSimpleName().toLowerCase();  
  26.             url = "/" + classSimpleName.substring(0,  
  27.                             classSimpleName.indexOf("servlet"));  
  28.         }  
  29.         if (url != null) {  
  30.             if(url.endsWith("/")){  
  31.                 url = url.substring(url.length() - 1);  
  32.             }  
  33.             ServletHandlerFactory.put(clazz);  
  34.             logger.info(" Load servlet handler class:" + clazz.getName() + " url:" + url);  
  35.             scanHandlerMethod(clazz,url);  
  36.         }  
  37.     }  
  38.   
  39.     public static void scanHandlerMethod(Class<?> clazz,String classMapping) {  
  40.         Method[] methods = clazz.getDeclaredMethods();  
  41.         for (Method method : methods) {  
  42.             HandlerMapping handlerMapping = method.getAnnotation(HandlerMapping.class);  
  43.             if (handlerMapping != null && handlerMapping.value() != null) {  
  44.                 String mapping = handlerMapping.value();  
  45.                 if(!mapping.startsWith("/")){  
  46.                     mapping = "/" + mapping;  
  47.                 }  
  48.                 mapping = classMapping + mapping;  
  49.                 addMethodMapping( mapping,method);  
  50.             }  
  51.         }  
  52.     }  
  53.   
  54.     public static void addMethodMapping(String url,Method method) {  
  55.         logger.info(" Load servlet handler mapping, method:" + method.getName() + " for url:" + url);  
  56.         Method handlerMethod = servletHandlerMapping.get(url);  
  57.         if(handlerMethod != null){  
  58.             throw new IllegalArgumentException(" url :" + url + " is already mapped by :" + handlerMethod);  
  59.         }else{  
  60.             servletHandlerMapping.put(url, method);  
  61.         }  
  62.     }  
  63.   
  64.     public static Method getMethodMapping(String url) {  
  65.         return servletHandlerMapping.get(url);  
  66.     }  
  67. }  
package com.meet58.base.servlet.mapping;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;

import com.meet58.base.servlet.annotation.HandlerMapping;
import com.meet58.base.servlet.context.ServletHandlerFactory;

public class ServletHandlerMappingFactory {
	
	private static Logger logger = Logger.getLogger(ServletHandlerMappingFactory.class);

	private static Map<String, Method> servletHandlerMapping = new HashMap<String, Method>();


	public static void addClassMapping(Class<?> clazz) {
		String url = null;
		HandlerMapping handlerMapping = clazz.getAnnotation(HandlerMapping.class);
		if (handlerMapping != null) {
			url = handlerMapping.value();
		} else {
			String classSimpleName = clazz.getSimpleName().toLowerCase();
			url = "/" + classSimpleName.substring(0,
							classSimpleName.indexOf("servlet"));
		}
		if (url != null) {
			if(url.endsWith("/")){
				url = url.substring(url.length() - 1);
			}
			ServletHandlerFactory.put(clazz);
			logger.info(" Load servlet handler class:" + clazz.getName() + " url:" + url);
			scanHandlerMethod(clazz,url);
		}
	}

	public static void scanHandlerMethod(Class<?> clazz,String classMapping) {
		Method[] methods = clazz.getDeclaredMethods();
		for (Method method : methods) {
			HandlerMapping handlerMapping = method.getAnnotation(HandlerMapping.class);
			if (handlerMapping != null && handlerMapping.value() != null) {
				String mapping = handlerMapping.value();
				if(!mapping.startsWith("/")){
					mapping = "/" + mapping;
				}
				mapping = classMapping + mapping;
				addMethodMapping( mapping,method);
			}
		}
	}

	public static void addMethodMapping(String url,Method method) {
		logger.info(" Load servlet handler mapping, method:" + method.getName() + " for url:" + url);
		Method handlerMethod = servletHandlerMapping.get(url);
		if(handlerMethod != null){
			throw new IllegalArgumentException(" url :" + url + " is already mapped by :" + handlerMethod);
		}else{
			servletHandlerMapping.put(url, method);
		}
	}

	public static Method getMethodMapping(String url) {
		return servletHandlerMapping.get(url);
	}
}

在這個類中掃描了每個ServletHandler類中的方法,並記錄他們的要處理的URL,接下來就是通過容器實例化這些ServletHandler類了:
  1. package com.meet58.base.servlet.context;  
  2.   
  3. import java.util.HashMap;  
  4. import java.util.Map;  
  5.   
  6. import org.apache.log4j.Logger;  
  7.   
  8. public class ServletHandlerFactory {  
  9.       
  10.     private static Logger logger = Logger.getLogger(ServletHandlerFactory.class);  
  11.   
  12.     private static Map<String,Object> classes = new HashMap<String,Object>();  
  13.       
  14.     public static void put(Class<?> clazz){  
  15.         try {  
  16.             logger.info("初始化ServletHandler類:"+ clazz.getName());  
  17.             Object servlet = clazz.newInstance();  
  18.             classes.put(clazz.getName(), servlet);  
  19.         } catch (InstantiationException e) {  
  20.             logger.error("初始化Servlet類:" + clazz.getName() + "失敗:" + e.getMessage());  
  21.         } catch (IllegalAccessException e) {  
  22.             logger.error("初始化Servlet類:" + clazz.getName() + "失敗:" + e.getMessage());  
  23.         }  
  24.     }  
  25.       
  26.     @SuppressWarnings("unchecked")  
  27.     public static <T> T get(String className){  
  28.         return (T)classes.get(className);  
  29.     }  
  30. }  
package com.meet58.base.servlet.context;

import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;

public class ServletHandlerFactory {
	
	private static Logger logger = Logger.getLogger(ServletHandlerFactory.class);

	private static Map<String,Object> classes = new HashMap<String,Object>();
	
	public static void put(Class<?> clazz){
		try {
			logger.info("初始化ServletHandler類:"+ clazz.getName());
			Object servlet = clazz.newInstance();
			classes.put(clazz.getName(), servlet);
		} catch (InstantiationException e) {
			logger.error("初始化Servlet類:" + clazz.getName() + "失敗:" + e.getMessage());
		} catch (IllegalAccessException e) {
			logger.error("初始化Servlet類:" + clazz.getName() + "失敗:" + e.getMessage());
		}
	}
	
	@SuppressWarnings("unchecked")
	public static <T> T get(String className){
		return (T)classes.get(className);
	}
}

在ServletHandler類處理完成,並知道他們分別處理哪些URL之後,就可以通過一個調度器進行對對應的URL進行請求的分發了:
  1. package com.meet58.base.servlet;  
  2.   
  3. import java.io.IOException;  
  4. import java.lang.reflect.InvocationTargetException;  
  5. import java.lang.reflect.Method;  
  6. import java.util.ArrayList;  
  7. import java.util.List;  
  8.   
  9. import javax.servlet.ServletException;  
  10. import javax.servlet.annotation.WebServlet;  
  11. import javax.servlet.http.HttpServlet;  
  12. import javax.servlet.http.HttpServletRequest;  
  13. import javax.servlet.http.HttpServletResponse;  
  14.   
  15. import org.apache.log4j.Logger;  
  16.   
  17. import com.meet58.base.context.WebHttpRequestContext;  
  18. import com.meet58.base.servlet.context.ServletHandlerFactory;  
  19. import com.meet58.base.servlet.mapping.ServletHandlerMappingFactory;  
  20. import com.meet58.util.WebUtils;  
  21.   
  22. @WebServlet(urlPatterns = { "*.do" })  
  23. public class WebHttpDispatchServlet extends HttpServlet {  
  24.   
  25.     private static final long serialVersionUID = 1L;  
  26.   
  27.     private Logger logger = Logger.getLogger(this.getClass());  
  28.   
  29.     private List<String> excludeUrls = new ArrayList<String>();  
  30.   
  31.     @Override  
  32.     public void init() throws ServletException {  
  33.         // 屏蔽websocket地址  
  34.         excludeUrls.add("/meet.do");  
  35.         super.init();  
  36.     }  
  37.   
  38.     public void doGet(HttpServletRequest request, HttpServletResponse response)  
  39.             throws ServletException, IOException {  
  40.         this.doPost(request, response);  
  41.     }  
  42.   
  43.     public void doPost(HttpServletRequest request, HttpServletResponse response)  
  44.             throws ServletException, IOException {  
  45.         try {  
  46.             String url = request.getRequestURI().replace(  
  47.                     request.getContextPath(), "");  
  48.             if (excludeUrls.contains(url)) {  
  49.                 return;  
  50.             }  
  51.             Method handlerMethod = ServletHandlerMappingFactory.getMethodMapping(url);  
  52.             if (handlerMethod == null) {  
  53.                 response.sendError(404"No handler found for " + url);  
  54.                 logger.error("No handler found for " + url);  
  55.                 return;  
  56.             }  
  57.   
  58.             Object servlet = ServletHandlerFactory.get(handlerMethod  
  59.                     .getDeclaringClass().getName());  
  60.   
  61.             if (servlet == null) {  
  62.                 response.sendError(404"No handler class found for " + url);  
  63.                 logger.error("No handler class found for " + url);  
  64.                 return;  
  65.             }  
  66.   
  67.             Object result = invokeHandlerMethod(servlet, handlerMethod);  
  68.             handleInvokeResult(result);  
  69.   
  70.             // this.doService();  
  71.         } catch (Throwable e) {  
  72.             handlerException(e);  
  73.         }  
  74.     }  
  75.   
  76.     public void handleInvokeResult(Object result) {  
  77.         String location = "";  
  78.         if (result instanceof String) {  
  79.             if (((String) result).startsWith("redirect:")) {  
  80.                 location = ((String) result).substring("redirect:".length(),  
  81.                         ((String) result).length());  
  82.                 WebUtils.redirect(location);  
  83.             } else if (((String) result).startsWith("forward:")) {  
  84.                 location = ((String) result).substring("forward:".length(),  
  85.                         ((String) result).length());  
  86.                 WebUtils.forward(location);  
  87.             }  
  88.         }  
  89.     }  
  90.   
  91.     public Object invokeHandlerMethod(Object object, Method method)  
  92.             throws Throwable {  
  93.         Object result = null;  
  94.         if (method != null) {  
  95.             try {  
  96.                 result = method.invoke(object);  
  97.             } catch (InvocationTargetException e) {  
  98.                 throw e.getTargetException();  
  99.             }  
  100.         }  
  101.         return result;  
  102.     }  
  103.   
  104.     public void handlerException(Throwable e) {  
  105.         String message = e.getMessage() != null ? e.getMessage() : e.toString();  
  106.         e.printStackTrace();  
  107.         if (WebHttpRequestContext.isAsyncRequest()) {  
  108.             WebUtils.writeFailure(message);  
  109.         } else {  
  110.             try {  
  111.                 WebHttpRequestContext.getResponse().sendError(500, message);  
  112.             } catch (IOException e1) {  
  113.                 e1.printStackTrace();  
  114.             }  
  115.         }  
  116.     }  
  117.   
  118.     public String getMappingClass(String url) {  
  119.         return null;  
  120.     }  
  121.   
  122. }  
package com.meet58.base.servlet;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;

import com.meet58.base.context.WebHttpRequestContext;
import com.meet58.base.servlet.context.ServletHandlerFactory;
import com.meet58.base.servlet.mapping.ServletHandlerMappingFactory;
import com.meet58.util.WebUtils;

@WebServlet(urlPatterns = { "*.do" })
public class WebHttpDispatchServlet extends HttpServlet {

	private static final long serialVersionUID = 1L;

	private Logger logger = Logger.getLogger(this.getClass());

	private List<String> excludeUrls = new ArrayList<String>();

	@Override
	public void init() throws ServletException {
		// 屏蔽websocket地址
		excludeUrls.add("/meet.do");
		super.init();
	}

	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		this.doPost(request, response);
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		try {
			String url = request.getRequestURI().replace(
					request.getContextPath(), "");
			if (excludeUrls.contains(url)) {
				return;
			}
			Method handlerMethod = ServletHandlerMappingFactory.getMethodMapping(url);
			if (handlerMethod == null) {
				response.sendError(404, "No handler found for " + url);
				logger.error("No handler found for " + url);
				return;
			}

			Object servlet = ServletHandlerFactory.get(handlerMethod
					.getDeclaringClass().getName());

			if (servlet == null) {
				response.sendError(404, "No handler class found for " + url);
				logger.error("No handler class found for " + url);
				return;
			}

			Object result = invokeHandlerMethod(servlet, handlerMethod);
			handleInvokeResult(result);

			// this.doService();
		} catch (Throwable e) {
			handlerException(e);
		}
	}

	public void handleInvokeResult(Object result) {
		String location = "";
		if (result instanceof String) {
			if (((String) result).startsWith("redirect:")) {
				location = ((String) result).substring("redirect:".length(),
						((String) result).length());
				WebUtils.redirect(location);
			} else if (((String) result).startsWith("forward:")) {
				location = ((String) result).substring("forward:".length(),
						((String) result).length());
				WebUtils.forward(location);
			}
		}
	}

	public Object invokeHandlerMethod(Object object, Method method)
			throws Throwable {
		Object result = null;
		if (method != null) {
			try {
				result = method.invoke(object);
			} catch (InvocationTargetException e) {
				throw e.getTargetException();
			}
		}
		return result;
	}

	public void handlerException(Throwable e) {
		String message = e.getMessage() != null ? e.getMessage() : e.toString();
		e.printStackTrace();
		if (WebHttpRequestContext.isAsyncRequest()) {
			WebUtils.writeFailure(message);
		} else {
			try {
				WebHttpRequestContext.getResponse().sendError(500, message);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
	}

	public String getMappingClass(String url) {
		return null;
	}

}

這段代碼中就是通過URL找到對應的處理方法來進行處理,並且捕獲異常。

這種方法Struts也是用到了,不過這個只是簡單的興趣研究並沒有在實際項目中運用,可能會存在線程安全的問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章