springboot+zuul实现自定义过滤器、动态路由、动…
2018-09-18 06:36:00来源:博客园 阅读 ()
参考:https://blog.csdn.net/u014091123/article/details/75433656
https://blog.csdn.net/u013815546/article/details/68944039
Zuul是Netflix开源的微服务网关,他的核心是一系列的过滤器,通过这些过滤器我们可以轻松的实现服务的访问认证、限流、路由、负载、熔断等功能。
基于对已有项目代码零侵入的需求,本文没有将zuul网关项目注册到eureka中心,而是将zuul与springboot结合作为一个独立的项目进行请求转发,因此本项目是非spring cloud架构。
开始编写zuul网关项目
首先,新建一个spring boot项目。加入zuul依赖,开启@EnableZuulProxy注解。
pom.xml
1 <dependency> 2 <groupId>org.springframework.cloud</groupId> 3 <artifactId>spring-cloud-starter-zuul</artifactId> 4 <version>1.4.4.RELEASE</version> 5 </dependency>
application.properties
1 server.port=8090 2 eureka.client.enable=false 3 zuul.ribbon.eager-load.enabled=true 4 5 zuul.SendErrorFilter.post.disable=true
由于后续会使用到动态路由,所以这里我们并不需要在application.properties中做网关地址转发映射。
SpringBootZuulApplication.java
1 package com.syher.zuul; 2 3 import com.google.common.util.concurrent.ThreadFactoryBuilder; 4 import com.syher.zuul.core.zuul.router.PropertiesRouter; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.boot.CommandLineRunner; 7 import org.springframework.boot.SpringApplication; 8 import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 10 import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent; 11 import org.springframework.cloud.netflix.zuul.filters.RouteLocator; 12 import org.springframework.context.ApplicationEventPublisher; 13 import org.springframework.context.annotation.ComponentScan; 14 15 import java.io.File; 16 import java.util.concurrent.Executors; 17 import java.util.concurrent.ScheduledExecutorService; 18 import java.util.concurrent.TimeUnit; 19 20 /** 21 * @author braska 22 * @date 2018/06/25. 23 **/ 24 @EnableAutoConfiguration 25 @EnableZuulProxy 26 @ComponentScan(basePackages = { 27 "com.syher.zuul.core", 28 "com.syher.zuul.service" 29 }) 30 public class SpringBootZuulApplication implements CommandLineRunner { 31 @Autowired 32 ApplicationEventPublisher publisher; 33 @Autowired 34 RouteLocator routeLocator; 35 36 private ScheduledExecutorService executor; 37 private Long lastModified = 0L; 38 private boolean instance = true; 39 40 public static void main(String[] args) { 41 SpringApplication.run(SpringBootZuulApplication.class, args); 42 } 43 44 @Override 45 public void run(String... args) throws Exception { 46 executor = Executors.newSingleThreadScheduledExecutor( 47 new ThreadFactoryBuilder().setNameFormat("properties read.").build() 48 ); 49 executor.scheduleWithFixedDelay(() -> publish(), 0, 1, TimeUnit.SECONDS); 50 } 51 52 private void publish() { 53 if (isPropertiesModified()) { 54 publisher.publishEvent(new RoutesRefreshedEvent(routeLocator)); 55 } 56 } 57 58 private boolean isPropertiesModified() { 59 File file = new File(this.getClass().getClassLoader().getResource(PropertiesRouter.PROPERTIES_FILE).getPath()); 60 if (instance) { 61 instance = false; 62 return false; 63 } 64 if (file.lastModified() > lastModified) { 65 lastModified = file.lastModified(); 66 return true; 67 } 68 return false; 69 } 70 }
一、自定义过滤器
自定义zuul过滤器比较简单。我们先讲过滤器。
zuul过滤器分为pre、route、post、error四种类型。作用我就不详细讲了,网上资料一大把。本文主要写路由前的过滤,即pre类型。
要自定义一个过滤器,只需要要继承ZuulFilter,然后指定过滤类型、过滤顺序、是否执行这个过滤器、过滤内容就OK了。
为了便于扩展,这里用到了模板模式。
AbstractZuulFilter.java
1 package com.syher.zuul.core.zuul.filter; 2 3 import com.netflix.zuul.ZuulFilter; 4 import com.netflix.zuul.context.RequestContext; 5 import com.syher.zuul.core.zuul.ContantValue; 6 7 /** 8 * @author braska 9 * @date 2018/06/29. 10 **/ 11 public abstract class AbstractZuulFilter extends ZuulFilter { 12 13 protected RequestContext context; 14 15 @Override 16 public boolean shouldFilter() { 17 RequestContext ctx = RequestContext.getCurrentContext(); 18 return (boolean) (ctx.getOrDefault(ContantValue.NEXT_FILTER, true)); 19 } 20 21 @Override 22 public Object run() { 23 context = RequestContext.getCurrentContext(); 24 return doRun(); 25 } 26 27 public abstract Object doRun(); 28 29 public Object fail(Integer code, String message) { 30 context.set(ContantValue.NEXT_FILTER, false); 31 context.setSendZuulResponse(false); 32 context.getResponse().setContentType("text/html;charset=UTF-8"); 33 context.setResponseStatusCode(code); 34 context.setResponseBody(String.format("{\"result\":\"%s!\"}", message)); 35 return null; 36 } 37 38 public Object success() { 39 context.set(ContantValue.NEXT_FILTER, true); 40 return null; 41 } 42 }
定义preFilter的抽象类,继承AbstractZuulFilter。指定pre类型,之后所有的pre过滤器都可以继承这个抽象类。
AbstractPreZuulFilter.java
1 package com.syher.zuul.core.zuul.filter.pre; 2 3 import com.syher.zuul.core.zuul.FilterType; 4 import com.syher.zuul.core.zuul.filter.AbstractZuulFilter; 5 6 /** 7 * @author braska 8 * @date 2018/06/29. 9 **/ 10 public abstract class AbstractPreZuulFilter extends AbstractZuulFilter { 11 @Override 12 public String filterType() { 13 return FilterType.pre.name(); 14 } 15 }
接着编写具体一个具体的过滤器,比如限流。
RateLimiterFilter.java
1 package com.syher.zuul.core.zuul.filter.pre; 2 3 import com.google.common.util.concurrent.RateLimiter; 4 import com.syher.zuul.core.zuul.FilterOrder; 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 8 import javax.servlet.http.HttpServletRequest; 9 10 /** 11 * @author braska 12 * @date 2018/06/29. 13 **/ 14 public class RateLimiterFilter extends AbstractPreZuulFilter { 15 16 private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterFilter.class); 17 18 /** 19 * 每秒允许处理的量是50 20 */ 21 RateLimiter rateLimiter = RateLimiter.create(50); 22 23 @Override 24 public int filterOrder() { 25 return FilterOrder.RATE_LIMITER_ORDER; 26 } 27 28 @Override 29 public Object doRun() { 30 HttpServletRequest request = context.getRequest(); 31 String url = request.getRequestURI(); 32 if (rateLimiter.tryAcquire()) { 33 return success(); 34 } else { 35 LOGGER.info("rate limit:{}", url); 36 return fail(401, String.format("rate limit:{}", url)); 37 } 38 } 39 }
其他类型的过滤器也一样。创建不同的抽象类,比如AbstractPostZuulFilter,指定filterType,然后具体的postFilter只要继承该抽象类即可。
最后,将过滤器托管给spring。
ZuulConfigure.java
1 package com.syher.zuul.core.config; 2 3 import com.netflix.loadbalancer.IRule; 4 import com.netflix.zuul.ZuulFilter; 5 import com.syher.zuul.core.ribbon.ServerLoadBalancerRule; 6 import com.syher.zuul.core.zuul.filter.pre.RateLimiterFilter; 7 import com.syher.zuul.core.zuul.filter.pre.TokenAccessFilter; 8 import com.syher.zuul.core.zuul.filter.pre.UserRightFilter; 9 import com.syher.zuul.core.zuul.router.PropertiesRouter; 10 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.boot.autoconfigure.web.ServerProperties; 12 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 13 import org.springframework.context.annotation.Bean; 14 import org.springframework.context.annotation.Configuration; 15 16 /** 17 * @author braska 18 * @date 2018/07/05. 19 **/ 20 @Configuration 21 public class ZuulConfigure { 22 23 @Autowired 24 ZuulProperties zuulProperties; 25 @Autowired 26 ServerProperties server; 27 28 /** 29 * 动态路由 30 * @return 31 */ 32 @Bean 33 public PropertiesRouter propertiesRouter() { 34 return new PropertiesRouter(this.server.getServletPrefix(), this.zuulProperties); 35 } 36 37 /** 38 * 动态负载 39 * @return 40 */ 41 @Bean 42 public IRule loadBalance() { 43 return new ServerLoadBalancerRule(); 44 } 45 46 /** 47 * 自定义过滤器 48 * @return 49 */ 50 @Bean 51 public ZuulFilter rateLimiterFilter() { 52 return new RateLimiterFilter(); 53 } 54 }
二、动态路由
接着写动态路由。动态路由需要配置可持久化且能动态刷新。
zuul默认使用的路由是SimpleRouteLocator,不具备动态刷新的效果。DiscoveryClientRouteLocator具备刷新功能,但是需要已有的项目将服务注册到eureka,这不符合已有项目代码零侵入的需求所以排除。那么还有个办法就是自定义路由然后实现RefreshableRouteLocator类。
部分代码如下:
AbstractDynamicRouter.java
1 package com.syher.zuul.core.zuul.router; 2 3 import com.syher.zuul.core.zuul.entity.BasicRoute; 4 import org.apache.commons.lang.StringUtils; 5 import org.slf4j.Logger; 6 import org.slf4j.LoggerFactory; 7 import org.springframework.beans.BeanUtils; 8 import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator; 9 import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator; 10 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 11 12 import java.util.LinkedHashMap; 13 import java.util.List; 14 import java.util.Map; 15 16 /** 17 * @author braska 18 * @date 2018/07/02. 19 **/ 20 public abstract class AbstractDynamicRouter extends SimpleRouteLocator implements RefreshableRouteLocator { 21 22 private static final Logger LOGGER = LoggerFactory.getLogger(AbstractDynamicRouter.class); 23 24 public AbstractDynamicRouter(String servletPath, ZuulProperties properties) { 25 super(servletPath, properties); 26 } 27 28 @Override 29 public void refresh() { 30 doRefresh(); 31 } 32 33 @Override 34 protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() { 35 LinkedHashMap<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<String, ZuulProperties.ZuulRoute>(); 36 routes.putAll(super.locateRoutes()); 37 38 List<BasicRoute> results = readRoutes(); 39 40 for (BasicRoute result : results) { 41 if (StringUtils.isEmpty(result.getPath()) ) { 42 continue; 43 } 44 ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute(); 45 try { 46 BeanUtils.copyProperties(result, zuulRoute); 47 } catch (Exception e) { 48 LOGGER.error("=============load zuul route info from db with error==============", e); 49 } 50 routes.put(zuulRoute.getPath(), zuulRoute); 51 } 52 return routes; 53 } 54 55 /** 56 * 读取路由信息 57 * @return 58 */ 59 protected abstract List<BasicRoute> readRoutes(); 60 }
由于本人比较懒。不想每次写个demo都要重新配置一大堆数据库信息。所以本文很多数据比如路由信息、比如负载策略。要么写在文本里面,要么直接java代码构造。
本demo的路由信息就是从properties里面读取。嗯,继承AbstractDynamicRouter即可。
PropertiesRouter.java
1 package com.syher.zuul.core.zuul.router; 2 3 import com.google.common.collect.Lists; 4 import com.google.common.util.concurrent.ThreadFactoryBuilder; 5 import com.syher.zuul.common.Context; 6 import com.syher.zuul.core.zuul.entity.BasicRoute; 7 import org.apache.commons.lang.StringUtils; 8 import org.slf4j.Logger; 9 import org.slf4j.LoggerFactory; 10 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; 11 12 import java.io.File; 13 import java.io.IOException; 14 import java.util.HashMap; 15 import java.util.List; 16 import java.util.Map; 17 import java.util.Properties; 18 import java.util.concurrent.Executors; 19 import java.util.concurrent.ScheduledExecutorService; 20 import java.util.stream.Collectors; 21 22 /** 23 * @author braska 24 * @date 2018/07/02. 25 **/ 26 public class PropertiesRouter extends AbstractDynamicRouter { 27 28 private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesRouter.class); 29 public static final String PROPERTIES_FILE = "router.properties"; 30 private static final String ZUUL_ROUTER_PREFIX = "zuul.routes"; 31 32 33 public PropertiesRouter(String servletPath, ZuulProperties properties) { 34 super(servletPath, properties); 35 } 36 37 @Override 38 protected List<BasicRoute> readRoutes() { 39 List<BasicRoute> list = Lists.newArrayListWithExpectedSize(3); 40 try { 41 Properties prop = new Properties(); 42 prop.load( 43 this.getClass().getClassLoader().getResourceAsStream(PROPERTIES_FILE) 44 ); 45 46 Context context = new Context(new HashMap<>((Map) prop)); 47 Map<String, String> data = context.getSubProperties(ZUUL_ROUTER_PREFIX); 48 List<String> ids = data.keySet().stream().map(s -> s.substring(0, s.indexOf("."))).distinct().collect(Collectors.toList()); 49 ids.stream().forEach(id -> { 50 Map<String, String> router = context.getSubProperties(String.join(".", ZUUL_ROUTER_PREFIX, id)); 51 52 String path = router.get("path"); 53 path = path.startsWith("/") ? path : "/" + path; 54 55 String serviceId = router.getOrDefault("serviceId", null); 56 String url = router.getOrDefault("url", null); 57 58 BasicRoute basicRoute = new BasicRoute(); 59 basicRoute.setId(id); 60 basicRoute.setPath(path); 61 basicRoute.setUrl(router.getOrDefault("url", null)); 62 basicRoute.setServiceId((StringUtils.isBlank(url) && StringUtils.isBlank(serviceId)) ? id : serviceId); 63 basicRoute.setRetryable(Boolean.parseBoolean(router.getOrDefault("retry-able", "false"))); 64 basicRoute.setStripPrefix(Boolean.parseBoolean(router.getOrDefault("strip-prefix", "false"))); 65 list.add(basicRoute); 66 }); 67 } catch (IOException e) { 68 LOGGER.info("error to read " + PROPERTIES_FILE + " :{}", e); 69 } 70 return list; 71 } 72 }
既然是动态路由实时刷新,那肯定需要一个定时器定时监控properties文件。所以我在启动类SpringBootZuulApplication加了个定时器监控properties是否发生过变更(之前有疑问的现在可以解惑了)。一旦文件被修改过就重新发布一下, 然后会触发routeLocator的refresh方法。
1 public void publish() { 2 if (isPropertiesModified()) { 3 publisher.publishEvent(new RoutesRefreshedEvent(routeLocator)); 4 } 5 }
当然,如果是从数据库或者其他地方比如redis读取就不需要用到定时器,只要在增删改的时候直接publish就好了。
最后,记得PropertiesRouter类交由spring托管(在ZuulConfigure类中配置bean)。
router.properties文件:
1 zuul.routes.dashboard.path=/** 2 zuul.routes.dashboard.strip-prefix=true 3 4 ##不使用动态负载需指定url 5 ##zuul.routes.dashboard.url=http://localhost:9000/ 6 ##zuul服务部署后,动态增加网关映射,无需重启即可实时路由到新的网关 7 ##zuul.routes.baidu.path=/**
三、动态负载
负载也算比较简单,复杂点的是写负载算法。
动态负载主要分两个步骤:
1、根据网关项目配置的host和port去数据库(我是java直接造的数据)查找负载策略,比如轮询、比如随机、比如iphash等等。
2、根据策略结合每台服务器分配的权重选出合适的服务。
实现动态负载需要自定义rule类然后继承AbstractLoadBalancerRule类。
首先看负载策略的选择:
ServerLoadBalancerRule.java
1 package com.syher.zuul.core.ribbon; 2 3 import com.google.common.base.Preconditions; 4 import com.netflix.client.config.IClientConfig; 5 import com.netflix.loadbalancer.AbstractLoadBalancerRule; 6 import com.netflix.loadbalancer.ILoadBalancer; 7 import com.netflix.loadbalancer.Server; 8 import com.syher.zuul.common.util.SystemUtil; 9 import com.syher.zuul.core.ribbon.balancer.LoadBalancer; 10 import com.syher.zuul.core.ribbon.balancer.RandomLoadBalancer; 11 import com.syher.zuul.core.ribbon.balancer.RoundLoadBalancer; 12 import com.syher.zuul.entity.GatewayAddress; 13 import com.syher.zuul.service.GatewayService; 14 import org.apache.commons.lang.StringUtils; 15 import org.slf4j.Logger; 16 import org.slf4j.LoggerFactory; 17 import org.springframework.beans.factory.annotation.Autowired; 18 import org.springframework.beans.factory.annotation.Value; 19 20 /** 21 * @author braska 22 * @date 2018/07/05. 23 **/ 24 public class ServerLoadBalancerRule extends AbstractLoadBalancerRule { 25 26 private static final Logger LOGGER = LoggerFactory.getLogger(ServerLoadBalancerRule.class); 27 28 @Value("${server.host:127.0.0.1}") 29 private String host; 30 @Value("${server.port:8080}") 31 private Integer port; 32 33 @Autowired 34 private GatewayService gatewayService; 35 36 @Override 37 public void initWithNiwsConfig(IClientConfig iClientConfig) { 38 } 39 40 @Override 41 public Server choose(Object key) { 42 return getServer(getLoadBalancer(), key); 43 } 44 45 private Server getServer(ILoadBalancer loadBalancer, Object key) { 46 if (StringUtils.isBlank(host)) { 47 host = SystemUtil.ipList().get(0); 48 } 49 //Preconditions.checkArgument(host != null, "server.host must be specify."); 50 //Preconditions.checkArgument(port != null, "server.port must be specify."); 51 52 GatewayAddress address = gatewayService.getByHostAndPort(host, port); 53 if (address == null) { //这里的逻辑可以改,找不到网关配置信息可以指定默认的负载策略 54 LOGGER.error(String.format("must be config a gateway info for the server[%s:%s].", host, String.valueOf(port))); 55 return null; 56 } 57 58 LoadBalancer balancer = LoadBalancerFactory.build(address.getFkStrategyId()); 59 60 return balancer.chooseServer(loadBalancer); 61 } 62 63 static class LoadBalancerFactory { 64 65 public static LoadBalancer build(String strategy) { 66 GatewayAddress.StrategyType type = GatewayAddress.StrategyType.of(strategy); 67 switch (type) { 68 case ROUND: 69 return new RoundLoadBalancer(); 70 case RANDOM: 71 return new RandomLoadBalancer(); 72 default: 73 return null; 74 } 75 } 76 } 77 }
然后是负载算法接口代码。
LoadBalancer.java
1 package com.syher.zuul.core.ribbon.balancer; 2 3 import com.netflix.loadbalancer.ILoadBalancer; 4 import com.netflix.loadbalancer.Server; 5 6 /** 7 * @author braska 8 * @date 2018/07/06. 9 **/ 10 public interface LoadBalancer { 11 12 /** 13 * choose a loadBalancer 14 * @param loadBalancer 15 * @return 16 */ 17 Server chooseServer(ILoadBalancer loadBalancer); 18 }
定义抽象类,实现LoadBalancer接口
AbstractLoadBalancer.java
1 package com.syher.zuul.core.ribbon.balancer; 2 3 import com.netflix.loadbalancer.ILoadBalancer; 4 import com.netflix.loadbalancer.Server; 5 import com.syher.zuul.core.SpringContext; 6 import com.syher.zuul.service.ServerService; 7 import org.slf4j.Logger; 8 import org.slf4j.LoggerFactory; 9 10 /** 11 * @author braska 12 * @date 2018/07/06. 13 **/ 14 public abstract class AbstractLoadBalancer implements LoadBalancer { 15 private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLoadBalancer.class); 16 protected ServerService serverService; 17 18 @Override 19 public Server chooseServer(ILoadBalancer loadBalancer) { 20 this.serverService = SpringContext.getBean(ServerService.class); 21 Server server = choose(loadBalancer); 22 if (server != null) { 23 LOGGER.info(String.format("the server[%s:%s] has been select.", server.getHost(), server.getPort())); 24 } else { 25 LOGGER.error("could not find any server."); 26 } 27 return server; 28 } 29 30 public abstract Server choose(ILoadBalancer loadBalancer); 31 }
轮询负载算法
RoundLoadBalancer.java
1 package com.syher.zuul.core.ribbon.balancer; 2 3 import com.netflix.loadbalancer.ILoadBalancer; 4 import com.netflix.loadbalancer.Server; 5 import com.syher.zuul.common.Constant; 6 import com.syher.zuul.core.GlobalCache; 7 import com.syher.zuul.core.ribbon.LoadBalancerRuleUtil; 8 import com.syher.zuul.entity.ServerAddress; 9 10 import java.util.List; 11 12 /** 13 * 权重轮询 14 * 首次使用取最大权重的服务器。而后通过权重的不断递减,寻找适合的服务器。 15 * @author braska 16 * @date 2018/07/06. 17 **/ 18 public class RoundLoadBalancer extends AbstractLoadBalancer { 19 20 private Integer currentServer; 21 private Integer currentWeight; 22 private Integer maxWeight; 23 private Integer gcdWeight; 24 25 @Override 26 public Server choose(ILoadBalancer loadBalancer) { 27 List<ServerAddress> addressList = serverService.getAvailableServer(); 28 if (addressList != null && !addressList.isEmpty()) { 29 maxWeight = LoadBalancerRuleUtil.getMaxWeightForServers(addressList); 30 gcdWeight = LoadBalancerRuleUtil.getGCDForServers(addressList); 31 currentServer = Integer.parseInt(GlobalCache.instance().getOrDefault(Constant.CURRENT_SERVER_KEY, -1).toString()); 32 currentWeight = Integer.parseInt(GlobalCache.instance().getOrDefault(Constant.CURRENT_WEIGHT_KEY, 0).toString()); 33 34 Integer serverCount = addressList.size(); 35 36 if (1 == serverCount) { 37 return new Server(addressList.get(0).getHost(), addressList.get(0).getPort()); 38 } else { 39 while (true) { 40 currentServer = (currentServer + 1) % serverCount; 41 if (currentServer == 0) { 42 currentWeight = currentWeight - gcdWeight; 43 if (currentWeight <= 0) { 44 currentWeight = maxWeight; 45 if (currentWeight == 0) { 46 GlobalCache.instance().put(Constant.CURRENT_SERVER_KEY, currentServer); 47 GlobalCache.instance().put(Constant.CURRENT_WEIGHT_KEY, currentWeight); 48 Thread.yield(); 49 return null; 50 } 51 } 52 } 53 54 ServerAddress address = addressList.get(currentServer); 55 if (address.getWeight() >= currentWeight) { 56 GlobalCache.instance().put(Constant.CURRENT_SERVER_KEY, currentServer); 57 GlobalCache.instance().put(Constant.CURRENT_WEIGHT_KEY, currentWeight); 58 return new Server(address.getHost(), address.getPort()); 59 } 60 } 61 } 62 63 } 64 return null; 65 } 66 }
最后,ServerLoadBalancerRule交由spring托管。
至此,springboot+zuul实现自定义过滤器、动态路由、动态负载就都完成了。
源码:https://gitee.com/syher/spring-boot-project/tree/master/spring-boot-zuul
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
上一篇:JVM培训序幕篇
下一篇:Java基础(整理)
- DES/3DES/AES 三种对称加密算法实现 2020-06-11
- SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 后 2020-06-10
- Spring Boot 实现定时任务的 4 种方式 2020-06-10
- JSP+SSH+Mysql+DBCP实现的租车系统 2020-06-09
- Java实现的三种字符串反转 2020-06-09
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash