首页 文章 精选 留言 我的

精选列表

搜索[mvc],共2761篇文章
优秀的个人博客,低调大师

Spring MVC之RequestMappingHandlerMapping匹配

对于RequestMappingHandlerMapping,使用Spring的同学基本都不会陌生,该类的作用有两个: 通过request查找对应的HandlerMethod,即当前request具体是由Controller中的哪个方法进行处理; 查找当前系统中的Interceptor,将其与HandlerMethod封装为一个HandlerExecutionChain。 本文主要讲解RequestMappingHandlerMapping是如何获取HandlerMethod和Interceptor,并且将其封装为HandlerExecutionChain的。 1.整体封装结构 RequestMappingHandlerMapping实现了HandlerMapping接口,该接口的主要方法如下: public interface HandlerMapping { // 通过request获取HandlerExecutionChain对象 HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; } 这里我们直接看RequestMappingHandlerMapping是如何实现该接口的: @Override @Nullable public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { // 通过request获取具体的处理bean,这里handler可能有两种类型:HandlerMethod和String。 // 如果是String类型,那么就在BeanFactory中查找该String类型的bean,需要注意的是,返回的 // bean如果是需要使用RequestMappingHandlerAdapter处理,那么也必须是HandlerMethod类型的 Object handler = getHandlerInternal(request); if (handler == null) { // 如果找不到处理方法,则获取自定义的默认handler handler = getDefaultHandler(); } if (handler == null) { return null; } if (handler instanceof String) { // 如果获取的handler是String类型的,则在当前BeanFactory中获取该名称的bean, // 并将其作为handler返回 String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } // 获取当前系统中配置的Interceptor,将其与handler一起封装为一个HandlerExecutionChain HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); // 这里CorsUtils.isCorsRequest()方法判断的是当前请求是否为一个跨域的请求,如果是一个跨域的请求, // 则将跨域相关的配置也一并封装到HandlerExecutionChain中 if (CorsUtils.isCorsRequest(request)) { CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; } 从上面的代码可以看出,对于HandlerExecutionChain的获取,RequestMappingHandlerMapping首先会获取当前request对应的handler,然后将其与Interceptor一起封装为一个HandlerExecutionChain对象。这里在进行封装的时候,Spring会对当前request是否为跨域请求进行判断,如果是跨域请求,则将相关的跨域配置封装到HandlerExecutionChain中,关于跨域请求,读者可以阅读跨域资源共享 CORS 详解。 2. 获取HandlerMethod 关于RequestMappingHandlerMapping是如何获取handler的,其主要在getHandlerInternal()方法中,如下是该方法的源码: @Override protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { // 获取当前request的URI String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); if (logger.isDebugEnabled()) { logger.debug("Looking up handler method for path " + lookupPath); } // 获取注册的Mapping的读锁 this.mappingRegistry.acquireReadLock(); try { // 通过path和request查找具体的HandlerMethod HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); if (logger.isDebugEnabled()) { if (handlerMethod != null) { logger.debug("Returning handler method [" + handlerMethod + "]"); } else { logger.debug("Did not find handler method for [" + lookupPath + "]"); } } // 如果获取到的bean是一个String类型的,则在BeanFactory中查找该bean, // 并将其封装为一个HandlerMethod对象 return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } finally { // 释放当前注册的Mapping的读锁 this.mappingRegistry.releaseReadLock(); } } 上述方法中,其首先会获取当前request的uri,然后通过uri查找HandlerMethod,并且在最后,会判断获取到的HandlerMethod中的bean是否为String类型的,如果是,则在当前BeanFactory中查找该名称的bean,并且将其封装为HandlerMethod对象。这里我们直接阅读lookupHandlerMethod()方法: @Nullable protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<>(); // 通过uri直接在注册的RequestMapping中获取对应的RequestMappingInfo列表,需要注意的是, // 这里进行查找的方式只是通过url进行查找,但是具体哪些RequestMappingInfo是匹配的,还需要进一步过滤 List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { // 对获取到的RequestMappingInfo进行进一步过滤,并且将过滤结果封装为一个Match列表 addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { // 如果无法通过uri进行直接匹配,则对所有的注册的RequestMapping进行匹配,这里无法通过uri // 匹配的情况主要有三种: // ①在RequestMapping中定义的是PathVariable,如/user/detail/{id}; // ②在RequestMapping中定义了问号表达式,如/user/?etail; // ③在RequestMapping中定义了*或**匹配,如/user/detail/** addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } if (!matches.isEmpty()) { // 对匹配的结果进行排序,获取相似度最高的一个作为结果返回,这里对相似度的判断时, // 会判断前两个是否相似度是一样的,如果是一样的,则直接抛出异常,如果不相同, // 则直接返回最高的一个 Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); matches.sort(comparator); if (logger.isTraceEnabled()) { logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches); } // 获取匹配程度最高的一个匹配结果 Match bestMatch = matches.get(0); if (matches.size() > 1) { // 如果匹配结果不止一个,首先会判断是否是跨域请求,如果是, // 则返回PREFLIGHT_AMBIGUOUS_MATCH,如果不是,则会判断前两个匹配程度是否相同, // 如果相同则抛出异常 if (CorsUtils.isPreFlightRequest(request)) { return PREFLIGHT_AMBIGUOUS_MATCH; } Match secondBestMatch = matches.get(1); if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.handlerMethod.getMethod(); Method m2 = secondBestMatch.handlerMethod.getMethod(); throw new IllegalStateException("Ambiguous handler methods mapped for" + " HTTP path '" + request.getRequestURL() + "': {" + m1 + ", " + m2 + "}"); } } // 这里主要是对匹配结果的一个处理,主要包含对传入参数和返回的MediaType的处理 handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.handlerMethod; } else { // 如果匹配结果是空的,则对所有注册的Mapping进行遍历,判断当前request具体是哪种情况导致 // 的无法匹配:①RequestMethod无法匹配;②Consumes无法匹配;③Produces无法匹配; // ④Params无法匹配 return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); } } 这里对于结果的匹配,首先会通过uri进行直接匹配,如果能匹配到,则在匹配结果中尝试进行RequestMethod,Consumes和Produces等配置的匹配;如果通过uri不能匹配到,则直接对所有定义的RequestMapping进行匹配,这里主要是进行正则匹配,如果能匹配到。如果能够匹配到,则对匹配结果按照相似度进行排序,并且对前两个结果相似度进行比较,如果相似度一样,则抛出异常,如果不一样,则返回相似度最高的一个匹配结果。如果无法获取到匹配结果,则对所有的匹配结果进行遍历,判断当前request具体是哪一部分参数无法匹配到结果。对于匹配结果的获取,主要在addMatchingMappings()方法中,这里我们继续阅读该方法的源码: private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) { for (T mapping : mappings) { T match = getMatchingMapping(mapping, request); if (match != null) { matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping))); } } } 对于RequestMapping的匹配,这里逻辑比较简单,就是对所有的RequestMappingInfo进行遍历,然后将request分别于每个RequestMappingInfo进行匹配,如果匹配上了,其返回值就不为空,最后将所有的匹配结果返回。如下是getMatchingMapping()方法的源码(其最终调用的是RequestMappingInfo.getMatchingCondition()方法): @Override @Nullable public RequestMappingInfo getMatchingCondition(HttpServletRequest request) { // 判断request请求的类型是否与当前RequestMethod匹配 RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request); // 判断request请求的参数是否与RequestMapping中params参数配置的一致 ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request); // 判断request请求的headers是否与RequestMapping中headers参数配置的一致 HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request); // 判断request的请求体类型是否与RequestMapping中配置的consumes参数配置的一致 ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request); // 判断当前RequestMapping将要返回的请求体类型是否与request中Accept的header指定的一致 ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request); // 对于上述几个判断,如果匹配上了,那么其返回值都不会为空,因而这里会对每个返回值都进行判断, // 如果有任意一个为空,则说明没匹配上,那么就返回null if (methods == null || params == null || headers == null || consumes == null || produces == null) { return null; } // 对于前面的匹配,都是一些静态属性的匹配,其中最重要的uri的匹配,主要是正则匹配, // 就是在下面这个方法中进行的 PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request); // 如果URI没匹配上,则返回null if (patterns == null) { return null; } // 这里主要是对用户自定义的匹配条件进行匹配 RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request); if (custom == null) { return null; } // 如果上述所有条件都匹配上了,那么就将匹配结果封装为一个RequestMappingInfo返回 return new RequestMappingInfo(this.name, patterns, methods, params, headers, consumes, produces, custom.getCondition()); } 可以看到,对于一个RequestMapping的匹配,主要包括:RequestMethod,Params,Headers,Consumes,Produces,Uri和自定义条件的匹配,如果这几个条件都匹配上了,才能表明当前RequestMapping与request匹配上了。 3. Interceptor的封装 关于Inteceptor的封装,由前述第一点可以看出,其主要在getHandlerExecutionChain()方法中,如下是该方法的源码: protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { // 将当前handler封装到HandlerExecutionChain对象中 HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler)); // 获取当前request的URI,用于MappedInterceptor的匹配 String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); // 对当前所有注册的Interceptor进行遍历,如果其是MappedInterceptor类型,则调用其matches() // 方法,判断当前Interceptor是否能够应用于该request,如果可以,则添加到HandlerExecutionChain中 for (HandlerInterceptor interceptor : this.adaptedInterceptors) { if (interceptor instanceof MappedInterceptor) { MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { chain.addInterceptor(mappedInterceptor.getInterceptor()); } } else { // 如果当前Interceptor不是MappedInterceptor类型,则直接将其添加到 // HandlerExecutionChain中 chain.addInterceptor(interceptor); } } return chain; } 对于拦截器,理论上,Spring是会将所有的拦截器都进行一次调用,对于是否需要进行拦截,都是用户自定义实现的。这里如果对于URI有特殊的匹配,可以使用MappedInterceptor,然后实现其matches()方法,用于判断当前MappedInterceptor是否能够应用于当前request。 4. 小结 本文首先讲解了Spring是如何通过request进行匹配,从而找到具体处理当前请求的RequestMapping的,然后讲解了Spring是如何封装Interceptor,将HandlerMethod和Interceptor封装为一个HandlerExecutionChain的。

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

MVC框架,webx 流程解读

写在前面的话 写这篇文章的目的,主要还是站在一个新人的角度做一些沉淀,同样也为了方便后面的人快速熟悉集团web开发常用技术。这篇文章,将分析一个请求发到服务端,经tomcat容器、webx映射直到最后调用controller入口的过程,对java web基础知识结合webx做一个整体回顾。同时,也将简单分析集团web的基本分层方式,以及VO、DTO、DO等数据实例在各层所起的作用。 Web容器 理解web容器,也是理解我们程序的运行平台,同时也是了解servlet处理流程的基础所在。tomcat的主要结构如下图: Connector Connector是tomcat的连接器。Tomcat在监听80端口的时候,一个HTTP请求访问过来,实际上是通过在80端口用socket来输入HTTP报文。Connector通过socket读取报文文本并进行解析,然后将报文内容封装为request实体,并将响应结果利用response进行封装,新起一个线程,并交给container容器进行处理。 Container container是所有子容器的父接口。Engine主要负责对请求进行分发,而Host就是tomcat虚拟host功能的实体,Context就是我们一个应用服务的完整环境,每次一个请求对应的一个新的servlet,都是封装在Context中的。 Tomcat在处理Connector传来的request和response时,就是通过责任链模式,一层一层的去调用Engine、Host、Context并封装出一个servletWrapper来进行doService操作。 Servlet处理流程及WebX的调用 上面简单介绍了一下Tomcat的容器结构,接下来具体分析处理一个请求的过程。 tomcat容器执行过程 请求到达后,会新起一个线程并由Container进行处理。 Container中的Engine、Host、Context、Wrapper都继承了ValveBase抽象类并实现了其invoke方法。这也类似于webx的pipeline,对请求进行流水线处理。 首先是StandardEngineValve的invoke。engine通过对请求host域名的解析,映射到合适的host容器中去,然后调用host的invoke方法。 StandardEngine engine = (StandardEngine)this.getContainer(); Host host = (Host)engine.map(request, true); if(host == null) { ((HttpServletResponse)response.getResponse()).sendError(400, sm.getString("standardEngine.noHost", request.getRequest().getServerName())); } else { host.invoke(request, response); } 在host的invoke中,实际是调用了流水线的invoke方法: public void invoke(Request request, Response response) throws IOException, ServletException { this.pipeline.invoke(request, response); } 最终又调用到standardPipeline中的invokeNext: public void invokeNext(Request request, Response response) throws IOException, ServletException { Integer current = (Integer)this.state.get(); int subscript = current.intValue(); this.state.set(new Integer(subscript + 1)); if(subscript < this.valves.length) { this.valves[subscript].invoke(request, response, this); } else { if(subscript != this.valves.length || this.basic == null) { throw new ServletException(sm.getString("standardPipeline.noValve")); } this.basic.invoke(request, response, this); } } 可以看到流水线的设计模式是将engine、host、context等容器的实例放进一个数组中,并在运行时依次执行。 然后就到了StandardHostValve中,同样实行invoke方法: StandardHost host = (StandardHost)this.getContainer(); Context context = (Context)host.map(request, true); 逻辑也类似,就是找到对应的context,也就是我们的应用并调用invoke方法。 context的invoke主要做了两件事: 1、获取session并绑定 2、找到该url对应的wrapper,并调用wrapper的invoke方法。 这里先略过session的获取绑定过程,直接来到wrapper的调用中去。在servletWrapper中,我们可以看到几个熟悉的对象: StandardWrapper wrapper = (StandardWrapper)this.getContainer(); ServletRequest sreq = request.getRequest(); ServletResponse sres = response.getResponse(); Servlet servlet = null; HttpServletRequest hreq = null; servlet会通过wrapper进行分配: servlet = wrapper.allocate(); 从allocate方法中,我们可以看到servlet实际是通过类加载器记载的,并且当servlet加载完毕后,会调用初始化方法init。 classClass = classLoader.loadClass(actualClass); ... servlet = (Servlet)classClass.newInstance(); ... servlet.init(this.facade); 有了servlet以后,就开始出现另一个重要角色:ApplicationFilterChain。FilterChain会传入request和response并对servlet进行过滤: filterChain.doFilter(sreq, sres); 在doFilter中,会调用到internalDoFilter 方法。该方法对filters进行遍历调用: this.iterator = this.filters.iterator(); ... filter.doFilter(request, response, this); 可以看到,FilterChain 采用链式模式,通过多次调用chain.doFilter(request, response)方法会多次调用internalDoFilter方法,从而通过Iterator调用所有注册的Filter。 Webx执行流程 以一个简单的接口为例子,来看一看webX最终是如何调用到我们的execute方法的。 public class DemoScreen extends BmsBaseModule { public void execute(@Param('param') String param) { ... } } 首先,继续上一部分的Filter开始看。Filter是servlet的标准过滤器,而上部分最后提到的filters则封装了web.xml文件中关于Filter的配置: <filter> <filter-name>webx</filter-name> <filter-class>com.alibaba.citrus.webx.servlet.WebxFrameworkFilter</filter-class> <init-param> <param-name>excludes</param-name> <param-value>*/checkpreload.htm<!-- 需要被“排除”的URL路径,以逗号分隔,如/static, *.jpg。适合于映射静态页面、图片。 --></param-value> </init-param> <init-param> <param-name>passthru</param-name> <param-value><!-- 需要被“略过”的URL路径,以逗号分隔,如/myservlet, *.jsp。适用于映射servlet、filter。 对于passthru请求,webx的request-contexts服务、错误处理、开发模式等服务仍然可用。 --></param-value> </init-param> </filter> 上面是一个典型的webx配置,可以看到,设置的过滤器WebXFrameworkFilter会被ApplicationFilterChain执行。来看其中的doFilter方法: if (isExcluded(path)) { log.debug("Excluded request: {}", path); chain.doFilter(request, response); return; } 校验我们设置的excluded参数,如果需要排除,那么通过doFilter的internalDoFilter来链式调用下一个过滤器。 关键语句在此: getWebxComponents().getWebxRootController().service(request, response, chain); 调用webx RootController的service方法。service中首先对HttpServletRequest 和 HttpServletResponse 实例封装进RequestContext中。后续会调用 WebxRootControllerImpl 的 handleRequest 方法: 然后,会根据路径找到WebxComponent: WebxComponent component = getComponents().findMatchedComponent(path); 在findMatchedComponent中会依据url来进行匹配: for (WebxComponent component : this) { if (component == defaultComponent) { continue; } String componentPath = component.getComponentPath(); if (!path.startsWith(componentPath)) { continue; } // path刚好等于componentPath,或者path以componentPath/为前缀 if (path.length() == componentPath.length() || path.charAt(componentPath.length()) == '/') { matched = component; break; } } webx在Spring启动的时候,通过对文件目录的扫描,装配进符合规则的组件Component,比如xxx.module.screen下的类,来和url做匹配。找到对应的component后: served = component.getWebxController().service(requestContext); 然后就到了webx的pipeline处理流程: public boolean service(RequestContext requestContext) throws Exception { PipelineInvocationHandle handle = pipeline.newInvocation(); handle.invoke(); // 假如pipeline被中断,则视作请求未被处理。filter将转入chain中继续处理请求。 return !handle.isBroken(); } pipeline 同样是链式模式,会依次调用注册的valve。常用的valve可能包括url检查、登录检查、csrfToken校验等,同样pipeline中的一些条件语句比如Choose、Loop等同样也是一个valve,调用其invoke方法其实也是生成了一个新的pipeline并执行invoke。比如这样一个when-otherwise语句中就会有如下代码: if (!satisfied && otherwiseBlock != null) { otherwiseBlock.newInvocation(pipelineContext).invoke(); } 此外,比较重要的valve就是负责处理表单的PerformActionVavle、负责执行screen的PerformScreenValve。 因为screen在应用中经常是一个接口的入口,并最终调用到业务逻辑部分的代码。所以这里我们分析一下PerformScreenValve这个阀门是如何调用到我们的业务逻辑代码的。screenValve的invoke最终调用了performScreenModule函数。显示依据规则找到了我们的module,然后执行了execute方法: Module module = finder.getScreenModule(); ... module.execute(); 然后调用了DataBindingAdapter的execute方法: private final MethodInvoker executeMethod; ... executeMethod.invoke(moduleObject, log); 在MethodInvoker这个类里面,我们可以看到装配execute方法参数的过程。 首先,在扫描类中注解的时候,webX会为每一个execute方法中有@Param注解的参数生成一个DataResolver。MethodInvoker中会有如下代码获取参数,最终会调用FastMethod的invoke方法。 Object[] args = new Object[resolvers.length]; for (int i = 0; i < args.length; i++) { Object value; value = resolvers[i].resolve(); } ... fastMethod.invoke(moduleObject, args); 最后就是反射调用的过程了,我们的接口object即为moduleObject,而args即为带有@Param注解的参数。最终的整体调用流程如下图: 项目分层 关于web层、service层、manager层与DAO层 执行到我们发布的接口后,再来简单的说一下web项目常见分层手段。 在我们的项目中,常见的是依次调用web层,service层,manager,dao层。数据实体在VO、DTO、DO之间流转。 因为在执行某个业务的时候,可能需要查询多个表的数据并进行拼接。比如现在需要这样一条数据D,由A,B,C组成。如果仅仅提供一个方法查询A、B、C并拼接为D返回,那么下次如果又有一个需求为E(A+B)的数据,无疑又要重新写查询A和B的代码并拼接。 这样,我们把代码分层,使各部分职责独立。其中service负责拼接,manager负责查询,这样不同的service可以对manager进行复用。同时,由于DTO和DO不是完全对应,查询的时候往往要把DTO转换成DO查询条件去调用DAO,而DAO返回的结果DO又和web层所需要的数据格式不一致,所以常常要转换成DTO。 manager的作用,就是处理DTO到DO,然后DAO生成DO再到DTO的过程。如下就是一个manager: BmsPurchaseOrderQuery purchaseOrderQuery = new BmsPurchaseOrderQuery(); purchaseOrderQuery.setPurchaseCode(purchaseCode); List<BmsPurchaseOrderDO> orderDOList = bmsPurchaseOrderDAO.select(purchaseOrderQuery); Assert.notEmpty(orderDOList); return orderDOList.get(0); 假设查询了订单的数据。然后再来一个manger查询订单明细的数据: @Override public List<BmsPurchaseOrderTransOutInfoDO> getTranOutInfoListByCode(String subscribeCode) { //查询子单明细 BmsPurchaseOrderTransOutInfoDO transOutInfoQuery = new BmsPurchaseOrderTransOutInfoDO(); transOutInfoQuery.setSubscribeCode(subscribeCode); List<BmsPurchaseOrderTransOutInfoDO> transOutInfoDOList = bmsPurchaseOrderTransOutInfoDAO.select(transOutInfoQuery); Assert.notNull(transOutInfoDOList); return transOutInfoDOList; } 然后,在service对订单和明细进行拼接。 总而言之,分层如下: web层(也就是webX中的screen):处理表单,调用服务,返回结果 service层:业务逻辑,数据拼接 manager层:粒度比较细的查询 dao层:sql语句映射 分层可能会导致代码量增大,但是可以提高程序的可复用性,方便程序维护。 总结 至此为止,关于webx服务的请求处理的来龙去脉简单的分析了一遍。了解大致处理流程还是比较有必要的,这样当我们的程序出现异常,可以及时的定位问题。当打断点时,面对层层的调用栈也不会显得不知所措。当然,在这里也是对自己经常接触到的技术做一个简单的沉淀。

资源下载

更多资源
Mario

Mario

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

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

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

用户登录
用户注册