专栏首页BAT的乌托邦【小家Spring】Spring MVC容器的web九大组件之---HandlerMapping源码详解(二)---RequestMappingHandlerMapping系列

【小家Spring】Spring MVC容器的web九大组件之---HandlerMapping源码详解(二)---RequestMappingHandlerMapping系列

前言

上篇博客: 【小家Spring】Spring MVC容器的web九大组件之—HandlerMapping源码详解(一)—BeanNameUrlHandlerMapping系列 分析过了HandlerMapping的一些抽象实现,以及AbstractHandlerMapping的一个主要分支:AbstractUrlHandlerMapping体系的实现原理分析:它是基于类级别的Handler实现,大体上和源生servlet如出一辙,也还没有脱离源生servlet的API。作为第一版的实现,便捷度自然存在一些欠缺,但大的框架还是非常稳的。可以看出Spring的眼光、抽象思维算是顶级水准~

本文将介绍它的另外一个系列:AbstractHandlerMethodMapping系列,基于方法级别的Handler实现。也是当下最为主流的实现方式,更是最为常用使用方式

AbstractHandlerMethodMapping系列

AbstractHandlerMethodMapping系列是将method作为handler来使用的,比如@RequestMapping所注释的方法就是这种handler(当然它并不强制你一定得使用@RequestMapping这样的注解)。

在前面我们已经知道了AbstractHandlerMethodMapping的父类AbstractHandlerMapping,其定义了抽象方法getHandlerInternal(HttpServletRequest request),那么这里主要看看它对此抽象方法的实现:

// @since 3.1  Spring3.1之后才出现,这个时候注解驱动也出来了
// 实现了initializingBean接口,其实主要的注册操作则是通过afterPropertiesSet()接口方法来调用的
// 它是带有泛型T的。
// T:包含HandlerMethod与传入请求匹配所需条件的handlerMethod的映射~
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
	// SCOPED_TARGET的BeanName的前缀
	private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
	private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));
	// 跨域相关
	private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
	static {
		ALLOW_CORS_CONFIG.addAllowedOrigin("*");
		ALLOW_CORS_CONFIG.addAllowedMethod("*");
		ALLOW_CORS_CONFIG.addAllowedHeader("*");
		ALLOW_CORS_CONFIG.setAllowCredentials(true);
	}
	
	// 默认不会去祖先容器里面找Handlers
	private boolean detectHandlerMethodsInAncestorContexts = false;
	// @since 4.1提供的新接口
	// 为处HandlerMetho的映射分配名称的策略接口   只有一个方法getName()
	// 唯一实现为:RequestMappingInfoHandlerMethodMappingNamingStrategy
	// 策略为:@RequestMapping指定了name属性,那就以指定的为准  否则策略为:取出Controller所有的`大写字母` + # + method.getName()
	// 如:AppoloController#match方法  最终的name为:AC#match 
	// 当然这个你也可以自己实现这个接口,然后set进来即可(只是一般没啥必要这么去干~~)
	@Nullable
	private HandlerMethodMappingNamingStrategy<T> namingStrategy;
	// 内部类:负责注册~
	private final MappingRegistry mappingRegistry = new MappingRegistry();

	// 此处细节:使用的是读写锁  比如此处使用的是读锁   获得所有的注册进去的Handler的Map
	public Map<T, HandlerMethod> getHandlerMethods() {
		this.mappingRegistry.acquireReadLock();
		try {
			return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
		} finally {
			this.mappingRegistry.releaseReadLock();
		}
	}
	// 此处是根据mappingName来获取一个Handler  此处需要注意哦~~~
	@Nullable
	public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) {
		return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName);
	}
	// 最终都是委托给mappingRegistry去做了注册的工作   此处日志级别为trace级别
	public void registerMapping(T mapping, Object handler, Method method) {
		if (logger.isTraceEnabled()) {
			logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
		}
		this.mappingRegistry.register(mapping, handler, method);
	}
	public void unregisterMapping(T mapping) {
		if (logger.isTraceEnabled()) {
			logger.trace("Unregister mapping \"" + mapping + "\"");
		}
		this.mappingRegistry.unregister(mapping);
	}

	// 这个很重要,是初始化HandlerMethods的入口~~~~~
	@Override
	public void afterPropertiesSet() {
		initHandlerMethods();
	}
	// 看initHandlerMethods(),观察是如何实现加载HandlerMethod
	protected void initHandlerMethods() {
		// getCandidateBeanNames:Object.class相当于拿到当前容器(一般都是当前容器) 内所有的Bean定义信息
		// 如果阁下容器隔离到到的话,这里一般只会拿到@Controller标注的web组件  以及其它相关web组件的  不会非常的多的~~~~
		for (String beanName : getCandidateBeanNames()) {
			// BeanName不是以这个打头得  这里才会process这个BeanName~~~~
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				// 会在每个Bean里面找处理方法,HandlerMethod,然后注册进去
				processCandidateBean(beanName);
			}
		}
		// 略:它就是输出一句日志:debug日志或者trace日志   `7 mappings in 'requestMappingHandlerMapping'`
		handlerMethodsInitialized(getHandlerMethods());
	}

	// 确定指定的候选bean的类型,如果标识为Handler类型,则调用DetectHandlerMethods
	// isHandler(beanType):判断这个type是否为Handler类型   它是个抽象方法,由子类去决定到底啥才叫Handler~~~~
	// `RequestMappingHandlerMapping`的判断依据为:该类上标注了@Controller注解或者@Controller注解  就算作是一个Handler
	// 所以此处:@Controller起到了一个特殊的作用,不能等价于@Component的哟~~~~
	protected void processCandidateBean(String beanName) {
		Class<?> beanType = null;
		try {
			beanType = obtainApplicationContext().getType(beanName);
		} catch (Throwable ex) {
			// 即使抛出异常  程序也不会终止~
		}
		if (beanType != null && isHandler(beanType)) {
			// 这个和我们上篇博文讲述的类似,都属于detect探测系列~~~~
			detectHandlerMethods(beanName);
		}
	}

	// 在指定的Handler的bean中查找处理程序方法Methods  找打就注册进去:mappingRegistry
	protected void detectHandlerMethods(Object handler) {
		Class<?> handlerType = (handler instanceof String ?
				obtainApplicationContext().getType((String) handler) : handler.getClass());

		if (handlerType != null) {
			Class<?> userType = ClassUtils.getUserClass(handlerType);
		
			// 又是非常熟悉的方法:MethodIntrospector.selectMethods
			// 它在我们招@EventListener、@Scheduled等注解方法时已经遇到过多次
			// 此处特别之处在于:getMappingForMethod属于一个抽象方法,由子类去决定它的寻找规则~~~~  什么才算作一个处理器方法
			Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
					(MethodIntrospector.MetadataLookup<T>) method -> {
						try {
							return getMappingForMethod(method, userType);
						} catch (Throwable ex) {
							throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex);
						}
					});
			
			// 把找到的Method  一个个遍历,注册进去~~~~
			methods.forEach((method, mapping) -> {
				// 找到这个可调用的方法(AopUtils.selectInvocableMethod)
				Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
				registerHandlerMethod(handler, invocableMethod, mapping);
			});
		}
	}
}

