SpringCloud之Zuul源码解析

news/2024/7/7 18:40:53

Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Zuul 可以适当的对多个 Amazon Auto Scaling Groups 进行路由请求。
其架构如下图所示:

Zuul提供了一个框架,可以对过滤器进行动态的加载,编译,运行。过滤器之间没有直接的相互通信。他们是通过一个RequestContext的静态类来进行数据传递的。RequestContext类中有ThreadLocal变量来记录每个Request所需要传递的数据。
过滤器是由Groovy写成。这些过滤器文件被放在Zuul Server上的特定目录下面。Zuul会定期轮询这些目录。修改过的过滤器会动态的加载到Zuul Server中以便于request使用。

客户定制:比如我们可以定制一种STATIC类型的过滤器,用来模拟生成返回给客户的response。
过滤器的生命周期如下所示:

就像上图中所描述的一样,Zuul 提供了四种过滤器的 API,分别为前置(Pre)、后置(Post)、路由(Route)和错误(Error)四种处理方式。
一个请求会先按顺序通过所有的前置过滤器,之后在路由过滤器中转发给后端应用,得到响应后又会通过所有的后置过滤器,最后响应给客户端。在整个流程中如果发生了异常则会跳转到错误过滤器中。
一般来说,如果需要在请求到达后端应用前就进行处理的话,会选择前置过滤器,例如鉴权、请求转发、增加请求参数等行为。在请求完成后需要处理的操作放在后置过滤器中完成,例如统计返回值和调用时间、记录日志、增加跨域头等行为。路由过滤器一般只需要选择 Zuul 中内置的即可,错误过滤器一般只需要一个,这样可以在 Gateway 遇到错误逻辑时直接抛出异常中断流程,并直接统一处理返回结果。
Zuul可以通过加载动态过滤机制,从而实现以下各项功能:
验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。
审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。
动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。
负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。
除此之外,Netflix公司还利用Zuul的功能通过金丝雀版本实现精确路由与压力测试。

Ribbon客户端默认选择静态类HttpClientRibbonConfiguration创建的HttpClientRibbonCommandFactory工厂类创建,HTTP Client客户端选择HttpClientConfiguration中静态类ApacheHttpClientConfiguration创建Apache相关客户端。

候选类ZuulProxyAutoConfiguration初始化RestClientRibbonConfiguration、OkHttpRibbonConfiguration、HttpClientRibbonConfiguration以及RibbonRoutingFilterSimpleHostRoutingFilter

RibbonCommandFactoryConfiguration:设置Ribbon相关属性。

ZuulServerAutoConfiguration:初始化ZuulControllerZuulHandlerMapping、CompositeRouteLocator、SimpleRouteLocator。

Zuul提供了两种形式的网关服务:微服务【服务治理】方式 & http协议方式。


1.http协议方式提供网关功能

public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
    @Autowired
	private DiscoveryClient discovery;

	@Bean
	@ConditionalOnMissingBean(DiscoveryClientRouteLocator.class)
	public DiscoveryClientRouteLocator discoveryRouteLocator() {
        String prefix = this.server.getServlet().getServletPrefix()
		return new DiscoveryClientRouteLocator(prefix, this.discovery, this.zuulProperties,
				this.serviceRouteMapper, this.registration);
	}

}

public class ZuulServerAutoConfiguration {
    @Autowired
	protected ZuulProperties zuulProperties;

	@Bean
	@Primary
	public CompositeRouteLocator primaryRouteLocator(Collection<RouteLocator> routeLocators) {
		//routeLocators:DiscoveryClientRouteLocator
		return new CompositeRouteLocator(routeLocators);
	}
}

 1.1.ZuulProperties

ZuulProperties:负责加载zuul相关的路由属性。

zuul.routes.blog.path=/blog/**
zuul.routes.blog.serviceId=http://mp-admin-blog.csdn.net
@ConfigurationProperties("zuul")
public class ZuulProperties {
	
	private String prefix = "";
	// 集合routes中key表示 服务名 或者 域名
	private Map<String, ZuulRoute> routes = new LinkedHashMap<>();

	private Set<String> ignoredServices = new LinkedHashSet<>();

	private Set<String> ignoredPatterns = new LinkedHashSet<>();

	private Host host = new Host();//设置http连接相关属性,如超时相关属性

	public static class ZuulRoute {
        //zuul.routes.blog.path=/blog/** ,其中id 即为blog
		private String id;

		private String path;

		private String serviceId;

		private String url;

		private boolean stripPrefix = true;

		private Boolean retryable;

		private Set<String> sensitiveHeaders = new LinkedHashSet<>();

		private boolean customSensitiveHeaders = false;
		
	}
}


1.2.ZuulHandlerMapping 的初始化

ZuulHandlerMapping通过Order控制其所有接口HandlerMapping实现类中优先级最高。

public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {

    private final RouteLocator routeLocator;
    private final ZuulController zuul;

	public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {
		this.routeLocator = routeLocator;//CompositeRouteLocator
		this.zuul = zuul;
		setOrder(-200);
	}
}

截此为止服务启动过程中完成 ZuulHandlerMapping 持有路由相关属性。


1.3.ZuulHandlerMapping执行路由匹配请求

当SpringMVC处理请求时,由于在众多HandlerMapping实现类中ZuulHandlerMapping优先级是最高的,所以任何请求ZuulHandlerMapping优先处理。如果ZuulHandlerMapping通过request uri可以得到目标handler,即ZuulController,则后续请求流程由当前ZuulHandlerMapping触发完成,否则RequestMappingHandlerMapping完成。那ZuulHandlerMapping是如何匹配到ZuulController呢?

public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
	
	private final RouteLocator routeLocator;

	private final ZuulController zuul;

	private volatile boolean dirty = true;

	@Override
	protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
		if (this.errorController != null && urlPath.equals(this.errorController.getErrorPath())) {
			return null;
		}
		// 集合 IgnoredPaths 是否包含 当前路径 urlPath
		if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) return null;
		RequestContext ctx = RequestContext.getCurrentContext();
		if (ctx.containsKey("forward.to")) {
			return null;
		}
		if (this.dirty) {
			synchronized (this) {
				if (this.dirty) {
					// 将 ZuulProperties 配置的所有路由path信息 与 ZuulController建立绑定关系, 添加至 HandlerMapping抽象类
					registerHandlers();
					this.dirty = false;
				}
			}
		}
        // SpringMVC 正常相关逻辑
		return super.lookupHandler(urlPath, request);
	}

	private void registerHandlers() {
		Collection<Route> routes = this.routeLocator.getRoutes();
		if (routes.isEmpty()) {
			this.logger.warn("No routes found from RouteLocator");
		}else {
			for (Route route : routes) {
                // 最后将fullPath & ZuulController对应关系添加到抽象类AbstractUrlHandlerMapping属性handlerMap中
				registerHandler(route.getFullPath(), this.zuul);
			}
		}
	}
}

urlPath【当前请求对应的uri】如果命中集合 IgnoredPaths 中元素,表明当前请求是被ZuulFilter所忽略,返回null即意味着当前请求继续被RequestMappingHandlerMapping处理。

lookupHandler:利用urlPath正则匹配【Ant模式】抽象类AbstractUrlHandlerMapping属性handlerMap中元素,如果匹配通过则返回handler之ZuulController。

属性dirty的重要性:提前建立ZuulRoute 与 ZuulController之间的对应关系。


1.3.1.dirty属性

抽象类AbstractUrlHandlerMapping存在Map类型的属性之handlerMap。属性元素key为fullPath,value为ZuulController。当务之急就是将类ZuulRoute中属性转化为类Route相关属性。

public Route(String id, String path, String location, String prefix,
	Boolean retryable, Set<String> ignoredHeaders, boolean prefixStripped) {
	this(id, path, location, prefix, retryable, ignoredHeaders);
	this.prefixStripped = prefixStripped;
}
  • path:请求下游服务真实URI路径。
  • location: 优先获取ZuulRoute中URL属性,否则选择path属性。
  • prefix:ZuulProperties中prefix属性。
  • prefixStripped:ZuulRoute中属性stripPrefix,默认为true。
  • fullPath:prefix + path。
  • ZuulRoute & ZuulProperties均存在属性stripPrefix,表示是否对path进行截取,最终目的是得到path。
public class CompositeRouteLocator{

	private final Collection<? extends RouteLocator> routeLocators;

	@Override
	public List<Route> getRoutes() {
		List<Route> route = new ArrayList<>();
		// 通常情况下 集合routeLocators 中元素只有 DiscoveryClientRouteLocator
		for (RouteLocator locator : routeLocators) {
			route.addAll(locator.getRoutes());
		}
		return route;
	}
}

public class SimpleRouteLocator{
	@Override
	public List<Route> getRoutes() {
		List<Route> values = new ArrayList<>();
		// ZuulRoute元素其实即为ZuulProperties映射的配置属性
		for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
			ZuulRoute route = entry.getValue();
			String path = route.getPath();
			values.add(getRoute(route, path));
		}
		return values;
	}

	protected Route getRoute(ZuulRoute route, String path) {
		if (route == null) {
			return null;
		}
		String targetPath = path;
		//首先判断ZuulProperties中属性 zuul.prefix 是否存在值
		String prefix = this.properties.getPrefix();
		if(prefix.endsWith("/")) {//去掉前缀值中存在的后斜杠
			prefix = prefix.substring(0, prefix.length() - 1);
		}
		if (path.startsWith(prefix + "/") && this.properties.isStripPrefix()) {
			targetPath = path.substring(prefix.length());//将path中prefix截掉
		}
		if (route.isStripPrefix()) {
			// 获取首个字符 * 的索引index
			int index = route.getPath().indexOf("*") - 1;
			if (index > 0) {
				// 获取path中首个字符 * 之前的全部字符
				String routePrefix = route.getPath().substring(0, index);
				//将 routePrefix 字符串 全部替换为 空字符串
				targetPath = targetPath.replaceFirst(routePrefix, "");
				// 重新在 routePrefix 之前拼接 prefix。
				// 如果 prefix = admin, path = admin/like/**, 则此时 prefix 为 admin/admin/like
				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, 
				route.isStripPrefix());
	}
}

通过dirty属性提前在handlerMap建立ZuulRoute 与 ZuulController之间的对应关系,下一步就需要通过请求URL从handlerMap获取对应的ZuulController。

以上完成请求地址的路由匹配。


1.4.ZuulController执行过滤器ZuulFilter

  1. 通过ZuulServlet触发过滤器流程的开始。毕竟过滤器属于Servlet的范畴。
  2. 由ZuulRunner引申出FilterProcessor,FilterProcessor控制pre、route、post三类过滤器先后执行顺序。
  3. FilterProcessor加载出不同类型的全部过滤器,并依次执行全部的过滤器。
  4. 过滤器真正执行逻辑是由抽象类ZuulFilter定义的。
public class SimpleControllerHandlerAdapter implements HandlerAdapter {

	@Override
	public boolean supports(Object handler) {
		return (handler instanceof Controller);
	}

	@Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler){
		return ((Controller) handler).handleRequest(request, response);
	}

 ZuulController匹配的adapter为SimpleControllerHandlerAdapter。

public class ZuulController extends ServletWrappingController {

	@Override
	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
		return super.handleRequestInternal(request, response);
	}
}
public class ServletWrappingController extends AbstractController{
	
	private Servlet servletInstance;//ZuulServlet

	protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response){
		this.servletInstance.service(request, response);
		return null;
	}
}

 由ZuulServlet真正触发过滤器的执行流程。

public class ZuulServlet extends HttpServlet {
	public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

        RequestContext context = RequestContext.getCurrentContext();
        context.setZuulEngineRan();

        try {
            preRoute();//前置路由
            route();// 路由
            postRoute();// 后置路由
        } catch (ZuulException e) {
            postRoute();
            return;
        }
        
    }
}
public class ZuulServlet extends HttpServlet {

	private ZuulRunner zuulRunner;

	public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

        RequestContext context = RequestContext.getCurrentContext();
        context.setZuulEngineRan();

        try {
            preRoute();//前置路由
            route();// 路由
            postRoute();// 后置路由
        } catch (ZuulException e) {
            postRoute();
            return;
        }
        
    }

    void postRoute() throws ZuulException {
        zuulRunner.postRoute();
    }

    void route() throws ZuulException {
        zuulRunner.route();
    }

    void preRoute() throws ZuulException {
        zuulRunner.preRoute();
    }
}

public class ZuulRunner {
	
	public void postRoute() throws ZuulException {
        FilterProcessor.getInstance().postRoute();
    }

    public void route() throws ZuulException {
    	//FilterProcessor#
        FilterProcessor.getInstance().route();
    }

    public void preRoute() throws ZuulException {
        FilterProcessor.getInstance().preRoute();
    }
}
public class FilterProcessor {
	
	public void postRoute() throws ZuulException {
        runFilters("post");
    }

    public void route() throws ZuulException {
        runFilters("route");
    }

    
    public void preRoute() throws ZuulException {
        runFilters("pre");
    }

     public Object runFilters(String sType) {// 所有网关涉及的全部过滤器必经路径
        
        boolean bResult = false;
        //获取当前类型【pre 、post、route】的全部过滤器
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {//依次执行每个过滤器
                ZuulFilter zuulFilter = list.get(i);
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        //pre 、post、route返回值没有任何意义
        return bResult;
    }

    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

        RequestContext ctx = RequestContext.getCurrentContext();
        String filterName = "";
        RequestContext copy = null;
        Object o = null;
        Throwable t = null;
        // 执行全部过滤器的父类过滤器
        ZuulFilterResult result = filter.runFilter();
        ExecutionStatus s = result.getStatus();
        switch (s) {
            case FAILED:// 处理核心逻辑异常信息
                t = result.getException();
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                break;
            case SUCCESS:// 核心方法任何返回值都按成功处理
                o = result.getResult();
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                break;
            default:
                break;
        }
        if (t != null) throw t;
        usageNotifier.notify(filter, s);
        return o;
    }
}

前置过滤器包含:ServletDetectionFilter、Servlet30WrapperFilter、FormBodyWrapperFilter、DebugFilter、PreDecorationFilter。

PreDecorationFilter主要是设置一些代理东西,不常用

对于route类型的过滤器存在三种默认的过滤器: RibbonRoutingFilterSimpleHostRoutingFilter、SendForwardFilter【重定向相关拦截处理】。

后置过滤器:SendResponseFilter。

1.4.1.ZuulFilter

以下是每个类型的过滤器都会执行的必经逻辑:

  1. 首先执行shoulderFilter。
  2. 其次执行真正的核心流程run。

核心方法run任何返回值类型都按success处理;只有出现异常则按失败处理,即过滤器直接抛出异常。 

public abstract class ZuulFilter{
    public ZuulFilterResult runFilter() {
        ZuulFilterResult zr = new ZuulFilterResult();
        if (!isFilterDisabled()) {
            if (shouldFilter()) {// 首先判断当前过滤器是否允许执行核心逻辑
                Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
                try {
                    Object res = run();// 过滤器的核心逻辑 ~ 对于核心方法的返回值没有任何意义
                    zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
                } catch (Throwable e) {// 只有核心方法run 出现异常,才会终止请求流程
                    t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                    zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                    zr.setException(e);
                } finally {
                    t.stopAndLog();
                }
            } else {
                //跳过当前过滤器
                zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
            }
        }
        return zr;
    }
}

1.5.调用下游downstream服务

对于route类型的过滤器,自定义的过滤器的优先级远高于自带三种过滤器。如果需要触发下游服务继续调用,目前只有RibbonRoutingFilter、SimpleHostRoutingFilter两种过滤器支持。其中,RibbonRoutingFilter是通过微服务(服务治理)方式实现,SimpleHostRoutingFilter则是通过http方式调用下游服务。

RibbonRoutingFilter、SimpleHostRoutingFilter、SendResponseFilter过滤器生效的共同条件是:RequestContext之sendZuulResponse属性必须为true。意味着在自定义过滤器内部如果允许下游服务继续访问,必须显式设置sendZuulResponse的属性值。

1.5.1.RibbonRoutingFilter

public class RibbonRoutingFilter extends ZuulFilter {
	@Override
	public boolean shouldFilter() {
		RequestContext ctx = RequestContext.getCurrentContext();
		return (ctx.getRouteHost() == null && ctx.get("serviceId") != null
				&& ctx.sendZuulResponse());
	}
}

1.5.2.SimpleHostRoutingFilter

public class SimpleHostRoutingFilter extends ZuulFilter {
	@Override
	public boolean shouldFilter() {
		return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse();
	}

	public Object run() {
		RequestContext context = RequestContext.getCurrentContext();
		...
		String uri = this.helper.buildZuulRequestURI(request);
		this.helper.addIgnoredHeaders();
		// 通过http方式调用下游服务
		CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,headers, params, requestEntity);
		setResponse(response);
		return null;
	}

	private void setResponse(HttpResponse response) throws IOException {
		// 将下游服务的响应封装在 上下文 属性zuulResponse中
		RequestContext.getCurrentContext().set("zuulResponse", response);
		// 将下游服务的响应头、响应实体等信息 单独添加至 上下文中
		this.helper.setResponse(response.getStatusLine().getStatusCode(),
				response.getEntity() == null ? null : response.getEntity().getContent(),
				revertHeaders(response.getAllHeaders()));
	}
}

触发下游服务 & 将下游服务响应的相关属性跟上下文RequestContext绑定。 

1.5.3.SendResponseFilter

通过以下得知,该过滤器整合下游服务响应内容的前提是RibbonRoutingFilter or SimpleHostRoutingFilter 至少有一个生效。

public class SendResponseFilter extends ZuulFilter {
	
	@Override
	public boolean shouldFilter() {
		RequestContext context = RequestContext.getCurrentContext();
		return context.getThrowable() == null
				&& (!context.getZuulResponseHeaders().isEmpty()// 下游服务的响应头必须被添加至上下文ZuulResponseHeaders属性中
					// 此处是指下游服务的响应不能为空
					|| context.getResponseDataStream() != null
					|| context.getResponseBody() != null);
	}

	@Override
	public Object run() {
		addResponseHeaders();
		writeResponse();
		return null;
	}

	private void writeResponse() throws Exception {
		RequestContext context = RequestContext.getCurrentContext();
		if (context.getResponseBody() == null && context.getResponseDataStream() == null) {
			return;
		}
		HttpServletResponse servletResponse = context.getResponse();
		if (servletResponse.getCharacterEncoding() == null) { // only set if not set
			servletResponse.setCharacterEncoding("UTF-8");
		}
		//此处是指在自定义过滤器中添加的响应信息
		OutputStream outStream = servletResponse.getOutputStream();
		InputStream is = null;
		if (context.getResponseBody() != null) {// 如果在自定义过滤器中添加了ResponseBody,则下游服务的响应不会被添加到最终响应值中
			String body = context.getResponseBody();
			is = new ByteArrayInputStream(body.getBytes(servletResponse.getCharacterEncoding()));
		}else {
			is = context.getResponseDataStream();// 获取下游服务的响应值
			if (is!=null && context.getResponseGZipped()) {// 下游服务是否需要压缩
				if (isGzipRequested(context)) {
					servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
				}else {
					is = handleGzipStream(is);
				}
			}
		}
		
		if (is!=null) {
			// 将下游服务的响应is写到 outStream中,即为最终响应内容
			writeResponse(is, outStream);
		}
		...
	}
}

自定义过滤器内部如果存在通过字符流Writer写入数据,则在SendResponseFilter内部抛出异常:getWriter() has already been called for this response,但是不会打印出堆栈信息,该异常非常隐蔽。自定义过滤器内部可以选择字节流方式写入数据。

记 SpringBoot 拦截器报错 getWriter() has already been called for this response


http://lihuaxi.xjx100.cn/news/2217804.html

相关文章

SpringCloud入门之常用的配置文件 application.yml和 bootstrap.yml区别

1. 加载时机 - bootstrap.yml - 在Spring应用程序启动的早期阶段加载&#xff0c;早于application.yml。 - 它主要用于加载应用程序的上下文或环境设置&#xff0c;例如配置中心的地址、加密解密信息等。 - 通常在ApplicationContext初始化之前加载&#xff0c;因此适用…

从WWDC 2023看苹果的未来:操作系统升级与AI技术的融合

引言 在2024年的WWDC&#xff08;苹果全球开发者大会&#xff09;上&#xff0c;苹果公司展示了一系列创新技术和产品&#xff0c;其中最引人注目的莫过于操作系统的升级与AI技术的深度融合。作为一个备受期待的发布会&#xff0c;WWDC不仅向我们展示了苹果在技术上的前瞻性布…

记一次护网通过外网弱口令一路到内网

文章中涉及的敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打码处理。传播、利用本文章所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本…

读《任正非文集》

《任正非文集》其实不是一本书&#xff0c;而是任正非在华为内容的讲话内容&#xff0c;有人把这些讲话内容集结成册&#xff0c;目前记录了从1994年到2018年间一共400多篇谈话。 感兴趣的可以在这里下载。 我是下载后导入到微信读书中听的&#xff0c;一共有100多万字。 我…

[C语言]条件编译

C语言条件编译用途&#xff1a; 1.判断宏是否被定义&#xff1b; 2.判断宏是否未被定义&#xff1b; 3.选择性执行 #include <stdio.h>#define DEBUG 10 #define HIGH 2int main() {int value 10;//1.用于判断宏是否被定义 #ifdef DEBUGprintf("1.DEBUG define…

一个开源的快速准确地将 PDF 转换为 markdown工具

大家好&#xff0c;今天给大家分享的是一个开源的快速准确地将 PDF 转换为 markdown工具。 Marker是一款功能强大的PDF转换工具&#xff0c;它能够将PDF文件快速、准确地转换为Markdown格式。这款工具特别适合处理书籍和科学论文&#xff0c;支持所有语言的转换&#xff0c;并…

springboot与flowable(5):任务分配(表达式)

在做流程定义时我们需要给相关的用户节点指派对应的处理人。在flowable中提供了三种分配的方式。 一、固定分配 在分配用户时选择固定值选项确认即可。 二、表达式 1、值表达式 2、方法表达式 三、表达式流程图测试 1、导出并部署 导出流程图&#xff0c;复制到项目中 部署流…

反向海淘代购系统集成功能详解:从商品对接到物流转运的一体化解决方案

随着全球化进程的加速&#xff0c;反向海淘&#xff08;即从国外购买商品至国内&#xff09;的需求日益增长。为满足这一市场趋势&#xff0c;反向海淘代购系统正不断进化&#xff0c;集成多种功能以提供更加便捷、高效的服务。本文将深入探讨反向海淘代购系统的核心集成功能&a…