springcloud Zuul動(dòng)態(tài)路由的實(shí)現(xiàn)
前言
Zuul 是Netflix 提供的一個(gè)開(kāi)源組件,致力于在云平臺(tái)上提供動(dòng)態(tài)路由,監(jiān)控,彈性,安全等邊緣服務(wù)的框架。也有很多公司使用它來(lái)作為網(wǎng)關(guān)的重要組成部分,碰巧今年公司的架構(gòu)組決定自研一個(gè)網(wǎng)關(guān)產(chǎn)品,集動(dòng)態(tài)路由,動(dòng)態(tài)權(quán)限,限流配額等功能為一體,為其他部門(mén)的項(xiàng)目提供統(tǒng)一的外網(wǎng)調(diào)用管理,最終形成產(chǎn)品(這方面阿里其實(shí)已經(jīng)有成熟的網(wǎng)關(guān)產(chǎn)品了,但是不太適用于個(gè)性化的配置,也沒(méi)有集成權(quán)限和限流降級(jí))。
不過(guò)這里并不想介紹整個(gè)網(wǎng)關(guān)的架構(gòu),而是想著重于討論其中的一個(gè)關(guān)鍵點(diǎn),并且也是經(jīng)常在交流群中聽(tīng)人說(shuō)起的:動(dòng)態(tài)路由怎么做?
再闡釋什么是動(dòng)態(tài)路由之前,需要介紹一下架構(gòu)的設(shè)計(jì)。
傳統(tǒng)互聯(lián)網(wǎng)架構(gòu)圖
上圖是沒(méi)有網(wǎng)關(guān)參與的一個(gè)最典型的互聯(lián)網(wǎng)架構(gòu)(本文中統(tǒng)一使用book代表應(yīng)用實(shí)例,即真正提供服務(wù)的一個(gè)業(yè)務(wù)系統(tǒng))
加入eureka的架構(gòu)圖
book注冊(cè)到eureka注冊(cè)中心中,zuul本身也連接著同一個(gè)eureka,可以拉取book眾多實(shí)例的列表。服務(wù)中心的注冊(cè)發(fā)現(xiàn)一直是值得推崇的一種方式,但是不適用與網(wǎng)關(guān)產(chǎn)品。因?yàn)槲覀兊木W(wǎng)關(guān)是面向眾多的其他部門(mén)的已有或是異構(gòu)架構(gòu)的系統(tǒng),不應(yīng)該強(qiáng)求其他系統(tǒng)都使用eureka,這樣是有侵入性的設(shè)計(jì)。
最終架構(gòu)圖
要強(qiáng)調(diào)的一點(diǎn)是,gateway最終也會(huì)部署多個(gè)實(shí)例,達(dá)到分布式的效果,在架構(gòu)圖中沒(méi)有畫(huà)出,請(qǐng)大家自行腦補(bǔ)。
本博客的示例使用最后一章架構(gòu)圖為例,帶來(lái)動(dòng)態(tài)路由的實(shí)現(xiàn)方式,會(huì)有具體的代碼。
動(dòng)態(tài)路由
動(dòng)態(tài)路由需要達(dá)到可持久化配置,動(dòng)態(tài)刷新的效果。如架構(gòu)圖所示,不僅要能滿(mǎn)足從spring的配置文件properties加載路由信息,還需要從數(shù)據(jù)庫(kù)加載我們的配置。另外一點(diǎn)是,路由信息在容器啟動(dòng)時(shí)就已經(jīng)加載進(jìn)入了內(nèi)存,我們希望配置完成后,實(shí)施發(fā)布,動(dòng)態(tài)刷新內(nèi)存中的路由信息,達(dá)到不停機(jī)維護(hù)路由信息的效果。
zuul–HelloWorldDemo
項(xiàng)目結(jié)構(gòu)
<groupId>com.sinosoft</groupId> <artifactId>zuul-gateway-demo</artifactId> <packaging>pom</packaging> <version>1.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> </parent> <modules> <module>gateway</module> <module>book</module> </modules> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
tip:springboot-1.5.2對(duì)應(yīng)的springcloud的版本需要使用Camden.SR6,一開(kāi)始想專(zhuān)門(mén)寫(xiě)這個(gè)demo時(shí),只替換了springboot的版本1.4.0->1.5.2,結(jié)果啟動(dòng)就報(bào)錯(cuò)了,最后發(fā)現(xiàn)是版本不兼容的鍋。
gateway項(xiàng)目:
啟動(dòng)類(lèi):GatewayApplication.java
@EnableZuulProxy @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
配置:application.properties
#配置在配置文件中的路由信息 zuul.routes.books.url=http://localhost:8090 zuul.routes.books.path=/books/** #不使用注冊(cè)中心,會(huì)帶來(lái)侵入性 ribbon.eureka.enabled=false #網(wǎng)關(guān)端口 server.port=8080
book項(xiàng)目:
啟動(dòng)類(lèi):BookApplication.java
@RestController @SpringBootApplication public class BookApplication { @RequestMapping(value = "/available") public String available() { System.out.println("Spring in Action"); return "Spring in Action"; } @RequestMapping(value = "/checked-out") public String checkedOut() { return "Spring Boot in Action"; } public static void main(String[] args) { SpringApplication.run(BookApplication.class, args); } }
配置類(lèi):application.properties
server.port=8090
測(cè)試訪問(wèn):http://localhost:8080/books/available
上述demo是一個(gè)簡(jiǎn)單的靜態(tài)路由,簡(jiǎn)單看下源碼,zuul是怎么做到轉(zhuǎn)發(fā),路由的。
@Configuration @EnableConfigurationProperties({ ZuulProperties.class }) @ConditionalOnClass(ZuulServlet.class) @Import(ServerPropertiesAutoConfiguration.class) public class ZuulConfiguration { @Autowired //zuul的配置文件,對(duì)應(yīng)了application.properties中的配置信息 protected ZuulProperties zuulProperties; @Autowired protected ServerProperties server; @Autowired(required = false) private ErrorController errorController; @Bean public HasFeatures zuulFeature() { return HasFeatures.namedFeature("Zuul (Simple)", ZuulConfiguration.class); } //核心類(lèi),路由定位器,最最重要 @Bean @ConditionalOnMissingBean(RouteLocator.class) public RouteLocator routeLocator() { //默認(rèn)配置的實(shí)現(xiàn)是SimpleRouteLocator.class return new SimpleRouteLocator(this.server.getServletPrefix(), this.zuulProperties); } //zuul的控制器,負(fù)責(zé)處理鏈路調(diào)用 @Bean public ZuulController zuulController() { return new ZuulController(); } //MVC HandlerMapping that maps incoming request paths to remote services. @Bean public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) { ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController()); mapping.setErrorController(this.errorController); return mapping; } //注冊(cè)了一個(gè)路由刷新監(jiān)聽(tīng)器,默認(rèn)實(shí)現(xiàn)是ZuulRefreshListener.class,這個(gè)是我們動(dòng)態(tài)路由的關(guān)鍵 @Bean public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() { return new ZuulRefreshListener(); } @Bean @ConditionalOnMissingBean(name = "zuulServlet") public ServletRegistrationBean zuulServlet() { ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(), this.zuulProperties.getServletPattern()); // The whole point of exposing this servlet is to provide a route that doesn't // buffer requests. servlet.addInitParameter("buffer-requests", "false"); return servlet; } // pre filters @Bean public ServletDetectionFilter servletDetectionFilter() { return new ServletDetectionFilter(); } @Bean public FormBodyWrapperFilter formBodyWrapperFilter() { return new FormBodyWrapperFilter(); } @Bean public DebugFilter debugFilter() { return new DebugFilter(); } @Bean public Servlet30WrapperFilter servlet30WrapperFilter() { return new Servlet30WrapperFilter(); } // post filters @Bean public SendResponseFilter sendResponseFilter() { return new SendResponseFilter(); } @Bean public SendErrorFilter sendErrorFilter() { return new SendErrorFilter(); } @Bean public SendForwardFilter sendForwardFilter() { return new SendForwardFilter(); } @Configuration protected static class ZuulFilterConfiguration { @Autowired private Map<String, ZuulFilter> filters; @Bean public ZuulFilterInitializer zuulFilterInitializer() { return new ZuulFilterInitializer(this.filters); } } //上面提到的路由刷新監(jiān)聽(tīng)器 private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> { @Autowired private ZuulHandlerMapping zuulHandlerMapping; private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor(); @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextRefreshedEvent || event instanceof RefreshScopeRefreshedEvent || event instanceof RoutesRefreshedEvent) { //設(shè)置為臟,下一次匹配到路徑時(shí),如果發(fā)現(xiàn)為臟,則會(huì)去刷新路由信息 this.zuulHandlerMapping.setDirty(true); } else if (event instanceof HeartbeatEvent) { if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) { this.zuulHandlerMapping.setDirty(true); } } } } }
我們要解決動(dòng)態(tài)路由的難題,第一步就得理解路由定位器的作用。
很失望,因?yàn)閺慕涌陉P(guān)系來(lái)看,spring考慮到了路由刷新的需求,但是默認(rèn)實(shí)現(xiàn)的SimpleRouteLocator沒(méi)有實(shí)現(xiàn)RefreshableRouteLocator接口,看來(lái)我們只能借鑒DiscoveryClientRouteLocator去改造SimpleRouteLocator使其具備刷新能力。
public interface RefreshableRouteLocator extends RouteLocator { void refresh(); }
DiscoveryClientRouteLocator比SimpleRouteLocator多了兩個(gè)功能,第一是從DiscoveryClient(如Eureka)發(fā)現(xiàn)路由信息,之前的架構(gòu)圖已經(jīng)給大家解釋清楚了,我們不想使用eureka這種侵入式的網(wǎng)關(guān)模塊,所以忽略它,第二是實(shí)現(xiàn)了RefreshableRouteLocator接口,能夠?qū)崿F(xiàn)動(dòng)態(tài)刷新。
對(duì)SimpleRouteLocator.class的源碼加一些注釋?zhuān)奖愦蠹议喿x:
public class SimpleRouteLocator implements RouteLocator { //配置文件中的路由信息配置 private ZuulProperties properties; //路徑正則配置器,即作用于path:/books/** private PathMatcher pathMatcher = new AntPathMatcher(); private String dispatcherServletPath = "/"; private String zuulServletPath; private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>(); public SimpleRouteLocator(String servletPath, ZuulProperties properties) { this.properties = properties; if (servletPath != null && StringUtils.hasText(servletPath)) { this.dispatcherServletPath = servletPath; } this.zuulServletPath = properties.getServletPath(); } //路由定位器和其他組件的交互,是最終把定位的Routes以list的方式提供出去,核心實(shí)現(xiàn) @Override public List<Route> getRoutes() { if (this.routes.get() == null) { this.routes.set(locateRoutes()); } List<Route> values = new ArrayList<>(); for (String url : this.routes.get().keySet()) { ZuulRoute route = this.routes.get().get(url); String path = route.getPath(); values.add(getRoute(route, path)); } return values; } @Override public Collection<String> getIgnoredPaths() { return this.properties.getIgnoredPatterns(); } //這個(gè)方法在網(wǎng)關(guān)產(chǎn)品中也很重要,可以根據(jù)實(shí)際路徑匹配到Route來(lái)進(jìn)行業(yè)務(wù)邏輯的操作,進(jìn)行一些加工 @Override public Route getMatchingRoute(final String path) { if (log.isDebugEnabled()) { log.debug("Finding route for path: " + path); } if (this.routes.get() == null) { this.routes.set(locateRoutes()); } if (log.isDebugEnabled()) { log.debug("servletPath=" + this.dispatcherServletPath); log.debug("zuulServletPath=" + this.zuulServletPath); log.debug("RequestUtils.isDispatcherServletRequest()=" + RequestUtils.isDispatcherServletRequest()); log.debug("RequestUtils.isZuulServletRequest()=" + RequestUtils.isZuulServletRequest()); } String adjustedPath = adjustPath(path); ZuulRoute route = null; if (!matchesIgnoredPatterns(adjustedPath)) { for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) { String pattern = entry.getKey(); log.debug("Matching pattern:" + pattern); if (this.pathMatcher.match(pattern, adjustedPath)) { route = entry.getValue(); break; } } } if (log.isDebugEnabled()) { log.debug("route matched=" + route); } return getRoute(route, adjustedPath); } private Route getRoute(ZuulRoute route, String path) { if (route == null) { return null; } String targetPath = path; String prefix = this.properties.getPrefix(); if (path.startsWith(prefix) && this.properties.isStripPrefix()) { targetPath = path.substring(prefix.length()); } if (route.isStripPrefix()) { int index = route.getPath().indexOf("*") - 1; if (index > 0) { String routePrefix = route.getPath().substring(0, index); targetPath = targetPath.replaceFirst(routePrefix, ""); prefix = prefix + routePrefix; } } Boolean retryable = this.properties.getRetryable(); if (route.getRetryable() != null) { retryable = route.getRetryable(); } return new Route(route.getId(), targetPath, route.getLocation(), prefix, retryable, route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null); } //注意這個(gè)類(lèi)并沒(méi)有實(shí)現(xiàn)refresh接口,但是卻提供了一個(gè)protected級(jí)別的方法,旨在讓子類(lèi)不需要重復(fù)維護(hù)一個(gè)private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();也可以達(dá)到刷新的效果 protected void doRefresh() { this.routes.set(locateRoutes()); } //具體就是在這兒定位路由信息的,我們之后從數(shù)據(jù)庫(kù)加載路由信息,主要也是從這兒改寫(xiě) /** * Compute a map of path pattern to route. The default is just a static map from the * {@link ZuulProperties}, but subclasses can add dynamic calculations. */ protected Map<String, ZuulRoute> locateRoutes() { LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>(); for (ZuulRoute route : this.properties.getRoutes().values()) { routesMap.put(route.getPath(), route); } return routesMap; } protected boolean matchesIgnoredPatterns(String path) { for (String pattern : this.properties.getIgnoredPatterns()) { log.debug("Matching ignored pattern:" + pattern); if (this.pathMatcher.match(pattern, path)) { log.debug("Path " + path + " matches ignored pattern " + pattern); return true; } } return false; } private String adjustPath(final String path) { String adjustedPath = path; if (RequestUtils.isDispatcherServletRequest() && StringUtils.hasText(this.dispatcherServletPath)) { if (!this.dispatcherServletPath.equals("/")) { adjustedPath = path.substring(this.dispatcherServletPath.length()); log.debug("Stripped dispatcherServletPath"); } } else if (RequestUtils.isZuulServletRequest()) { if (StringUtils.hasText(this.zuulServletPath) && !this.zuulServletPath.equals("/")) { adjustedPath = path.substring(this.zuulServletPath.length()); log.debug("Stripped zuulServletPath"); } } else { // do nothing } log.debug("adjustedPath=" + path); return adjustedPath; } }
重寫(xiě)過(guò)后的自定義路由定位器如下:
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{ public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class); private JdbcTemplate jdbcTemplate; private ZuulProperties properties; public void setJdbcTemplate(JdbcTemplate jdbcTemplate){ this.jdbcTemplate = jdbcTemplate; } public CustomRouteLocator(String servletPath, ZuulProperties properties) { super(servletPath, properties); this.properties = properties; logger.info("servletPath:{}",servletPath); } //父類(lèi)已經(jīng)提供了這個(gè)方法,這里寫(xiě)出來(lái)只是為了說(shuō)明這一個(gè)方法很重要?。?! // @Override // protected void doRefresh() { // super.doRefresh(); // } @Override public void refresh() { doRefresh(); } @Override protected Map<String, ZuulRoute> locateRoutes() { LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>(); //從application.properties中加載路由信息 routesMap.putAll(super.locateRoutes()); //從db中加載路由信息 routesMap.putAll(locateRoutesFromDB()); //優(yōu)化一下配置 LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>(); for (Map.Entry<String, ZuulRoute> entry : routesMap.entrySet()) { String path = entry.getKey(); // Prepend with slash if not already present. if (!path.startsWith("/")) { path = "/" + path; } if (StringUtils.hasText(this.properties.getPrefix())) { path = this.properties.getPrefix() + path; if (!path.startsWith("/")) { path = "/" + path; } } values.put(path, entry.getValue()); } return values; } private Map<String, ZuulRoute> locateRoutesFromDB(){ Map<String, ZuulRoute> routes = new LinkedHashMap<>(); List<ZuulRouteVO> results = jdbcTemplate.query("select * from gateway_api_define where enabled = true ",new BeanPropertyRowMapper<>(ZuulRouteVO.class)); for (ZuulRouteVO result : results) { if(org.apache.commons.lang3.StringUtils.isBlank(result.getPath()) || org.apache.commons.lang3.StringUtils.isBlank(result.getUrl()) ){ continue; } ZuulRoute zuulRoute = new ZuulRoute(); try { org.springframework.beans.BeanUtils.copyProperties(result,zuulRoute); } catch (Exception e) { logger.error("=============load zuul route info from db with error==============",e); } routes.put(zuulRoute.getPath(),zuulRoute); } return routes; } public static class ZuulRouteVO { /** * The ID of the route (the same as its map key by default). */ private String id; /** * The path (pattern) for the route, e.g. /foo/**. */ private String path; /** * The service ID (if any) to map to this route. You can specify a physical URL or * a service, but not both. */ private String serviceId; /** * A full physical URL to map to the route. An alternative is to use a service ID * and service discovery to find the physical address. */ private String url; /** * Flag to determine whether the prefix for this route (the path, minus pattern * patcher) should be stripped before forwarding. */ private boolean stripPrefix = true; /** * Flag to indicate that this route should be retryable (if supported). Generally * retry requires a service ID and ribbon. */ private Boolean retryable; private Boolean enabled; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getServiceId() { return serviceId; } public void setServiceId(String serviceId) { this.serviceId = serviceId; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public boolean isStripPrefix() { return stripPrefix; } public void setStripPrefix(boolean stripPrefix) { this.stripPrefix = stripPrefix; } public Boolean getRetryable() { return retryable; } public void setRetryable(Boolean retryable) { this.retryable = retryable; } public Boolean getEnabled() { return enabled; } public void setEnabled(Boolean enabled) { this.enabled = enabled; } } }
配置這個(gè)自定義的路由定位器:
@Configuration public class CustomZuulConfig { @Autowired ZuulProperties zuulProperties; @Autowired ServerProperties server; @Autowired JdbcTemplate jdbcTemplate; @Bean public CustomRouteLocator routeLocator() { CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServletPrefix(), this.zuulProperties); routeLocator.setJdbcTemplate(jdbcTemplate); return routeLocator; } }
現(xiàn)在容器啟動(dòng)時(shí),就可以從數(shù)據(jù)庫(kù)和配置文件中一起加載路由信息了,離動(dòng)態(tài)路由還差最后一步,就是實(shí)時(shí)刷新,前面已經(jīng)說(shuō)過(guò)了,默認(rèn)的ZuulConfigure已經(jīng)配置了事件監(jiān)聽(tīng)器,我們只需要發(fā)送一個(gè)事件就可以實(shí)現(xiàn)刷新了。
public class RefreshRouteService { @Autowired ApplicationEventPublisher publisher; @Autowired RouteLocator routeLocator; public void refreshRoute() { RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator); publisher.publishEvent(routesRefreshedEvent); } }
具體的刷新流程其實(shí)就是從數(shù)據(jù)庫(kù)重新加載了一遍,有人可能會(huì)問(wèn),為什么不自己是手動(dòng)重新加載Locator.dorefresh?非要用事件去刷新。這牽扯到內(nèi)部的zuul內(nèi)部組件的工作流程,不僅僅是Locator本身的一個(gè)變量,具體想要了解的還得去看源碼。
到這兒我們就實(shí)現(xiàn)了動(dòng)態(tài)路由了,所以的實(shí)例代碼和建表語(yǔ)句我會(huì)放到github上,下載的時(shí)候記得給我star QAQ ?。?!
鏈接:https://github.com/lexburner/zuul-gateway-demo
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Spring Cloud 網(wǎng)關(guān)服務(wù) zuul 動(dòng)態(tài)路由的實(shí)現(xiàn)方法
- SpringCloud Zuul實(shí)現(xiàn)動(dòng)態(tài)路由
- 詳解Spring Cloud Zuul中路由配置細(xì)節(jié)
- SpringCloud實(shí)戰(zhàn)小貼士之Zuul的路徑匹配
- 利用Spring Cloud Zuul實(shí)現(xiàn)動(dòng)態(tài)路由示例代碼
- Spring Cloud OAuth2 實(shí)現(xiàn)用戶(hù)認(rèn)證及單點(diǎn)登錄的示例代碼
- Spring Cloud Zuul路由規(guī)則動(dòng)態(tài)更新解析
相關(guān)文章
Java swing讀取txt文件實(shí)現(xiàn)學(xué)生考試系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java swing讀取txt文件實(shí)現(xiàn)學(xué)生考試系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06利用javadoc注釋自動(dòng)生成Swagger注解
由于現(xiàn)在controller方法上面沒(méi)有swagger注解,只能拿到接口url地址,無(wú)法獲得接口功能描述,所以本文為大家介紹一下如何利用javadoc注釋自動(dòng)生成Swagger注解,感興趣的可以了解下2023-08-08java 靜態(tài)工廠代替多參構(gòu)造器的適用情況與優(yōu)劣
這篇文章主要介紹了java 靜態(tài)工廠代替多參構(gòu)造器的優(yōu)劣,幫助大家更好的理解和使用靜態(tài)工廠方法,感興趣的朋友可以了解下2020-12-12完全解析Java編程中finally語(yǔ)句的執(zhí)行原理
這篇文章主要深度介紹了Java編程中finally語(yǔ)句的執(zhí)行原理,細(xì)致講解了finally在異常處理中的流程控制作用,需要的朋友可以參考下2015-11-11Atomikos + MybatisPlus解決多數(shù)據(jù)源事務(wù)一致性問(wèn)題解決
在實(shí)際項(xiàng)目的開(kāi)發(fā)過(guò)程中,我們經(jīng)常會(huì)遇到在同一個(gè)項(xiàng)目或微服務(wù)中牽涉到使用兩個(gè)或多個(gè)數(shù)據(jù)源的,本文主要介紹了Atomikos + MybatisPlus解決多數(shù)據(jù)源事務(wù)一致性問(wèn)題解決,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07Spring聲明式事務(wù)注解之@EnableTransactionManagement解析
這篇文章主要介紹了Spring聲明式事務(wù)注解之@EnableTransactionManagement解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08java 查詢(xún)oracle數(shù)據(jù)庫(kù)所有表DatabaseMetaData的用法(詳解)
下面小編就為大家?guī)?lái)一篇java 查詢(xún)oracle數(shù)據(jù)庫(kù)所有表DatabaseMetaData的用法(詳解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-11-11