该抽象类完成了所有的Handler以及handler里面所有的HandlerMethod的模版操作,但是决定哪些Bean是Handler类哪些方法才是HandlerMathod,这些逻辑都是交给子类自己去实现,所以这层抽象可谓也是非常的灵活,并没有把Handler的实现方式定死,允许不同

这里面有个核心内容:那就是注册handlerMethod,是交给AbstractHandlerMethodMapping的一个内部类MappingRegistry去完成的,用来专门维持所有的映射关系,并提供方法去查找方法去提供当前url映射的方法

AbstractHandlerMethodMapping.MappingRegistry:内部类注册中心

维护几个Map(键值对),用来存储映射的信息, 还有一个MappingRegistration专门保存注册信息

MappingRegistration:就是一个private的内部类,维护着T mapping、HandlerMethod handlerMethod、List<String> directUrls、String mappingName等信息,提供get方法访问。木有任何其它逻辑

	class MappingRegistry {
		// mapping对应的其MappingRegistration对象~~~
		private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
		// 保存着mapping和HandlerMethod的对应关系~
		private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
		// 保存着URL与匹配条件(mapping)的对应关系  当然这里的URL是pattern式的,可以使用通配符
		// 这里的Map不是普通的Map,而是MultiValueMap,它是个多值Map。其实它的value是一个list类型的值
		// 至于为何是多值?有这么一种情况  URL都是/api/v1/hello  但是有的是get post delete等方法   所以有可能是会匹配到多个MappingInfo的
		private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
		// 这个Map是Spring MVC4.1新增的(毕竟这个策略接口HandlerMethodMappingNamingStrategy在Spring4.1后才有,这里的name是它生成出来的)
		// 保存着name和HandlerMethod的对应关系(一个name可以有多个HandlerMethod)
		private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
		
		// 这两个就不用解释了
		private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
		// 读写锁~~~ 读写分离  提高启动效率
		private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

		... // 提供一些查找方法,都不是线程安全的
		
		// 读锁提供给外部访问,写锁自己放在内部即可~~~
		public void acquireReadLock() {
			this.readWriteLock.readLock().lock();
		}
		public void releaseReadLock() {
			this.readWriteLock.readLock().unlock();
		}

		// 注册Mapping和handler 以及Method    此处上写锁保证线程安全~
		public void register(T mapping, Object handler, Method method) {
			this.readWriteLock.writeLock().lock();
			try {
				// 此处注意:都是new HandlerMethod()了一个新的出来~~~~
				HandlerMethod handlerMethod = createHandlerMethod(handler, method);
				// 同样的:一个URL Mapping只能对应一个Handler
				// 这里可能会出现常见的一个异常信息:Ambiguous mapping. Cannot map XXX 
				assertUniqueMethodMapping(handlerMethod, mapping);
		
				// 缓存Mapping和handlerMethod的关系  
				this.mappingLookup.put(mapping, handlerMethod);

				// 保存url和RequestMappingInfo(mapping)对应关系
				// 这里注意:多个url可能对应着同一个mappingInfo呢~  毕竟@RequestMapping的url是可以写多个的~~~~
				List<String> directUrls = getDirectUrls(mapping);
				for (String url : directUrls) {
					this.urlLookup.add(url, mapping);
				}

				// 保存name和handlerMethod的关系  同样也是一对多
				String name = null;
				if (getNamingStrategy() != null) {
					name = getNamingStrategy().getName(handlerMethod, mapping);
					addMappingName(name, handlerMethod);
				}

				CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
				if (corsConfig != null) {
					this.corsLookup.put(handlerMethod, corsConfig);
				}

				// 注册mapping和MappingRegistration的关系
				this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
			}
			// 释放锁
			finally {
				this.readWriteLock.writeLock().unlock();
			}
		}

		// 相当于进行一次逆向操作~
		public void unregister(T mapping) { ... }
		...
	}

