面url訪問到后臺程序是從一個url開始的,怎么根據url找到對應的處理程序,這個就要從RequestMapping原理理解。
SpringMVC 啟動的時候,會加載 加了注解 @Controller 的 Bean.
栗子:
@Controller
@RequestMapping("/test")
public class TestController {
@RequestMapping(value = {"/show"})
public String testTest() {
return "/jsp/index";
}
}
@Controller和@RequestMapping結合起來完成了Spring MVC請求的派發流程。
簡述:new 出來的,帶有父類 AbstractHandlerMethodMapping 屬性 mappingRegistry , mappingRegistry key value 中的 value 指的是 帶有 requestMapping 注解的方法 。
如果想要 RequestMapping 注解生效,必須在 xml 文件中配置,< mvc:annotation-driven/>。
配置完 xml 之后 ,下一步解析 bean。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
//如果該元素屬于默認命名空間走此邏輯。Spring的默認namespace為:http://www.springframework.org/schema/beans“
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
//對document中的每個元素都判斷其所屬命名空間,然后走相應的解析邏輯
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
//如果該元素屬于自定義namespace走此邏輯 ,比如AOP,MVC等。
delegate.parseCustomElement(root);
}
}
beans 等默認命名空間執行 parseDefaultElement() 方法,其他命名空間執行 parseCustomElement() 方法.
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}
進入parseCustomElement(ele, null)方法。
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
//獲取該元素namespace url
String namespaceUri = getNamespaceURI(ele);
//得到NamespaceHandlerSupport實現類解析元素
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
進入NamespaceHandlerSupport類的parse()方法。
public BeanDefinition parse(Element element, ParserContext parserContext) {
//此處得到AnnotationDrivenBeanDefinitionParser類來解析該元素
return findParserForElement(element, parserContext).parse(element, parserContext);
}
獲取解析類
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
String localName = parserContext.getDelegate().getLocalName(element);
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
解析< mvc:annotation-driven/>元素 進入AnnotationDrivenBeanDefinitionParser類的parse()方法。
注解解析器處理
public BeanDefinition parse(Element element, ParserContext parserContext) {
Object source = parserContext.extractSource(element);
XmlReaderContext readerContext = parserContext.getReaderContext();
CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source);
parserContext.pushContainingComponent(compDefinition);
RuntimeBeanReference contentNegotiationManager = getContentNegotiationManager(element, source, parserContext);
//生成RequestMappingHandlerMapping bean信息
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerMappingDef.getPropertyValues().add("order", 0);
handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
......
//此處HANDLER_MAPPING_BEAN_NAME值為:RequestMappingHandlerMapping類名
//容器中注冊name為RequestMappingHandlerMapping類名
parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME));
......
}
Spring 容器中注冊了一個名為 “HANDLER_MAPPING_BEAN_NAME”,類型為 RequestMappingHandlerMapping.
RequestMappingHandlerMapping 實例化
RequestMappingHandlerMapping 繼承圖
RequestMappingHandlerMapping實現了 HandlerMapping 接口,同時還實現了 ApplicationContectAware 和 IntialingBean 接口。
這個接口只包含以下方法:
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
如果一個類實現了 ApplicationContextAware 接口,Spring容器在初始化該類時候會自動回調該類的setApplicationContext()方法。這個接口主要用來讓實現類得到Spring 容器上下文信息。
這個接口包含下面方法:
void afterPropertiesSet() throws Exception;
如果一個bean實現了該接口,Spring 容器初始化bean時會回調afterPropertiesSet()方法。這個接口的主要作用是讓bean在初始化時可以實現一些自定義的操作。
RequestMappingHandlerMapping 實現了 ApplicationContextAware 接口 , 那么實例化后會執行 setApplicationContext 方法
@Override
public final void setApplicationContext(@Nullable ApplicationContext context) throws BeansException {
if (context == null && !isContextRequired()) {
// Reset internal context state.
this.applicationContext = null;
this.messageSourceAccessor = null;
}
else if (this.applicationContext == null) {
// Initialize with passed-in context.
if (!requiredContextClass().isInstance(context)) {
throw new ApplicationContextException(
"Invalid application context: needs to be of type [" + requiredContextClass().getName() + "]");
}
this.applicationContext = context;
this.messageSourceAccessor = new MessageSourceAccessor(context);
initApplicationContext(context);
}
else {
// Ignore reinitialization if same context passed in.
if (this.applicationContext != context) {
throw new ApplicationContextException(
"Cannot reinitialize with different application context: current one is [" +
this.applicationContext + "], passed-in one is [" + context + "]");
}
}
}
這個方法把容器上下文賦值給 applicationContext 變量,賦值的就是 spring mvc 容器。
RequestMappingHandlerMapping 實現了 InitializingBean 接口。設置完屬性后回調 afterpropertiesSet 方法.
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setTrailingSlashMatch(useTrailingSlashMatch());
this.config.setContentNegotiationManager(getContentNegotiationManager());
if (getPatternParser() != null) {
this.config.setPatternParser(getPatternParser());
Assert.isTrue(!this.useSuffixPatternMatch && !this.useRegisteredSuffixPatternMatch,
"Suffix pattern matching not supported with PathPatternParser.");
}
else {
this.config.setSuffixPatternMatch(useSuffixPatternMatch());
this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
this.config.setPathMatcher(getPathMatcher());
}
super.afterPropertiesSet();
}
同時還調用了父類的 afterPropertiesSet 方法
public void afterPropertiesSet() {
//初始化handler函數
initHandlerMethods();
}
繼續查看 RequestMappingHandlerMapping.initHandlerMethods 方法
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " + getApplicationContext());
}
//1.獲取容器中所有bean 的name。
//根據detectHandlerMethodsInAncestorContexts bool變量的值判斷是否獲取父容器中的bean,默認為false。因此這里只獲取Spring MVC容器中的bean,不去查找父容器
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
//循環遍歷bean
for (String beanName : beanNames) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
Class<?> beanType = null;
try {
beanType = getApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
}
}
//2.判斷bean是否含有@Controller或者@RequestMappin注解
if (beanType != null && isHandler(beanType)) {
//3.對含有注解的bean進行處理,獲取handler函數信息。
detectHandlerMethods(beanName);
}
}
}
handlerMethodsInitialized(getHandlerMethods());
}
可以看出 RequestMappingHandlerMapping.initHandlerMethods 方法進行了如下操作:
查看 RequestMappingHandlerMapping.isHandle 處理邏輯
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
看對含義 Controller 和 RequestMapping 的怎么處理
查看 RequestMappingHandlerMapping.detectHandlerMethods 方法
protected void detectHandlerMethods(final Object handler) {
//1.獲取bean的類信息
Class<?> handlerType = (handler instanceof String ?
getApplicationContext().getType((String) handler) : handler.getClass());
final Class<?> userType = ClassUtils.getUserClass(handlerType);
//2.遍歷函數獲取有@RequestMapping注解的函數信息
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
new MethodIntrospector.MetadataLookup<T>() {
@Override
public T inspect(Method method) {
try {
//如果有@RequestMapping注解,則獲取函數映射信息
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
}
});
if (logger.isDebugEnabled()) {
logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
}
//3.遍歷映射函數列表,注冊handler
for (Map.Entry<Method, T> entry : methods.entrySet()) {
Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
T mapping = entry.getValue();
//注冊handler函數
registerHandlerMethod(handler, invocableMethod, mapping);
}
};
detectHandlerMethods 主要功能就是獲取該bean和父接口中所有用@RequestMapping注解的函數信息,并把這些保存到methodMap變量中。
查看 RequestMappingHandlerMapping.selectMethods 方法實現邏輯
public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
final Map<Method, T> methodMap = new LinkedHashMap<Method, T>();
Set<Class<?>> handlerTypes = new LinkedHashSet<Class<?>>();
Class<?> specificHandlerType = null;
//把自身類添加到handlerTypes中
if (!Proxy.isProxyClass(targetType)) {
handlerTypes.add(targetType);
specificHandlerType = targetType;
}
//獲取該bean所有的接口,并添加到handlerTypes中
handlerTypes.addAll(Arrays.asList(targetType.getInterfaces()));
/對自己及所有實現接口類進行遍歷
for (Class<?> currentHandlerType : handlerTypes) {
final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
//獲取函數映射信息
ReflectionUtils.doWithMethods(currentHandlerType, new ReflectionUtils.MethodCallback() {
//循環獲取類中的每個函數,通過回調處理
@Override
public void doWith(Method method) {
//對類中的每個函數進行處理
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
//回調inspect()方法給個函數生成RequestMappingInfo
T result = metadataLookup.inspect(specificMethod);
if (result != null) {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
//將生成的RequestMappingInfo保存到methodMap中
methodMap.put(specificMethod, result);
}
}
}
}, ReflectionUtils.USER_DECLARED_METHODS);
}
//返回保存函數映射信息后的methodMap
return methodMap;
}
上面邏輯中doWith()回調了inspect(),inspect()又回調了getMappingForMethod()方法。
我們看看getMappingForMethod()是如何生成函數信息的。
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
//創建函數信息
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
}
return info;
}
查看RequestMappingHandlerMapping#createRequestMappingInfo()方法。
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
//如果該函數含有@RequestMapping注解,則根據其注解信息生成RequestMapping實例,
//如果該函數沒有@RequestMapping注解則返回空
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
//如果requestMapping不為空,則生成函數信息MAP后返回
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
看看RequestMappingHandlerMapping#createRequestMappingInfo是如何實現的。
protected RequestMappingInfo createRequestMappingInfo(
RequestMapping requestMapping, RequestCondition<?> customCondition) {
return RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name())
.customCondition(customCondition)
.options(this.config)
.build();
可以看到上面把RequestMapping注解中的信息都放到一個RequestMappingInfo實例中后返回。當生成含有@RequestMapping注解的函數映射信息后,最后一步是調用registerHandlerMethod 注冊handler和處理函數映射關系。
protectedvoid registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
看到把所有的handler方法都注冊到了mappingRegistry這個變量中。
到此就把RequestMappingHandlerMapping bean的實例化流程就分析完了。
Spring MVC容器初始化流程,查看在 FrameworkServlet#initWebApplicationContext 方法。
protected WebApplicationContext initWebApplicationContext() {
//1.獲得rootWebApplicationContext
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
......
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
//2.創建 Spring 容器
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
//3.初始化容器
onRefresh(wac);
}
......
return wac;
}
查看 DispatchServlet#onRefresh 方法
@Override
protected void onRefresh(ApplicationContext context) {
//執行初始化策略
initStrategies(context);
}
查看 initStrategies 方法
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
查看 initHandlerMappings 方法
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
//容器中查找HandlerMapping的實例
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
//把找到的bean放到hanlderMappings中。
this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isDebugEnabled()) {
logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
}
}
}
最終是 實例化的 RequestMappingHandlerMapping 放到了一個 HandlerMapping 中。
DispatchServlet 繼承自Servlet,那所有的請求都會在service()方法中進行處理。
查看service()方法。
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//獲取請求方法
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
//若是patch請求執行此邏輯
if (HttpMethod.PATCH == httpMethod || httpMethod == null) {
processRequest(request, response);
}
else {
//其它請求走此邏輯
super.service(request, response);
}
}
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
" processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
}
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<String, Object>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith("org.springframework.web.servlet")) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
try {
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
得到不管get、post最后都會執行到DispatcherServlet#doDispatch(request, response);
查看 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);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
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()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
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);
}
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
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
查看 mappedHandler = getHandler(processedRequest); 方法
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//獲取HandlerMapping實例
for (HandlerMapping hm : this.handlerMappings) {
if (logger.isTraceEnabled()) {
logger.trace(
"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
}
//得到處理請求的handler
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
return null;
}
上面遍歷handlerMappings獲得所有HandlerMapping實例,還記得handlerMappings變量吧,這就是前面initHandlerMappings()方法中設置進去的值,其實就是為了找到 RequestMappingHandlerMapping。
@Override
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
......
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
......
return executionChain;
}
進入 AbstractHandlerMethodMapping#getHandlerInternal 方法
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
//獲取函數url
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
......
try {
//查找HandlerMethod
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
......
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
進入 lookupHandlerMethod 方法
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
......
}
最后從 mappingRegistry 找到對應的處理方法 這個 mappingRegistry 就是來自 實例化后的 RequestMappingHandlerMapping 。
在初始化容器時,會 new 一個RequestMappingHandlerMapping,其父類是AbstractHandlerMethodMapping 。并把這個RequestMappingHandlerMapping 放到一個 HandlerMapping 中。
實例化后帶有屬性 mappingRegistry , mappingRegistry 的 value 指的是帶有 requestMapping 注解的方法。
在服務請求時,遍歷 HandlerMapping ,并根據請求信息,從RequestMappingHandlerMapping 中找到對應的處理方法handler
歡迎關注公眾號:程序員開發者社區
整項目地址:vue-element-admin
https://github.com/PanJiaChen/vue-element-admin
做這個 vueAdmin-template 的主要原因是: vue-element-admin 這個項目的初衷是一個 vue 的管理后臺集成方案,把平時用到的一些組件或者經驗分享給大家,同時它也在不斷的維護和拓展中,比如最近重構了dashboard,加入了全屏功能,新增了 tabs-view 等等。所以項目會越來越復雜,不太適合很多初用 vue 的同學來構建后臺。所以就寫了這個基礎模板,它沒有復雜的功能,只包含了一個后臺需要最基礎的東西。 vueAdmin-template 主要是基于vue-cli webpack模板為基礎開發的,引入了如下dependencies:
該項目只做了一個管理后臺需要極簡的功能,封裝了axios請求,支持無限層級路由,動態權限和動態側邊欄。 如果需要更多復雜的功能可以參考 vue-element-admin,若還有不足,歡迎提issue或者pr。下文會簡單說一下用該模板需要注意的地方。
路由懶加載應該是寫大一點的項目都會用的一個功能,只有在使用這個component的時候才會加載這個相應的組件,這樣寫大大減少了初始頁面 js 的大小并且能更好的利用瀏覽器的緩存。
const Foo = resolve => require(['./Foo.vue'], resolve)
//或者
const Foo = () => import('./Foo');
復制代碼
在懶加載頁面不多的情況下一切是那么的美好,但我司后臺業務在不斷地迭代,現在項目近百個路由,這時候使用路由懶加載在開發模式下就是一件痛苦的事情了,隨手改一行代碼熱更新都是要6000ms+的,這怎么能忍。樓主整整花了一天多的時間找原因,能webpack優化的方法都用了,什么 dll, HappyPack 等方法都是過了,但提升的效果都不是很明顯,正好那段時間出了 webpack3 樓主也升級了,編譯速度也得到了很大幅度的提升,不過也要2000ms+。后來經過大神 @jzlxiaohei 的指點發現原來是路由懶加載搞得鬼,樓主猜測可能是異步加載導致 webpack 每次的 cache 失效了,所以每次的rebuild 才會這么的慢。找到了原因我們就可以對癥下藥了,我們就自己封裝了一個_import()的方法,只有在正式環境下才使用懶加載。這樣解決了困擾多事的rebuild慢問題。代碼
const _import = require('./_import_' + process.env.NODE_ENV);
const Foo = _import('Foo');
復制代碼
整整比原來6000ms快了十多倍,我終于又能愉快的開發了。
在手摸手,帶你用vue擼后臺 系列二(登錄權限篇)這章中其實已經詳細介紹過了。該項目中權限的實現方式是:通過獲取當前用戶的權限去比對路由表,生成當前用戶具的權限可訪問的路由表,通過router.addRoutes動態掛載到router上。 但其實很多公司的業務邏輯可能不是這樣的,舉一個例子來說,很多公司的需求是每個頁面的權限是動態配置的,不像本項目中是寫死預設的。但其實原理是相同的。如這個例子,你可以在后臺通過一個tree控件或者其它展現形式給每一個頁面動態配置權限,之后將這份路由表存儲到后端。當用戶登錄后根據role,后端返回一個相應的路由表或者前端去請求之前存儲的路由表動態生成可訪問頁面,之后就是router.addRoutes動態掛載到router上,你會發現原來是相同的,萬變不離其宗。
側邊欄:本項目里的側邊欄是根據 router.js 配置的路由并且根據權限動態生成的,這樣就省去了寫一遍路由還要再手動寫側邊欄這種麻煩事,同是使用了遞歸組件,這樣不管你路由多少級嵌套,都能愉快的顯示了。權限驗證那里也做了遞歸的處理。
面包屑:本項目中也封裝了一個面包屑導航,它也是通過watch $route動態生成的。代碼
由于側邊欄導航和面包屑亦或是權限,你會發現其實都是和router密切相關的,所以基于vue-router路由信息對象上做了一下小小的拓展,自定義了一些屬性
icon : the icon show in the sidebar
element-ui自帶的圖標不是很豐富,但管理后臺圖標的定制性又很強。這里只給大家推薦使用阿里的 iconfont ,簡單好用又方便管理。本項目中已經嵌入了一些 iconfont 作為例子,大家可以自行替換。 這里來簡單介紹一下 iconfont 的使用方式。首先注冊好 iconfont 賬號之后,可以在我的項目中管理自己的 iconfont 。我司所有的項目都是用這個管理的,真心推薦使用。
創建好圖標庫后如果有更新替換也很方便,這里我使用了 Symbol 的方式引入,這里還有unicode,font-class的引入方式,有興趣的可以自行研究。 之后我們點擊下載 Symbol,會發現有如下這些文件,我們只要關心iconfont.js就可以了
我們將它替換項目中的 iconfont.js 就可以了。本項目中也封裝了一個svg component 方便大家使用。
<icon-svg icon-class="填入你需要的iconfont名字就能使用了"></icon-svg>
復制代碼
每個項目都需要有一個屬于自己的favicon。
其實實現起來非常的方便,我們主需要借助html-webpack-plugin
//webpack config
function resolveApp(relativePath) {
return path.resolve(relativePath);
}
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
favicon: resolveApp('favicon.ico')
}),
復制代碼
你只要將本項目跟目錄下的favicon.ico文件替換為你想要的圖標即可。
vue cli 默認提供了standard和airbnb 兩種 lint 規范,說真的一個j檢查校驗的太松一個又太緊,而且每個團隊的 lint 規范又是不同的,所以樓主干脆在項目里把大部分常用的 lint 規范都列舉了出來并寫上了注釋方便大家修改代碼地址,大家也可以把自己的規范上傳到npm,像 vue 一樣 vue-eslint-config。配置 eslint 對多人協作的項目有很大的好處,同時配置好lint 在加 ide 的 lint 插件寫代碼簡直要起飛。相關配置可見第一篇教程。
相信大部分 vue 的項目都是基于 vue-cli 來開發的,不過畢竟每個人需求都是不太一樣的,需要自定義一些的東西。就比如拿 postcss 來說 vue-cli 有一個小坑,它默認 autoprefixer 只會對通過 vue-loader 引入的樣式有作用,換而言之也就是 .vue 文件里面的 css autoprefixer 才會效果。相關問題issues/544,issues/600。解決方案也很簡單粗暴
//app.vue
<style lang="scss">
@import './styles/index.scss'; // 全局自定義的css樣式
</style>
復制代碼
你在 .vue 文件中引入你要的樣式就可以了,或者你可以改變 vue-cli的文件在 css-loader 前面在加一個 postcss-loader,在前面的issue地址中已經給出了解決方案。 這里再來說一下 postcss 的配置問題,新版的vue-cli webpack 模板 inti 之后跟目錄下默認有一個.postcssrc.js 。vue-loader 的 postcss 會默認讀取這個文件的里的配置項,所以在這里直接改配置文件就可以了。配置和postcss是一樣的。
//.postcssrc.js
module.exports = {
"plugins": {
// to edit target browsers: use "browserlist" field in package.json
"autoprefixer": {}
}
}
//package.json
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
復制代碼
如上代碼所述,autoprefixe r回去讀取 package.json 下 browserslist的配置文件
本項目暫時沒有兼容性需求,如有兼容性需求可自行使用babel-polyfill。 在Node/Browserify/webpack中使用
npm install --save babel-polyfill //下載依賴
復制代碼
在入口文件中引入
import 'babel-polyfill';
// 或者
require('babel-polyfill');//es6
復制代碼
在webpack.config.js中加入babel-polyfill到你的入口數組:
module.exports = {
entry:["babel-polyfill","./app/js"]
}
復制代碼
具體可參考 link
或者更簡單暴力 polyfill.io 使用它給的一個 cdn 地址,引入這段js之后它會自動判斷游覽器,加載缺少的那部分 polyfill,但國內速度肯能不行,大家可以自己搭 cdn。
樓主 vue 群里的小伙伴們問的最多的問題還是關于跨域的,其實跨域問題真的不是一個很難解決的問題。這里我來簡單總結一下我推薦的幾種跨域解決方案。
開發環境 生成環境 cors cors proxy nginx
這里我只推薦這兩種方式跨域,其它的跨域方式都很多,但真心主流的也就這兩種方式。
vue-element-admin 由于是一個純前端個人項目,所以所以的數據都是用mockjs生成的,它的原理是:攔截了所有的請求并代理到本地模擬數據,所以 network 中沒有任何的請求發出。不過這并不符合實際業務開發中的場景,所以這個項目中使用了前不久剛出的 easy-mock,支持跨域,mockjs 的語法,支持Swagger 這幾點還是挺不錯的。相關文章
線上或者測試環境接口的 base_url 不一樣是很長見得需求,或者你在本地用了如 easy-mock 這種模擬數據到線上環境你想用自己公司生產環境的數據,這些需求都可以簡單的通過用 baseurl 來解決。首先我們在config/下有dev.env.js和prod.env.js這兩個配置文件。用它來區分不同環境的配置參數。
//dev.env.js
module.exports = {
NODE_ENV: '"development"',
BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
}
//prod.env.js
module.exports = {
NODE_ENV: '"production"',
BASE_API: '"https://prod-xxx"',
}
復制代碼
同時本項目封裝了axios攔截器,方便大家使用,大家也可根據自己的業務自行修改。
import axios from 'axios';
import { Message } from 'element-ui';
import store from '../store';
// 創建axios實例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url 讀取config配置文件
timeout: 5000 // 請求超時時間
});
// request攔截器
service.interceptors.request.use(config => {
if (store.getters.token) {
config.headers['X-Token'] = store.getters.token; // 讓每個請求攜帶自定義token 請根據實際情況自行修改
}
return config;
}, error => {
// Do something with request error
console.log(error); // for debug
Promise.reject(error);
})
// respone攔截器
service.interceptors.response.use(
response => {
/**
* code為非20000是拋錯 可結合自己業務進行修改
*/
const res = response.data;
if (res.code !== 20000) {
Message({
message: res.data,
type: 'error',
duration: 5 * 1000
});
// 50008:非法的token; 50012:其他客戶端登錄了; 50014:Token 過期了;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm('你已被登出,可以取消繼續留在該頁面,或者重新登錄', '確定登出', {
confirmButtonText: '重新登錄',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('FedLogOut').then(() => {
location.reload();// 為了重新實例化vue-router對象 避免bug
});
})
}
return Promise.reject(error);
} else {
return response.data;
}
},
error => {
console.log('err' + error);// for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
});
return Promise.reject(error);
}
)
export default service;
復制代碼
由于axios每一個都是一個實例,你的請求都是基于這個實例來的,所以所以配置的參數屬性都繼承了下來.
//api.xxx.js
import fetch from '@/utils/fetch';
export function getInfo(token) {
return fetch({
url: '/user/info',
method: 'get',
params: { token }
});
}
//你可以直接這樣使用,之前攔截器寫的東西都是生效的,
//它自動會有一個你之前配置的baseURL,
//但你說我這個請求baseURL和其它的不同,
//這也是很方便的,你可以字請求內部修改,
//它會自動覆蓋你在創建實例時候寫的參數如
export function getInfo(token) {
return fetch({
baseURL: https://api2-xxxx.com
url: '/user/info',
method: 'get',
params: { token }
});
}
復制代碼
這篇文章主要是介紹了 vueAdmin 做了哪些事情,希望大家如果有后臺新項目要開發,建議基于 vue-admin-template 來開發,而 vue-element-admin 更多的是用來當做一個集成方案,你要什么功能就去里面找拿來用,因為兩者的基礎架構是一樣的,所以復用成本也很低。
NET (C#) 中,發送 HTTP GET 和 POST 請求可以通過多種方式實現,主要依賴于 .NET Framework 或 .NET Core/5+ 的版本。.NET中提供了多種方法來發送HTTP請求,每種方法都有其優缺點。選擇哪種方法取決于具體需求和環境。
HttpClient 是 .NET 中處理 HTTP 請求的現代方法。它是線程安全的,支持異步操作,并且可以用于多個請求。
適用平臺:.NET Framework 4.5+, .NET Standard 1.1+, .NET Core 1.0+
其它平臺的移植版本可以通過Nuget來安裝。(Nuget使用方法:VS(Visual Studio)中Nuget的使用-CJavaPy)
命名空間:using System.Net.Http;
HttpClient推薦使用單一實例共享使用,發關請求的方法推薦使用異步的方式,代碼如下,
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
namespace ConsoleApplication
{
class Program
{
private static readonly HttpClient client = new HttpClient();
static void Main(string[] args)
{
//發送Get請求
var responseString = await client.GetStringAsync("http://www.example.com/recepticle.aspx");
//發送Post請求
var values = new Dictionary
{
{ "thing1", "hello" },
{ "thing2", "world" }
};
var content = new FormUrlEncodedContent(values);
var response = await client.PostAsync("http://www.example.com/recepticle.aspx", content);
var responseString = await response.Content.ReadAsStringAsync();
}
}
}
2、使用第三方類庫
除了上述方法,還有許多第三方庫可以用于發送HTTP請求,例如 RestSharp 和 Flurl。這些庫通常提供更高級的功能,例如支持多種身份驗證方法和自動重試機制。
1)Flurl.Http(可以通過Nuget來安裝)
Flurl.Http 是一個在 .NET 環境下使用的流行的 HTTP 客戶端庫。它提供了一個簡潔的 API 來創建 HTTP 請求,并支持異步操作。Flurl.Http 使得發送 HTTP 請求、接收響應、處理異常和解析數據變得非常簡單。
命名空間:using Flurl.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Flurl.Http;
namespace ConsoleApplication
{
class Program
{
public static async Task Main(string[] args)
{
// 示例URL
string getUrl = "https://example.com";
string postUrl = "https://example.com/post";
// 發送GET請求
string htmlContent = await GetHtmlAsync(getUrl);
Console.WriteLine("GET請求結果:");
Console.WriteLine(htmlContent);
// 發送POST請求
var postData = new { Name = "John", Age = 30 };
var postResponse = await PostJsonAsync<object>(postUrl, postData);
Console.WriteLine("POST請求結果:");
Console.WriteLine(postResponse);
// 發送帶有查詢參數和頭信息的GET請求
string htmlContentWithHeaders = await GetHtmlWithHeadersAsync(getUrl);
Console.WriteLine("帶查詢參數和頭信息的GET請求結果:");
Console.WriteLine(htmlContentWithHeaders);
// 安全地發送GET請求
string safeHtmlContent = await SafeRequestAsync(getUrl);
Console.WriteLine("安全GET請求結果:");
Console.WriteLine(safeHtmlContent);
}
public static async Task<string> GetHtmlAsync(string url)
{
return await url.GetStringAsync();
}
public static async Task<TResponse> PostJsonAsync<TResponse >(string url, object data)
{
return await url
.PostJsonAsync(data)
.ReceiveJson<TResponse>();
}
public static async Task<string> GetHtmlWithHeadersAsync(string url)
{
return await url
.SetQueryParams(new { param1 = "value1", param2 = "value2" })
.WithHeader("Accept", "application/json")
.GetStringAsync();
}
public static async Task<string> SafeRequestAsync(string url)
{
try
{
return await url.GetStringAsync();
}
catch (FlurlHttpException ex)
{
return "HTTP Error: " + ex.Message;
}
catch (Exception ex)
{
return "General Error: " + ex.Message;
}
}
}
}
2)RestSharp(可以通過Nuget來安裝)
RestSharp 是一個用于在 .NET 中發送 HTTP 請求的簡單 REST 和 HTTP API 客戶端。它可以用于構建和發送各種 HTTP 請求,并處理響應。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RestSharp;
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
//發送Get和Post請求
RestClient client = new RestClient("http://example.com");//指定請求的url
RestRequest req = new RestRequest("resource/{id}", Method.GET);//指定請求的方式,如果Post則改成Method.POST
request.AddParameter("name", "value"); // 添加參數到 URL querystring
request.AddUrlSegment("id", "123"); //替換上面指定請求方式中的{id}參數
//req.AddBody(body); /*如發送post請求,則用req.AddBody ()指定body內容*/
//發送請求得到請求的內容
//如果有header可以使用下面方法添加
//request.AddHeader("header", "value");
IRestResponse response = client.Execute(request);
//上傳一個文件
//request.AddFile("file", path);
var content = response.Content; // 未處理的content是string
//還可以下面幾種方式發送請求
//發送請求,反序列化請求結果
IRestResponse response2 = client.Execute(request);
var name = response2.Data.Name;
//發送請求下載一個文件,并保存到path路徑
client.DownloadData(request).SaveAs(path);
// 簡單發送異步請求
await client.ExecuteAsync(request);
// 發送異常請求并且反序列化結果
var asyncHandle = client.ExecuteAsync(request, response => {
Console.WriteLine(response.Data.Name);
});
//終止異步的請求
asyncHandle.Abort();
}
}
}
較舊的方法,現在通常推薦使用 HttpClient。但在一些舊項目或特定場景下,WebRequest 和 WebResponse 仍然有用。
適用平臺:.NET Framework 1.1+, .NET Standard 2.0+, .NET Core 1.0+
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.IO; // for StreamReader
namespace ConsoleApplication
{
class Program
{
static void Main(string[] args)
{
//發送Get請求
var request = (HttpWebRequest)WebRequest.Create("http://www.example.com/recepticle.aspx");
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
//發送Post請求
var request = (HttpWebRequest)WebRequest.Create("http://www.example.com/recepticle.aspx");
var postData = "thing1=hello";
postData += "&thing2=world";
var data = Encoding.ASCII.GetBytes(postData);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = data.Length;
using (var stream = request.GetRequestStream())
{
stream.Write(data, 0, data.Length);
}
var response = (HttpWebResponse)request.GetResponse();
var responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();
}
}
}
C#中,WebClient類提供了一個簡單的方法來發送和接收數據到和從一個URI(如一個網頁或Web服務)上。
適用平臺:.NET Framework 1.1+, .NET Standard 2.0+, .NET Core 2.0+
*請認真填寫需求信息,我們會在24小時內與您取得聯系。