这个注册中心,核心是保存了多个Map映射关系,相当于缓存下来。在请求过来时需要查找的时候,可以迅速定位到处理器

下面继续,终于来到AbstractHandlerMethodMapping它对父类抽象方法:getHandlerInternal的实现如下:

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
	...
	@Override
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
		// 要进行匹配的  请求的URI path
		String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
		this.mappingRegistry.acquireReadLock();
		try {
			//委托给方法lookupHandlerMethod() 去找到一个HandlerMethod去最终处理~
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		}
		finally {
			this.mappingRegistry.releaseReadLock();
		}
	}
	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
		// Match是一个private class,内部就两个属性:T mapping和HandlerMethod handlerMethod
		List<Match> matches = new ArrayList<>();
		
		// 根据lookupPath去注册中心里查找mappingInfos,因为一个具体的url可能匹配上多个MappingInfo的
		// 至于为何是多值?有这么一种情况  URL都是/api/v1/hello  但是有的是get post delete等方法  当然还有可能是headers/consumes等等不一样,都算多个的  所以有可能是会匹配到多个MappingInfo的
		// 所有这个里可以匹配出多个出来。比如/hello 匹配出GET、POST、PUT都成,所以size可以为3
		List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
		
		if (directPathMatches != null) {
			// 依赖于子类实现的抽象方法:getMatchingMapping()  看看到底匹不匹配,而不仅仅是URL匹配就行
			// 比如还有method、headers、consumes等等这些不同都代表着不同的MappingInfo的
			// 最终匹配上的,会new Match()放进matches里面去
			addMatchingMappings(directPathMatches, matches, request);
		}
	
		// 当还没有匹配上的时候,别无选择,只能浏览所有映射
		// 这里为何要浏览所有的mappings呢?而不是报错404呢?这里我有点迷糊,愿有知道的指明这个设计意图~~~
		if (matches.isEmpty()) {
			// No choice but to go through all mappings...
			addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
		}

		// 单反只要找到了一个匹配的  就进来这里了~~~
		// 请注意:因为到这里   匹配上的可能还不止一个  所以才需要继续处理~~
		if (!matches.isEmpty()) {
			// getMappingComparator这个方法也是抽象方法由子类去实现的。
			// 比如:`RequestMappingInfoHandlerMapping`的实现为先比较Method,patterns、params
			Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
			matches.sort(comparator);
			// 排序后的最佳匹配为get(0)
			Match bestMatch = matches.get(0);
	
			// 如果总的匹配个数大于1的话
			if (matches.size() > 1) {
				if (CorsUtils.isPreFlightRequest(request)) {
					return PREFLIGHT_AMBIGUOUS_MATCH;
				}
		
				// 次最佳匹配
				Match secondBestMatch = matches.get(1);
				// 如果发现次最佳匹配和最佳匹配  比较是相等的  那就报错吧~~~~
				// Ambiguous handler methods mapped for~~~
				// 注意:这个是运行时的检查,在启动的时候是检查不出来的~~~  所以运行期的这个检查也是很有必要的~~~   否则就会出现意想不到的效果
				if (comparator.compare(bestMatch, secondBestMatch) == 0) {
					Method m1 = bestMatch.handlerMethod.getMethod();
					Method m2 = secondBestMatch.handlerMethod.getMethod();
					String uri = request.getRequestURI();
					throw new IllegalStateException(
							"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
				}
			}
			// 把最最佳匹配的方法  放进request的属性里面~~~
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
			// 它也是做了一件事:request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath)
			handleMatch(bestMatch.mapping, lookupPath, request);
			// 最终返回的是HandlerMethod~~~
			return bestMatch.handlerMethod;
		}
		// 一个都没匹配上,handleNoMatch这个方法虽然不是抽象方法,protected方法子类复写
		// RequestMappingInfoHandlerMapping有复写此方法~~~~
		else {
			return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
		}
	}
	...

	// 因为上面说了mappings可能会有多个,比如get post put的都算~~~这里就是要进行筛选出所有match上的
	private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
		for (T mapping : mappings) {
			// 只有RequestMappingInfoHandlerMapping 实现了一句话:return info.getMatchingCondition(request);
			// 因此RequestMappingInfo#getMatchingCondition() 方法里大有文章可为~~~
			// 它会对所有的methods、params、headers... 都进行匹配  但凡匹配不上的就返回null  
			T match = getMatchingMapping(mapping, request);
			if (match != null) {
				matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
			}
		}
	}
}

// ===============RequestMappingInfo 的源码部分讲解================
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
	
	// 这些个匹配器都继承自AbstractRequestCondition,会进行各自的匹配工作  
	// 下面会以PatternsRequestCondition为例进行示例讲解~~~~~
	// 他们顶级抽象接口为:RequestCondition  @since 3.1 :Contract for request mapping conditions
	private final PatternsRequestCondition patternsCondition;
	private final RequestMethodsRequestCondition methodsCondition;
	private final ParamsRequestCondition paramsCondition;
	private final HeadersRequestCondition headersCondition;
	private final ConsumesRequestCondition consumesCondition;
	private final ProducesRequestCondition producesCondition;
	private final RequestConditionHolder customConditionHolder;

	// 因为类上和方法上都可能会有@RequestMapping注解,所以这里是把语意思合并  该方法来自顶层接口
	@Override
	public RequestMappingInfo combine(RequestMappingInfo other) {
		String name = combineNames(other);
		PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
		RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
		ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
		HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
		ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
		ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
		RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);

		return new RequestMappingInfo(name, patterns,
				methods, params, headers, consumes, produces, custom.getCondition());
	}

	// 合并后,就开始发挥作用了,该接口来自于顶层接口~~~~
	@Override
	@Nullable
	public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
		RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
		if (methods == null) {
			return null;
		}
		ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
		if (params == null) {
			return null;
		}
		HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
		if (headers == null) {
			return null;
		}
		ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
		if (consumes == null) {
			return null;
		}
		ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
		if (produces == null) {
			return null;
		}
		PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
		if (patterns == null) {
			return null;
		}
		RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
		if (custom == null) {
			return null;
		}

		return new RequestMappingInfo(this.name, patterns,
				methods, params, headers, consumes, produces, custom.getCondition());
	}
}

到这里,这个抽象类所做的工作都全部完成了。 可以看到它做的事还是非常非常多的。它用泛型来抽象Mapping关系(包括条件、属性等),实现并不要求一定是@RequestMapping这种注解的方式,可以是任意方式,体现了它对扩展开放的设计思想~



Spring MVC请求URL带后缀匹配的情况,如/hello.json也能匹配/hello

RequestMappingInfoHandlerMapping 在处理http请求的时候, 如果 请求url 有后缀,如果找不到精确匹配的那个@RequestMapping方法。 那么,就把后缀去掉,然后.*去匹配,这样,一般都可以匹配,默认这个行为是被开启的。

比如有一个@RequestMapping("/rest"), 那么精确匹配的情况下, 只会匹配/rest请求。 但如果我前端发来一个 /rest.abcdef 这样的请求, 又没有配置 @RequestMapping("/rest.abcdef") 这样映射的情况下, 那么@RequestMapping("/rest") 就会生效。

这样会带来什么问题呢?绝大多数情况下是没有问题的,但是如果你是一个对权限要求非常严格的系统,强烈关闭此项功能,否则你会有意想不到的"收获"

究其原因咱们可以接着上面的分析,其实就到了PatternsRequestCondition这个类上,具体实现是它的匹配逻辑来决定的。

public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
	...
	@Override
	@Nullable
	public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) {
		// patterns表示此MappingInfo可以匹配的值们。一般对应@RequestMapping注解上的patters数组的值
		if (this.patterns.isEmpty()) {
			return this;
		}
		// 拿到待匹配的值,比如此处为"/hello.json"
		String lookupPath = this.pathHelper.getLookupPathForRequest(request);
		
		// 最主要就是这个方法了,它拿着这个lookupPath匹配~~~~
		List<String> matches = getMatchingPatterns(lookupPath);
		// 此处如果为empty,就返回null了~~~~
		return (!matches.isEmpty() ? new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions) : null);
	}

	public List<String> getMatchingPatterns(String lookupPath) {
		List<String> matches = new ArrayList<>();
		for (String pattern : this.patterns) {
			
			// 最最最重点就是在getMatchingPattern()这个方法里~~~ 拿着lookupPath和pattern看它俩合拍不~
			String match = getMatchingPattern(pattern, lookupPath);
			if (match != null) {
				matches.add(match);
			}
		}
		// 解释一下为何匹配的可能是多个。因为url匹配上了,但是还有可能@RequestMapping的其余属性匹配不上啊,所以此处需要注意  是可能匹配上多个的  最终是唯一匹配就成~
		if (matches.size() > 1) {
			matches.sort(this.pathMatcher.getPatternComparator(lookupPath));
		}
		return matches;
	}


	// // ===============url的真正匹配规则  非常重要~~~===============
	// 注意这个方法的取名,上面是负数,这里是单数~~~~命名规范也是有艺术感的
	@Nullable
	private String getMatchingPattern(String pattern, String lookupPath) {
		// 完全相等,那就不继续聊了~~~
		if (pattern.equals(lookupPath)) {
			return pattern;
		}

		// 注意了:useSuffixPatternMatch 这个属性就是我们最终要关闭后缀匹配的关键
		// 这个值默外部给传的true(其实内部默认值是boolean类型为false)
		if (this.useSuffixPatternMatch) {

			// 这个意思是若useSuffixPatternMatch=true我们支持后缀匹配。我们还可以配置fileExtensions让只支持我们自定义的指定的后缀匹配,而不是下面最终的.*全部支持
			if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) {
				for (String extension : this.fileExtensions) {
					if (this.pathMatcher.match(pattern + extension, lookupPath)) {
						return pattern + extension;
					}
				}
			}
			
			// 若你没有配置指定后缀匹配,并且你的handler也没有.*这样匹配的,那就默认你的pattern就给你添加上后缀".*",表示匹配所有请求的url的后缀~~~
			else {
				boolean hasSuffix = pattern.indexOf('.') != -1;
				if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
					return pattern + ".*";
				}
			}
		}
		// 若匹配上了 直接返回此patter
		if (this.pathMatcher.match(pattern, lookupPath)) {
			return pattern;
		}

		// 这又是它支持的匹配规则。默认useTrailingSlashMatch它也是true
		// 这就是为何我们的/hello/也能匹配上/hello的原因  
		// 从这可以看出,Spring MVC的宽容度是很高的,容错处理做得是非常不错的~~~~~~~
		if (this.useTrailingSlashMatch) {
			if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) {
				return pattern + "/";
			}
		}
		return null;
	}
}

分析了URL的匹配原因,现在肯定知道为何默认情况下"/hello.aaaa"或者"/hello.aaaa/“或者”"/hello/""能匹配上我们/hello的原因了吧~~~

Spring和SpringBoot中如何关闭此项功能呢?

为何要关闭的理由,上面其实已经说了。当我们涉及到严格的权限校验(强权限控制)的时候。特备是一些银行系统、资产系统等等,关闭后缀匹配事非常有必要的。

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware {

	private boolean useSuffixPatternMatch = true;
	private boolean useTrailingSlashMatch = true;
}

可以看到这两个属性值都直接冒泡到RequestMappingHandlerMapping这个实现类上来了,所以我们直接通过配置来改变它的默认行为就成。

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    // 关闭后缀名匹配,关闭最后一个/匹配
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseSuffixPatternMatch(false);
        configurer.setUseTrailingSlashMatch(false);
    }
}

**就这么一下,我们的URL就安全了,再也不能后缀名任意匹配了。**在想用后缀匹配,就甩你四个大字:



RequestMappingInfoHandlerMapping

提供匹配条件RequestMappingInfo的解析处理。

// @since 3.1 此处泛型为:RequestMappingInfo   用这个类来表示mapping映射关系、参数、条件等
public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
	// 专门处理Http的Options方法的HandlerMethod
	private static final Method HTTP_OPTIONS_HANDLE_METHOD;
	static {
		try {
			HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
		} catch (NoSuchMethodException ex) {
			throw new IllegalStateException("Failed to retrieve internal handler method for HTTP OPTIONS", ex);
		}
	}
	
	// 构造函数:给set了一个HandlerMethodMappingNamingStrategy
	protected RequestMappingInfoHandlerMapping() {
		setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy());
	}

	// 复写父类的抽象方法:获取mappings里面的patters们~~~
	@Override
	protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
		return info.getPatternsCondition().getPatterns();
	}
	// 校验看看这个Mapping是否能匹配上这个request,若能匹配上就返回一个RequestMappingInfo
	@Override
	protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
		return info.getMatchingCondition(request);
	}
	@Override
	protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
		return (info1, info2) -> info1.compareTo(info2, request);
	}
	...
}

它主要做的事就是确定了泛型类型为:RequestMappingInfo,然后很多方法都依托它来完成判定逻辑,比如上面三个@Override方法就是对父类抽象方法的实现。委托给RequestMappingInfo去实现的~

RequestMappingInfo的构建工作,Spring MVC理论上是可以允许有多种方案。鉴于Spring MVC给出的唯一实现类为RequestMappingHandlerMapping


下面就介绍Spring MVC目前的唯一构造方案:通过@RequestMapping来构造一个RequestMappingInfo

RequestMappingHandlerMapping 唯一实现类

根据@RequestMapping注解生成RequestMappingInfo,同时提供isHandler实现。

直到这个具体实现类,才与具体的实现方式@RequestMapping做了强绑定了

有了三层抽象的实现,其实留给本类需要实现的功能已经不是非常的多了~

// @since 3.1  Spring3.1才提供的这种注解扫描的方式的支持~~~  它也实现了MatchableHandlerMapping分支的接口
// EmbeddedValueResolverAware接口:说明要支持解析Spring的表达式~
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
		implements MatchableHandlerMapping, EmbeddedValueResolverAware {
	
	...
	private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>();

	// 配置要应用于控制器方法的路径前缀
	// @since 5.1:Spring5.1才出来的新特性,其实有时候还是很好的使的  下面给出使用的Demo
	// 前缀用于enrich每个@RequestMapping方法的映射,至于匹不匹配由Predicate来决定  有种前缀分类的效果~~~~
	// 推荐使用Spring5.1提供的类:org.springframework.web.method.HandlerTypePredicate
	public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) {
		this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes));
	}
	// @since 5.1   注意pathPrefixes是只读的~~~因为上面Collections.unmodifiableMap了  有可能只是个空Map
	public Map<String, Predicate<Class<?>>> getPathPrefixes() {
		return this.pathPrefixes;
	}
	
	public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
		this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
		this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
	}
	// If enabled a method mapped to "/users" also matches to "/users/".
	public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
		this.useTrailingSlashMatch = useTrailingSlashMatch;
	}
	
	@Override
	public void afterPropertiesSet() {
		// 对RequestMappingInfo的配置进行初始化  赋值
		this.config = new RequestMappingInfo.BuilderConfiguration();
		this.config.setUrlPathHelper(getUrlPathHelper()); // 设置urlPathHelper默认为UrlPathHelper.class
		this.config.setPathMatcher(getPathMatcher()); //默认为AntPathMatcher,路径匹配校验器
		this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); // 是否支持后缀补充,默认为true
		this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); // 是否添加"/"后缀,默认为true
		this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); // 是否采用mediaType匹配模式,比如.json/.xml模式的匹配,默认为false      
		this.config.setContentNegotiationManager(getContentNegotiationManager()); //mediaType处理类:ContentNegotiationManager

		// 此处 必须还是要调用父类的方法的
		super.afterPropertiesSet();
	}
	...

	// 判断该类,是否是一个handler(此处就体现出@Controller注解的特殊性了)
	// 这也是为何我们的XXXController用@Bean申明是无效的原因(前提是类上木有@RequestMapping注解,否则也是阔仪的哦~~~)
	// 因此我个人建议:为了普适性,类上的@RequestMapping也统一要求加上,即使你不写@Value也木关系,这样是最好的
	@Override
	protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
	}

	// 还记得父类:AbstractHandlerMethodMapping#detectHandlerMethods的时候,回去该类里面找所有的指定的方法
	// 而什么叫指定的呢?就是靠这个来判定方法是否符合条件的~~~~~
	@Override
	@Nullable
	protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
		// 第一步:先拿到方法上的info
		RequestMappingInfo info = createRequestMappingInfo(method);
		if (info != null) {
			// 方法上有。在第二步:拿到类上的info
			RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
			if (typeInfo != null) {
				// 倘若类上面也有,那就combine把两者结合
				// combile的逻辑基如下:
				// names:name1+#+name2
				// path:路径拼接起来作为全路径(容错了方法里没有/的情况)
				// method、params、headers:取并集
				// consumes、produces:以方法的为准,没有指定再取类上的
				// custom:谁有取谁的。若都有:那就看custom具体实现的.combine方法去决定把  简单的说就是交给调用者了~~~
				info = typeInfo.combine(info);
			}

			// 在Spring5.1之后还要处理这个前缀问题~~~
			// 根据这个类,去找看有没有前缀  getPathPrefix():entry.getValue().test(handlerType) = true算是hi匹配上了
			// 备注:也支持${os.name}这样的语法拿值,可以把前缀也写在专门的配置文件里面~~~~
			String prefix = getPathPrefix(handlerType);
			if (prefix != null) {
				// RequestMappingInfo.paths(prefix)  相当于统一在前面加上这个前缀~
				info = RequestMappingInfo.paths(prefix).build().combine(info);
			}
		}
		return info;
	}

	// 根据此方法/类,创建一个RequestMappingInfo
	@Nullable
	private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
		// 注意:此处使用的是findMergedAnnotation  这也就是为什么虽然@RequestMapping它并不具有继承的特性,但是你子类仍然有继承的效果的原因~~~~
		RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
		
		// 请注意:这里进行了区分处理  如果是Class的话  如果是Method的话
		// 这里返回的是一个condition 也就是看看要不要处理这个请求的条件~~~~
		RequestCondition<?> condition = (element instanceof Class ?
				getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
		
		// 这个createRequestMappingInfo就是根据一个@RequestMapping以及一个condition创建一个
		// 显然如果没有找到此注解,这里就返回null了,表面这个方法啥的就不是一个info~~~~
		return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
	}

	// 他俩都是返回的null。protected方法留给子类复写,子类可以据此自己定义一套自己的规则来限制匹配
	// Provide a custom method-level request condition.
	// 它相当于在Spring MVC默认的规则的基础上,用户还可以自定义条件进行处理~~~~
	@Nullable
	protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
		return null;
	}
	@Nullable
	protected RequestCondition<?> getCustomMethodCondition(Method method) {
		return null;
	}

	// 根据@RequestMapping 创建一个RequestMappingInfo 
	protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {

		RequestMappingInfo.Builder builder = RequestMappingInfo
				// 强大的地方在此处:path里竟然还支持/api/v1/${os.name}/hello 这样形式动态的获取值
				// 也就是说URL还可以从配置文件里面读取  Spring考虑很周到啊~~~
				// @GetMapping("/${os.name}/hello") // 支持从配置文件里读取此值  Windows 10
				.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
				.methods(requestMapping.method())
				.params(requestMapping.params())
				.headers(requestMapping.headers())
				.consumes(requestMapping.consumes())
				.produces(requestMapping.produces())
				.mappingName(requestMapping.name());
		// 调用者自定义的条件~~~
		if (customCondition != null) {
			builder.customCondition(customCondition);
		}
		// 注意此处:把当前的config设置进去了~~~~
		return builder.options(this.config).build();
	}

	@Override
	public RequestMatchResult match(HttpServletRequest request, String pattern) { ... }
	// 支持了@CrossOrigin注解  Spring4.2提供的注解
	@Override
	protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { ... }
}

至此RequestMappingHandlerMapping的初始化完成了。

RequestMappingHandlerMapping 向容器中注册的时候,检测到实现了 InitializingBean接口,容器去执行afterPropertiesSet(),在afterPropertiesSet中完成Controller中完成方法的映射

以上就是Spring MVC在容器启动过程中,完成URL到Handler映射的所有内容~



@RequestMapping属性详解

使用@RequestMapping 来映射URL 到控制器类,或者是到Controller 控制器的处理方法上。 当@RequestMapping 标记在Controller 类上的时候,里面使用@RequestMapping 标记的方法的请求地址都是相对于类上的@RequestMapping 而言的;当Controller 类上没有标记@RequestMapping 注解时,方法上的@RequestMapping 都是绝对路径。

这种绝对路径和相对路径所组合成的最终路径都是相对于根路径“/ ”而言的

这个注解的属性众多,下面逐个解释一下:

// @since 2.5 用于将Web请求映射到具有灵活方法签名的请求处理类中的方法的注释  Both Spring MVC and `Spring WebFlux` support this annotation
// @Mapping这个注解是@since 3.0  但它目前还只有这个地方使用到了~~~ 我感觉是多余的
@Target({ElementType.METHOD, ElementType.TYPE}) // 能够用到类上和方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {

	//给这个Mapping取一个名字。若不填写,就用HandlerMethodMappingNamingStrategy去按规则生成
	String name() default "";

	// 路径  数组形式  可以写多个。  一般都是按照Ant风格进行书写~
	@AliasFor("path")
	String[] value() default {};
	@AliasFor("value")
	String[] path() default {};
	
	// 请求方法:GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
	// 显然可以指定多个方法。如果不指定,表示适配所有方法类型~~
	// 同时还有类似的枚举类:org.springframework.http.HttpMethod
	RequestMethod[] method() default {};
	
	// 指定request中必须包含某些参数值时,才让该方法处理
	// 使用 params 元素,你可以让多个处理方法处理到同一个URL 的请求, 而这些请求的参数是不一样的
	// 如:@RequestMapping(value = "/fetch", params = {"personId=10"} 和 @RequestMapping(value = "/fetch", params = {"personId=20"}
	// 这两个方法都处理请求`/fetch`,但是参数不一样,进入的方法也不一样~~~~
	// 支持!myParam和myParam!=myValue这种~~~
	String[] params() default {};

	// 指定request中必须包含某些指定的header值,才能让该方法处理请求
	// @RequestMapping(value = "/head", headers = {"content-type=text/plain"}
	String[] headers() default {};

	// 指定处理请求request的**提交内容类型**(Content-Type),例如application/json、text/html等
	// 相当于只有指定的这些Content-Type的才处理 
	// @RequestMapping(value = "/cons", consumes = {"application/json", "application/XML"}
	// 不指定表示处理所有~~  取值参见枚举类:org.springframework.http.MediaType
	// 它可以使用!text/plain形如这样非的表达方式
	String[] consumes() default {};
	// 指定返回的内容类型,返回的内容类型必须是request请求头(Accept)中所包含的类型
	// 仅当request请求头中的(Accept)类型中包含该指定类型才返回;
	// 参见枚举类:org.springframework.http.MediaType
	// 它可以使用!text/plain形如这样非的表达方式
	String[] produces() default {};

}

Spring4.3之后提供了组合注解5枚:

@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
consumes 与 headers 区别

consumes produces params headers四个属性都是用来缩小请求范围。 consumes只能指定 content-Type 的内容类型,但是headers可以指定所有。 所以可以认为:headers是更为强大的(所有需要指定key和value嘛),而consumesproduces是专用的,头的key是固定的,所以只需要写value值即可,使用起来也更加的方便~。

推荐一个类:org.springframework.http.HttpHeaders,它里面有常量:几乎所有的请求头的key,以及我们可以很方便的构建一个HttpHeader,平时可以作为参考使用

Spring MVC默认使用的HandlerMapping是什么?

Spring对这块的设计也是很灵活的,允许你自己配置,也允许你啥都不做使用Spring默认的配置。处理代码在:DispatcherServlet#initHandlerMappings

public class DispatcherServlet extends FrameworkServlet {
	
	// 为此DispatcherServlet 初始化HandlerMappings
	// 备注:DispatcherServlet是允许你有多个的~~~~
	private void initHandlerMappings(ApplicationContext context) {
		this.handlerMappings = null;
		
		//detectAllHandlerMappings该属性默认为true,表示会去容器内找所有的HandlerMapping类型的定义信息
		// 若想改为false,请调用它的setDetectAllHandlerMappings() 自行设置值(绝大部分情况下没啥必要)
		if (this.detectAllHandlerMappings) {
			// 这里注意:若你没有标注注解`@EnableWebMvc`,那么这里找的结果是空的
			// 若你标注了此注解,这个注解就会默认向容器内注入两个HandlerMapping:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
			Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);

			if (!matchingBeans.isEmpty()) {
				this.handlerMappings = new ArrayList<>(matchingBeans.values());
				// 多个的话 还需要进行一次排序~~~
				AnnotationAwareOrderComparator.sort(this.handlerMappings);
			}
		}
		// 不全部查找,那就只找一个名字为`handlerMapping`的HandlerMapping 实现精准控制
		// 绝大多数情况下  我们并不需要这么做~
		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.
			}
		}

		// 若一个都没找到自定义的,回滚到Spring的兜底策略,它会想容器注册两个:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping
		if (this.handlerMappings == null) {
			this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
			// 输出trace日志:表示使用了兜底策略~
			// 兜底策略配置文件:DispatcherServlet.properties
			if (logger.isTraceEnabled()) {
				logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
						"': using default strategies from DispatcherServlet.properties");
			}
		}
	}
}

通过这段代码,我们能够很清晰的看到。绝大部分情况下,我们容器内会有这两个HandlerMapping Bean: RequestMappingHandlerMapping和BeanNameUrlHandlerMapping 换句话说,默认情况下@RequestMapping和BeanNameUrl的方式都是被支持的~



请注意:使用@EnableWebMvc和不使用它有一个非常非常重要的区别: 使用@EnableWebMvc原来是依托于这个WebMvcConfigurationSupport config类向容器中注入了对应的Bean,所以他们都是交给了Spring管理的(所以你可以@Autowired他们) 但是,但是,但是(重说三),若是走了Spring它自己去读取配置文件走默认值,它的Bean是没有交给Spring管理的,没有交给Spring管理的。它是这样创建的:context.getAutowireCapableBeanFactory().createBean(clazz) 它创建出来的Bean都不会交给Spring管理。 参考博文:【小家Spring】为脱离Spring IOC容器管理的Bean赋能【依赖注入】的能力,并分析原理(借助AutowireCapableBeanFactory赋能)



小插曲:在Spring5以下DispatcherServlet.properties这个配置文件里写的是这样的:

相当于最底层默认使用的是DefaultAnnotationHandlerMapping,而在Spring5之后,改成了RequestMappingHandlerMappingDefaultAnnotationHandlerMapping是Spring2.5用来处理@RequestMapping注解的,自从Spring3.2后已被标记为:@Deprecated

需要注意的是:纯Spring MVC环境下我们都会开启@EnableWebMvc,所有我们实际使用的还是RequestMappingHandlerMapping的。 而在SpringBoot环境下,虽然我们一般不建议标注@EnableWebMvc,但是Boot它默认也会注册RequestMappingHandlerMapping它的。现在Spring5/Boot2以后一切都爽了~~~~

DefaultAnnotationHandlerMapping的一个小坑

在功能上DefaultAnnotationHandlerMappingRequestMappingHandlerMapping绝大多数是等价的。但是因为DefaultAnnotationHandlerMapping过于古老了,它并不支持像@GetMapping(Spring4.3后提供)这样的组合注解的。 从源码角度理由如下:

比如Handler这么写的:

    @ResponseBody
    @GetMapping("/hello/test")
    public Object test(String userName) {
        System.out.println(userName);
        return null;
    }

DefaultAnnotationHandlerMapping处理代码为:

...
RequestMapping mapping = AnnotationUtils.findAnnotation(method, RequestMapping.class);
...

值如下:

发现我们的URL并没有获取到。 但是RequestMappingHandlerMapping的获取代码为:

...
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
...

可以发现使用AnnotatedElementUtils.findMergedAnnotation是支持这个组合注解的。但是AnnotatedElementUtils整个工具类才Spring4.0后才有,而DefaultAnnotationHandlerMapping早在Spring3.2后就被标记为废弃了,因为就无需Spring也就无需继续维护了~~~~

所以若你是纯Spring MVC环境,为确保万无一失,请开启SpringMVC:@EnableWebMvc

备注:若使用非组合注解如@RequestMapping,两者大体一样。但既然人家都废弃了,所以非常不建议再继续使用~~~ 其实在Spring5.以后,就直接把这个两个类拿掉了,所以也就没有后顾之忧了。(DispatcherServlet.properties这个配置文件也做了对应的修改)

总结

Spring MVC在启动时会扫描所有的@RequestMapping并封装成对应的RequestMapingInfo。 一个请求过来会与RequestMapingInfo进行逐个比较,找到最适合的那个RequestMapingInfo

Spring MVC通过HandlerMapping建立起了Url Pattern和Handler的对应关系,这样任何一个URL请求过来时,就可以快速定位一个唯一的Handler,然后交给其进行处理了~ 当然这里面还有很多实现细节,其中还有一个非常重要的一块:HandlerAdapter,会在下文继续源码分析,请保持持续关注~

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • jQuery实用工具类--jQuery基础知识点(3)

    版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

    奋飛
  • Swoole引擎原理的快速入门干货

    过去半年使用PHP和Java两种技术栈完成了一个游戏服务器项目。由于项目中有高频的网络请求,所以PHP技术栈尝试使用Swoole引擎(基于事件的高性能异步并行网...

    全菜工程师小辉
  • WordPress自动更新太坑了,如何关闭自动更新?

    有些时候我们会收到关于WordPress自动更新成功了的邮件信息,提示你WordPress自动的给你升级了版本,这个有人喜有人忧的功能我觉得吧,确实得需要分开的...

    wordpress建站吧
  • 外行学 Python 爬虫 第十篇 爬虫框架Scrapy

    前面几个章节利用 python 的基础库实现网络数据的获取、解构以及存储,同时也完成了简单的数据读取操作。在这个过程中使用了其他人完成的功能库来加快我们的爬虫实...

    keinYe
  • 微信小程序实战教程:火车票查询(含demo)

    微信小程序自九月份推出内测资格以来,经历了舆论热潮到现在看似冷清,但并不意味着大家不那么关注或者不关注了。我想不管是否有内测资格,只要是感兴趣的开发者已经进入潜...

    用户5997198
  • 一个xss漏洞问题分析

    http://zhibo.sogou.com/gameZone_格斗游戏.whtml/gameZone_格斗游戏.whtml?product=live&page...

    杨肆月
  • 《细说PHP》第四版 样章 第二章 PHP的应用与发展 1

    学习任何编程语言之前,先了解一下它的应用与发展是很有必要的。从Web开发的历史看来,PHP、Python和Ruby几乎是同时出现的,都是十分有特点、优秀的开源语...

    ITXDL
  • PHP设计模式之观察者模式

    观察者,貌似在很多科幻作品中都会有这个角色的出现。比如我很喜欢的一部美剧《危机边缘》,在这个剧集中,观察者不停的穿越时空记录着各种各样的人或事。但是,设计模式中...

    硬核项目经理
  • 《Spring实战》摘录 - 27

    Q: #17.3.3-1 | RabbitMQ连接工厂的作用是创建到RabbitMQ的连接

    用户1335799
  • 渐进式Web应用清单(翻译转载)

    渐进式WEB应用(PWA)是可靠、快速和吸引人的,有很方法是可以把一个PWA从初级提升到高级。

    杨肆月

扫码关注云+社区

领取腾讯云代金券