-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 200 KB
/
content.json
1
{"meta":{"title":"Allen","subtitle":"","description":"","author":"Allen Chang","url":"https://lazyallen.github.io","root":"/"},"pages":[{"title":"404 Not Found:该页无法显示","date":"2022-12-01T08:31:35.050Z","updated":"2022-12-01T08:31:35.050Z","comments":false,"path":"/404.html","permalink":"https://lazyallen.github.io/404.html","excerpt":"","text":""},{"title":"关于","date":"2022-12-01T11:44:53.034Z","updated":"2022-12-01T11:44:53.034Z","comments":false,"path":"about/index.html","permalink":"https://lazyallen.github.io/about/index.html","excerpt":"","text":"这是什么记录、树洞、碎碎念 为什么会有这个有空的话,就记录一下 要怎么做多写多想每个字都算数技能、方法培养容易,好胜心无法培养,真诚与善良无法培养 我是谁后端开发工程师 我希望拥有的特质有好奇心,能够主动学习新事物、新知识和新技能对不确定性保持乐观不甘于平庸不傲娇,有延迟满足感对重要的事情有判断力,不要被短期选择 What’s thatWriting、Treehole 、Broken thoughts Why create thatMany times, in the face of uncontrollable factors, it is the right way to settle down, lay the foundation, and strive to improve yourself. How to do itWrite more and hesitate lessMore than technologyEvery word is crucialSkills and methods are easy to cultivate, goodwill cannot be cultivated, sincerity and kindness cannot be cultivated Who am IJunior back-end development engineer Who i want to beBe curious and actively learn new things, new knowledge and new skills.Be optimistic about uncertainty.Not willing to be mediocre.Not proud, with a sense of delay and satisfaction.Be judgmental about important things, don’t be chosen short-term"},{"title":"书单","date":"2022-12-01T08:31:35.054Z","updated":"2022-12-01T08:31:35.054Z","comments":false,"path":"books/index.html","permalink":"https://lazyallen.github.io/books/index.html","excerpt":"","text":""},{"title":"分类","date":"2022-12-01T08:31:35.054Z","updated":"2022-12-01T08:31:35.054Z","comments":false,"path":"categories/index.html","permalink":"https://lazyallen.github.io/categories/index.html","excerpt":"","text":""},{"title":"Repositories","date":"2022-12-01T08:31:35.061Z","updated":"2022-12-01T08:31:35.061Z","comments":false,"path":"repository/index.html","permalink":"https://lazyallen.github.io/repository/index.html","excerpt":"","text":""},{"title":"标签","date":"2022-12-01T08:31:35.062Z","updated":"2022-12-01T08:31:35.062Z","comments":false,"path":"tags/index.html","permalink":"https://lazyallen.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"记一次unicode编码引发的问题","slug":"unicode-normalization","date":"2023-05-04T16:00:00.000Z","updated":"2023-05-05T15:58:10.047Z","comments":true,"path":"2023/05/05/unicode-normalization/","link":"","permalink":"https://lazyallen.github.io/2023/05/05/unicode-normalization/","excerpt":"","text":"背景遇到的场景是:客户从PDF粘贴一段文字到搜索框,内容是工时规范四个中文字,发现搜不出结果,但是自己手动敲击文字则是正常。请求如下: 12https://xxx?name=%E5%B7%A5%E6%97%B6%E8%A7%84%E8%8C%83https://xxx?name=%E2%BC%AF%E6%97%B6%E8%A7%84%E8%8C%83 以上链接是URL编码后的结果。 对比得知两者区别在于编码是不一样的,但展示结果看起来是一致的。URL编码是16进制的,区别只有前面几个编码不同,也就是工字。 E5B7A5 E2BCAF 排查思路遇到的第一个问题是,为什么不同的编码会出现一样的字?通常URL编码对应的是UTF8编码格式,可从encode_utf8 网站检索如下。 字符 编码10进制 编码16进制 Unicode编码10进制 Unicode编码16进制 工 15054757 E5B7A5 24037 5DE5 ⼯ 14859439 E2BCAF 12079 2F2F 对于编码来讲,我的理解就是一组kv集合,key就是编码,v就是值。无论是UTF8还是unicode都是一样的,只是两者范围和大小上有所不同。 通过symbl可以通过unicode编码获取对应的字符是什么,也就是get(k)。 可以看到两者的归属是不一样的: Kangxi Radicals: 康熙部首 CJK Unified Ideographs: 中日韩统一表意文字 也就是说,两个工字都是中文,只是一个是部首偏旁,另外一个我们正常使用的汉字。目前有使用ChatGPT辅助我查找问题,当我扔上面两个字符给到ChatGPT时,它告诉我们区别在于是否是全角和半角,于是我开始搜索全角和半角的区别。但显然并不是这个原因,也就是说ChatGPT在回答这种问题时,是会误导用户的。 思路得知问题所在后,就是开始找解决方案了。检索发现unicode是提供 normalization 功能的。我理解解决的就是兼容性的问题,比如两个长得很像字符,不只是中文,其他语言也有类似的问题。在计算机的世界里面,这两者的编码不同,但在人类世界中可能是一致的。 12345678910111213>>> s1 = 'Spicy Jalape\\u00f1o'>>> s2 = 'Spicy Jalapen\\u0303o'>>> s1'Spicy Jalapeño'>>> s2'Spicy Jalapeño'>>> import unicodedata>>> t1 = unicodedata.normalize('NFC', s1)>>> t2 = unicodedata.normalize('NFC', s2)>>> t1 == t2True 对应Java代码也很简单 1Normalizer.normalize(argStr, Normalizer.Form.NFKC); 基本解决方案有了,但如何集成到工程当中呢。工程使用的SpringBoot, Web使用的是Spring MVC。问题是如何把这段逻辑放在工厂中,最简单的方案就是哪个字段有问题就转一下就可以,这样做的问题就是改的多了就是多处存在相同的代码片段。类似这种统一处理的问题,在Spring中最明显就是AOP方案。做一个切面,获取到所有的参数都做一遍规范化处理。这样做当然可以,只是过于简单粗暴了,笔者在想有没有更优雅的方案。那么需要先回答一个问题:Spring是如何帮我们自动把URL编码转成文本的?其实这里应该有两个过程: URL编码的二进制流转成文本 文本绑定到方法的参数上 在Spring MVC中,实际做这件事的是HandlerMethodArgumentResolver,这个接口有大量的默认实现去解析不同场景下的参数。例如RequestMappingHandlerAdapter#getDefaultArgumentResolvers() 123456789101112131415161718192021222324252627282930313233343536373839404142private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); resolvers.add(new ErrorsMethodArgumentResolver()); resolvers.add(new SessionStatusMethodArgumentResolver()); resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); // Custom arguments if (getCustomArgumentResolvers() != null) { resolvers.addAll(getCustomArgumentResolvers()); } // Catch-all resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); return resolvers; } HandlerMethodArgumentResolver 提供两个方法: 判断是否支持该解析器,返回值是boolean 实际用来解析的方法,返回的是Object,也就是解析后的值1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192public interface HandlerMethodArgumentResolver { /** * Whether the given {@linkplain MethodParameter method parameter} is * supported by this resolver. * @param parameter the method parameter to check * @return {@code true} if this resolver supports the supplied parameter; * {@code false} otherwise */ boolean supportsParameter(MethodParameter parameter); /** * Resolves a method parameter into an argument value from a given request. * A {@link ModelAndViewContainer} provides access to the model for the * request. A {@link WebDataBinderFactory} provides a way to create * a {@link WebDataBinder} instance when needed for data binding and * type conversion purposes. * @param parameter the method parameter to resolve. This parameter must * have previously been passed to {@link #supportsParameter} which must * have returned {@code true}.public interface HandlerMethodArgumentResolver { /** * Whether the given {@linkplain MethodParameter method parameter} is * supported by this resolver. * @param parameter the method parameter to check * @return {@code true} if this resolver supports the supplied parameter; * {@code false} otherwise */ boolean supportsParameter(MethodParameter parameter); /** * Resolves a method parameter into an argument value from a given request. * A {@link ModelAndViewContainer} provides access to the model for the * request. A {@link WebDataBinderFactory} provides a way to create * a {@link WebDataBinder} instance when needed for data binding and * type conversion purposes. * @param parameter the method parameter to resolve. This parameter must * have previously been passed to {@link #supportsParameter} which must * have returned {@code true}. * @param mavContainer the ModelAndViewContainer for the current request * @param webRequest the current request * @param binderFactory a factory for creating {@link WebDataBinder} instances * @return the resolved argument value, or {@code null} if not resolvable * @throws Exception in case of errors with the preparation of argument values */ @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;}public interface HandlerMethodArgumentResolver { /** * Whether the given {@linkplain MethodParameter method parameter} is * supported by this resolver. * @param parameter the method parameter to check * @return {@code true} if this resolver supports the supplied parameter; * {@code false} otherwise */ boolean supportsParameter(MethodParameter parameter); /** * Resolves a method parameter into an argument value from a given request. * A {@link ModelAndViewContainer} provides access to the model for the * request. A {@link WebDataBinderFactory} provides a way to create * a {@link WebDataBinder} instance when needed for data binding and * type conversion purposes. * @param parameter the method parameter to resolve. This parameter must * have previously been passed to {@link #supportsParameter} which must * have returned {@code true}. * @param mavContainer the ModelAndViewContainer for the current request * @param webRequest the current request * @param binderFactory a factory for creating {@link WebDataBinder} instances * @return the resolved argument value, or {@code null} if not resolvable * @throws Exception in case of errors with the preparation of argument values */ @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;} * @param mavContainer the ModelAndViewContainer for the current request * @param webRequest the current request * @param binderFactory a factory for creating {@link WebDataBinder} instances * @return the resolved argument value, or {@code null} if not resolvable * @throws Exception in case of errors with the preparation of argument values */ @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;} 很明显,@RequestParam 注解对应的类就是RequestParamMethodArgumentResolver 1234567891011121314151617181920212223242526protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); if (servletRequest != null) { Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { return mpArg; } } Object arg = null; MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class); if (multipartRequest != null) { List<MultipartFile> files = multipartRequest.getFiles(name); if (!files.isEmpty()) { arg = (files.size() == 1 ? files.get(0) : files); } } if (arg == null) { String[] paramValues = request.getParameterValues(name); if (paramValues != null) { arg = (paramValues.length == 1 ? paramValues[0] : paramValues); } } return arg; } 简单看其中逻辑可见,最后是通过request.getParameterValues(name);获取到实际的值,这里跟下去的话就到了tomcat那层的代码。笔者在思考是否有什么类似于编码的参数,可以用于配置编码这个流程,实际找到有HttpProperties这个配置类,但其中只是对于编码类型的配置,不涉及normalization的部分。现在目标就是如何增强RequestParamMethodArgumentResolver,使其有规范化的能力。思路只有在RequestParamMethodArgumentResolver本身动手。 是否提供扩展点进行定制化简单阅读代码发现,该类还继承AbstractNamedValueMethodArgumentResolver,该类有一个方法handleResolvedValue,签名如下 12345678910111213/** * Invoked after a value is resolved. * @param arg the resolved argument value * @param name the argument name * @param parameter the argument parameter type * @param mavContainer the {@link ModelAndViewContainer} (may be {@code null}) * @param webRequest the current request */protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {} 这个可以看作一个扩展点,但很无奈的是,这个只是一个void方法,Java是值传递,而且String时不可变的,所以就算重写这个方法也没办法对返回新的arg。 重新定义一个RequestParamMethodArgumentResolver要解决两个问题 重新定义RequestParamMethodArgumentResolver,并且重写resolveName方法。 如何覆盖默认的RequestParamMethodArgumentResolver 简单实现: 123456789101112131415161718192021222324public class RequestParamMethodNormalizedArgumentResolver extends RequestParamMethodArgumentResolver { public RequestParamMethodNormalizedArgumentResolver(boolean useDefaultResolution) { super(useDefaultResolution); } public RequestParamMethodNormalizedArgumentResolver(ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) { super(beanFactory, useDefaultResolution); } @Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { Object arg = super.resolveName(name, parameter, request); // 对参数进行NFKC规范化 if (Objects.nonNull(arg) && arg instanceof String ) { String argStr = String.valueOf(arg); if(StringUtils.isNotEmpty(argStr)){ arg = Normalizer.normalize(argStr, Normalizer.Form.NFKC); } } return arg; }} 对于第二个问题,根据上面getDefaultArgumentResolvers方法可知,其顺序是hardcode的。只要在合适时间去set这个list就可以。 123456789101112131415161718192021222324@Configurationpublic class WebMvcConfig extends WebMvcConfigurationSupport implements BeanFactoryAware, InitializingBean { @Nullable private ConfigurableBeanFactory beanFactory; @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { if (beanFactory instanceof ConfigurableBeanFactory) { this.beanFactory = (ConfigurableBeanFactory) beanFactory; } } @Override public void afterPropertiesSet() { List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers(); List<HandlerMethodArgumentResolver> newArgumentResolvers = new LinkedList<>(); newArgumentResolvers.add(new RequestParamMethodNormalizedArgumentResolver(this.beanFactory,false)); newArgumentResolvers.addAll(argumentResolvers); requestMappingHandlerAdapter.setArgumentResolvers(Collections.unmodifiableList(newArgumentResolvers)); }} 对于第2个问题,我同时也让ChatGPT提供了一段代码,发现其只是用来增加自定义的Resolver,顺序依旧在默认实现后面。其实ChatGPT的话不能全信,只能信一部份,作为提供思路的辅助,其提供的代码并不一定就能直接使用。到现在,对于URL的Query参数的normalization完成了。当然这只是解决Query参数的场景,对于Body里面的参数,甚至Header,Path里面的参数,要考虑的话都需要去考虑。实际上,笔者认为如果前端提交请求之前,做了normalization处理的话,就能从源头上避免大部份的问题。 感想事后有两个感想: 也许这个只是一个伪问题:因为用户的确提交的字符本身就是「错」的,只是这两个字符长得很像而已。是否有必要帮助用户去规范化字符,也许体验上会好很多,毕竟在实际使用中,很少人真的会搜寻部首。笔者的疑问是,对于一个错误的输入,是否应该返回一个对的输出?其实这个抉择在方案设计时经常会碰到,所谓的优化会不会去掩盖掉「错误」?笔者个人觉得,其实不应该返回对的输出。 ChatGPT的作用没有想象中那么大,其给出的回答可以作为参考方向,因为有可能回答部份都是错的。对于开发而言,依旧需要依托自己的经验,去不断提问题,探究问题的本质,整理思路,不断摸索,千万不能完全依托ChatGPT的回答。 引用 symbl.cc mytju.com 从⽅不是方到Unicode正规化NFD, NFC, NFKD, NFKC 中文和日文里长得一样的字在计算机中是不是同一个字?","categories":[{"name":"Cloud Native","slug":"Cloud-Native","permalink":"https://lazyallen.github.io/categories/Cloud-Native/"}],"tags":[{"name":"Helm","slug":"Helm","permalink":"https://lazyallen.github.io/tags/Helm/"}]},{"title":"Helm工程实践:如何维护一个Helm工程","slug":"helm-practice","date":"2023-02-17T16:00:00.000Z","updated":"2023-02-18T15:41:58.439Z","comments":true,"path":"2023/02/18/helm-practice/","link":"","permalink":"https://lazyallen.github.io/2023/02/18/helm-practice/","excerpt":"","text":"Helm简介 包管理 模板引擎:Go template helm 3 渲染资源的顺序假设有个chart “A” 创建了下面的Kubernetes对象: namespace “A-Namespace” statefulset “A-StatefulSet” service “A-Service” 另外,A是依赖于chart B创建的对象: namespace “B-Namespace” replicaset “B-ReplicaSet” service “B-Service” 安装/升级chart A后,会创建/修改一个单独的Helm版本。这个版本会按顺序创建/升级以下所有的Kubernetes对象: A-Namespace B-Namespace A-Service B-Service B-ReplicaSet A-StatefulSet 这是因为当Helm安卓/升级chart时,chart中所有的Kubernetes对象以及依赖会 聚合成一个单一的集合;然后 按照类型和名称排序; 然后按这个顺序创建/升级。至此会为chart及其依赖创建一个包含所有对象的release版本。 Kubernetes类型的安装顺序会按照kind_sorter.go(查看Helm源文件)中给出的枚举顺序进行。 CRD的限制不像大部分的Kubernetes对象,CRD是全局安装的。因此Helm管理CRD时会采取非常谨慎的方式。 CRD受到以下限制: CRD从不重新安装。 如果Helm确定crds/目录中的CRD已经存在(忽略版本),Helm不会安装或升级。 CRD从不会在升级或回滚时安装。Helm只会在安装时创建CRD。 CRD从不会被删除。自动删除CRD会删除集群中所有命名空间中的所有CRD内容。因此Helm不会删除CRD。希望升级或删除CRD的操作员应该谨慎地手动执行此操作。 chart 示例开发流程 kubeconfig : 用于chart 的部署测试。非必选,如没有k8s环境,后面只能使用template 输出渲染文件; VS code 及插件: https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools https://marketplace.visualstudio.com/items?itemName=Tim-Koehler.helm-intellisense https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml helm 及插件 https://helm.sh/docs/intro/install/ https://github.com/norwoodj/helm-docs https://github.com/karuppiah7890/helm-schema-gen https://github.com/chartmuseum/helm-push 获取开发环境使用Chart Starter 包对于相似度很高的项目,可以使用chart starter ,可以节约修改 chart name的时间;clone 仓库到本地后,加入到HELM_DATA_HOME目录下的starters目录中。之后再根据每个业务组件自身需要进行修改 使用helm create适合从零开始开发的chart 12helm create mychartrm -rf mychart/templates/* 在容器里开发 123# Kubernetesk8s config copy~/.kube/config$ docker image pull registry.xxx.com/xxx/helm-playground:xxxx$ docker run -it --rm -v ~/.kube:/root/.kube xxxx /bin/bash 修改templates 和 values修改需要部署的资源文件和values,保证一个应用的最小可部署资源集合; 开发建议 命名规范:template文件用小写字母;values中的变量使用驼峰;123456789101112131415161718192021$ tree ./ ./ Chart.yaml README.md README.md.gotmpl templates NOTES.txt _helpers.tpl bootstrap-configmap.yaml clusterrole.yaml clusterrolebinding.yaml deployment.yaml init-job.yaml persistentvolume.yaml persistentvolumeclaim.yaml service-monitor.yaml service.yaml serviceaccount.yaml storageclass.yaml values.schema.json values.yaml1 directory, 18 files 不要hardcode namespace,而是通过 – namespace flag 动态传入,后者灵活性更强。 12345# suggesthelm install business-defender ./ -n whale-component --debug --atomic# not suggestmetadata: namespace: {{ .Release.Namespace }} https://github.com/helm/helm/issues/5465 _helpers.tpl 可类比成 Utils 类:比较通用,复用比较多 的逻辑 比较复杂的逻辑 在_helpers.tpl 定义Named Templates 时,统一以chart.name 开头。相当于把chart.name 当作name templates 的命名空间,避免引用冲突; 12345678910# good{{/* Create a default fully qualified app name. */}}{{- define "business-defender.fullname" -}}...{{- end }}# bad{{/* Create a default fully qualified app name. */}}{{- define "fullname" -}}...{{- end }} 给values写注释,并用helm-docs生成文档,用helm schema-gen 生成schema.json文件1234567global: # -- machine architecture hostArch: "amd64" # -- docker registry dockerRegistry: "registry.xxxx.com" # -- prometheus namespace monitorNamespace: "monitoring" 验证语法验证 1234$ helm lint==> Linting .[INFO] Chart.yaml: icon is recommended1 chart(s) linted, 0 chart(s) failed 渲染验证 12helm template ./ --debug -n whale-componenthelm install business-defender ./ -n whale-component --debug --dry-run template 和 install –dry-run 的区别在于是否会和 Kubernetes Api Server 通行。使用 install –dry-run ,相当于helm 渲染资源后使用kubectl 的–dry-run 验证一样;因为可以和k8s通信,可以获取一些k8s的元数据,此时,.Capabilities 内置对象才有意义; 部署验证验证时,需要在 defender 环境下验证,因为chart中可能包含一些预定的资源依赖,比如 password-secret; 12345678910# atomic flaghelm install business-defender ./ -n whale-component --debug --atomic# wait deletehelm uninstall business-defender -n whale-component --wait --debug$ helm list -n whale-component# Release$ helm get all business-defender -n whale-component pre-commit:生成readme.md 和 values.schema.json1234# helm plugin schema-genvaluesschema.jsonhelm installvalues.yaml.helm schema-gen values.yaml > values.schema.json# valuesreadme.mdhelm-docs 发布手动发布需要手动安装 cm-push :https://github.com/chartmuseum/helm-push 1234$ helm plugin install https://github.com/chartmuseum/helm-push$ helm repo add --username <username> --password <password> business https://registry.xxx.com/chartrepo/business$ helm cm-push --username=<username> --password=<password> . business --version 2.2.1l 另外也可以通过页面上传 CI发布 12345678910111213141516171819202122232425262728image: name: registry.xxx.com/viper-ce/library/helm-tool:3.6.3 entrypoint: ["/bin/sh", "-c"]variables: HELM_CI_REPO_ADDR: https://registry.xxx.com/chartrepo/business HELM_CI_REPO_NAME: businessstages: - lint - render - releaselint-chart: stage: lint script:- helm lint .render-chart: stage: render script: - helm template .release-chart: stage: release script: - export CHART_VERSION=$(awk '/^version/{print $NF}' Chart.yaml) - helm repo add --username ${HELM_REPO_USERNAME} --password ${HELM_REPO_PASSWD} ${HELM_CI_REPO_NAME}${HELM_CI_REPO_ADDR} - helm push . ${HELM_CI_REPO_NAME} --version "${CHART_VERSION}-${CI_COMMIT_SHA:0:8}" - helm push . ${HELM_CI_REPO_NAME} --version "${CHART_VERSION}" --force only:- master 合入master时才发布一个版本;每次release 会push两次: 带 commit id 的版本 不带 commid id 的stable版本,强制覆盖仓库中的stable版本 repo管理仓库选择: harbor chart repo :https://registry.xxx.com/chartrepo/business infra团队自建并维护的chartmuseum:http://sz.xxx.xxx.net/chartrepo/sensefoundry-stable/ 是否需要单独创建一个project存储chartrepo,业务chart 和 中间件chart 是否需要区分? 版本管理版本约定helm chart 的版本管理主要是通过 Chart.yaml里面的appVersion和version去管理 appVersion:主要对应 应用 本身的版本比如 business-defender 的版本是1.0.2-SNAPSHOT version:主要对应 helm chart 版本,helm package 通过读取 Chart.yaml 里的 version进行 chart 打包,然后推送到仓库 如 当前业务系统的版本为 2.2.0 ,helm chart的commit_id 6336fbe,此时version:2.2.0-6336fbe 12version: 2.2.0appVersion: "1.0.2-SNAPSHOT" 仓库中存在: 以version/chart version/ 业务系统版本 的stable版本 带有commit id 的开发版本","categories":[{"name":"Cloud Native","slug":"Cloud-Native","permalink":"https://lazyallen.github.io/categories/Cloud-Native/"}],"tags":[{"name":"Helm","slug":"Helm","permalink":"https://lazyallen.github.io/tags/Helm/"}]},{"title":"浅析Dubbo的事件驱动机制","slug":"dubbo-event-driven-introduction","date":"2020-11-06T16:00:00.000Z","updated":"2022-12-07T07:03:32.627Z","comments":true,"path":"2020/11/07/dubbo-event-driven-introduction/","link":"","permalink":"https://lazyallen.github.io/2020/11/07/dubbo-event-driven-introduction/","excerpt":"本文介绍的是Dubbo 2.7.5中的事件驱动机制,简要分析了其原理,分析了引入事件驱动的原因。","text":"本文介绍的是Dubbo 2.7.5中的事件驱动机制,简要分析了其原理,分析了引入事件驱动的原因。 前言 事件驱动程序设计(英语:Event-driven programming)是一种电脑程序设计模型。这种模型的程序运行流程是由用户的动作(如鼠标的按键,键盘的按键动作)或者是由其他程序的消息来决定的。事件驱动程序设计这种设计模型是在交互程序(Interactive program)的情况下孕育而生的。 从这段维基百科的引用可知,事件驱动一开始是由于交互程序中的行为而产生的。这种设计也符合现实中的行为,生活当中经常需要应对一件事情从而行为做出改变。比如听到了闹钟声就要起床,闹钟声就是一个事件,而起床则是人们对于这件事做出的动作。我们可以将闹钟起床抽象拆分为两个部分: 事件:具体的事情; 事件触发时做出的反应; Java对Event的支持在JDK1.1版本中的java.util 包中有两个类:EventObject 和 EventListener,所对应的概念就是上面提到的两个部分。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354package java.util;/** * <p> * The root class from which all event state objects shall be derived. * <p> * All Events are constructed with a reference to the object, the "source", * that is logically deemed to be the object upon which the Event in question * initially occurred upon. * * @since JDK1.1 */public class EventObject implements java.io.Serializable { private static final long serialVersionUID = 5516075349620653480L; /** * The object on which the Event initially occurred. */ protected transient Object source; /** * Constructs a prototypical Event. * * @param source The object on which the Event initially occurred. * @exception IllegalArgumentException if source is null. */ public EventObject(Object source) { if (source == null) throw new IllegalArgumentException("null source"); this.source = source; } /** * The object on which the Event initially occurred. * * @return The object on which the Event initially occurred. */ public Object getSource() { return source; } /** * Returns a String representation of this EventObject. * * @return A a String representation of this EventObject. */ public String toString() { return getClass().getName() + "[source=" + source + "]"; }} EventObject中持有一个source的Object对象,同时可以通过getSource() 方法获取到source,所以source就是真实的事件。 123456789package java.util;/** * A tagging interface that all event listener interfaces must extend. * @since JDK1.1 */public interface EventListener {} 而EventListener 是一个没有方法的Interface,具体的动作行为我们可以通过继承的方式去定义。如前言中说的,事件驱动是由交互应用所产生,以上两个类在Java GUI编程中随处可见,在java.awt.event中封装了一系列的Event和EventLister,用于描述诸如鼠标,输入框、窗口等GUI事件。 Dubbo中 事件发布的实现 Java Event:在Dubbo 2.7.5的Release版本中,添加了事件驱动机制。在org.apache.dubbo.event中,主要看3个类:Event、EventListener、EventDispatcher,其中EventDispatcher可译作事件分发器,其用于分发和执行相应的Event。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051package org.apache.dubbo.event;import org.apache.dubbo.common.extension.ExtensionLoader;import org.apache.dubbo.common.extension.SPI;import java.util.concurrent.Executor;/** * {@link Event Dubbo Event} Dispatcher * * @see Event * @see EventListener * @see DirectEventDispatcher * @since 2.7.5 */@SPI("direct")public interface EventDispatcher extends Listenable<EventListener<?>> { /** * Direct {@link Executor} uses sequential execution model */ Executor DIRECT_EXECUTOR = Runnable::run; /** * Dispatch a Dubbo event to the registered {@link EventListener Dubbo event listeners} * * @param event a {@link Event Dubbo event} */ void dispatch(Event event); /** * The {@link Executor} to dispatch a {@link Event Dubbo event} * * @return default implementation directly invoke {@link Runnable#run()} method, rather than multiple-threaded * {@link Executor}. If the return value is <code>null</code>, the behavior is same as default. * @see #DIRECT_EXECUTOR */ default Executor getExecutor() { return DIRECT_EXECUTOR; } /** * The default extension of {@link EventDispatcher} is loaded by {@link ExtensionLoader} * * @return the default extension of {@link EventDispatcher} */ static EventDispatcher getDefaultExtension() { return ExtensionLoader.getExtensionLoader(EventDispatcher.class).getDefaultExtension(); }} 从接口上的注解@SPI(“direct”)可知,EventDispatcher是一个SPI接口,且默认实现对应为DirectEventDispatcher。 123456789101112131415package org.apache.dubbo.event;/** * Direct {@link EventDispatcher} implementation uses current thread execution model * * @see EventDispatcher * @since 2.7.5 */public final class DirectEventDispatcher extends AbstractEventDispatcher { public DirectEventDispatcher() { super(DIRECT_EXECUTOR); }} 大部分逻辑在AbstractEventDispatcher中实现,我们具体看AbstractEventDispatcher的内容。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051public abstract class AbstractEventDispatcher implements EventDispatcher { private final Object mutex = new Object(); private final ConcurrentMap<Class<? extends Event>, List<EventListener>> listenersCache = new ConcurrentHashMap<>(); private final Executor executor; ... @Override public void addEventListener(EventListener<?> listener) throws NullPointerException, IllegalArgumentException { Listenable.assertListener(listener); doInListener(listener, listeners -> { addIfAbsent(listeners, listener); }); } ... @Override public void dispatch(Event event) { Executor executor = getExecutor(); // execute in sequential or parallel execution model executor.execute(() -> { sortedListeners(entry -> entry.getKey().isAssignableFrom(event.getClass())) .forEach(listener -> { if (listener instanceof ConditionalEventListener) { ConditionalEventListener predicateEventListener = (ConditionalEventListener) listener; if (!predicateEventListener.accept(event)) { // No accept return; } } // Handle the event listener.onEvent(event); }); }); } /** * @return the non-null {@link Executor} */ @Override public final Executor getExecutor() { return executor; } } 从关键代码中可以看出,AbstractEventDispatcher中包含3个成员变量,其中listenersCache维护的是listeners列表,executor表示的是用户执行listener逻辑的线程执行器,默认使用当前线程。dispatch()中通过Event的类型从listenersCache找到对应listeners列表后,逐一执行listener.onEvent(event),onEvent()所定义的则是listener中的实际业务逻辑。 补充一下,Dubbo的Event和EventListener继承的是JDK的EventObject和EventListener,也就是说Dubbo的事件驱动基于JDK的事件驱动机制。 为什么Dubbo发布到2.7.5版本才添加了事件驱动机制在2.7.5版本前,Dubbo支持事件通知特性。对于特定服务,在调用之前、调用之后、出现异常时,会触发 oninvoke、onreturn、onthrow 三个事件,可以配置当事件发生时,通知哪个类的哪个方法.与其叫做事件通知,更像是一种AOP机制,在服务调用生命周期中切入业务逻辑。 之所以Dubbo在2.7.5才添加了事件驱动机制,是因为在这个版本,Dubbo同时加入服务自省架构。关于服务自省架构可以阅读Dubbo 迈出云原生重要一步 - 应用级服务发现解析解释的非常详细。在这里简单解释,通俗来讲,在Dubbo里面我们所讲的服务指的RPC Service,粒度是接口级别的。倘若后面再接触Spring Cloud体系的微服务,则发现其中服务指的是一个应用,粒度是应用级别的(之前我就很好奇为什么Spring Cloud为什么不维护接口的信息呢)。 对于Consumer而言,其更应该关注的是Provider实例的IP+PORT列表,而Provider所发布的接口信息并非是必选项。 粒度为接口级别的Dubbo服务会带来更多的数据冗余 为此,Dubbo决定把自己的微服务发现模型也向Spring Cloud去拉齐,也就是说之前在注册中心维护的数据结构从RPC Service -> Instance 变为 Application -> Instance。举个例子: RPC Service -> Instance: 1dubbo://10.74.139.59:20881/com.dubbo.spi.demo.api.IHelloService?anyhost=true&application=demo-provider&dubbo=2.6.0&generic=false&interface=com.dubbo.spi.demo.api.IHelloService&methods=hello&pid=40849&side=provider&timestamp=1594650881680 Application -> Instance 1{"name":"spring-cloud-alibaba-dubbo-provider","id":"9c5c1bce-13de-4023-b2d8-fde037ae6041","address":"10.74.139.119","port":9090,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"spring-cloud-alibaba-dubbo-provider-1","name":"spring-cloud-alibaba-dubbo-provider","metadata":{"dubbo.metadata-service.urls":"[ \\"dubbo://127.0.0.1:20880/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&application=spring-cloud-alibaba-dubbo-provider&bind.ip=127.0.0.1&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=spring-cloud-alibaba-dubbo-provider&interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&pid=15432&qos.enable=false&release=2.7.6&revision=1.0.0&side=provider&timestamp=1594977679715&version=1.0.0\\" ]","dubbo.protocols.dubbo.port":"20880","dubbo.protocols.rest.port":"9090"}},"registrationTimeUTC":1594977677964,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}} 看起来数据结构更大了,但由于服务粒度不同,总体来说需要维护的数据变少了。怎么兼容旧版本的设计?新的服务发现模型要实现对原有 Dubbo 消费端开发者的无感知迁移,即 Dubbo 继续面向 RPC 服务编程、面向 RPC 服务治理,做到对用户侧完全无感知。为此,Dubbo新增了一个MetaService的服务,服务端实例会暴露一个预定义的 MetadataService RPC 服务,消费端通过调用 MetadataService 获取每个实例 RPC 方法相关的配置信息。 服务的提供者需要维护一个元数据服务(MetadataService).服务自省架构流程更加复杂,执行动作之间的关联非常紧密。 相较于传统的 Dubbo 架构,服务自省架构的执行流程更为复杂,执行动作之间的关联非常紧密,如 Dubbo Service 服务实例注册前需要完成 Dubbo 服务 revision 的计算,并将其添加至服务实例的 metadata 中。又如当 Dubbo Service 服务实例出现变化时,Consumer 元数据需要重新计算。这些动作被 “事件”(Event)驱动,驱动者被定义为“事件分发器”( EventDispatcher ),而动作的处理则由“事件监听器”(EventListener)执行,三者均为 “Dubbo 事件“的核心组件 Dubbo 中的事件分类Dubbo 提供的事件类型如下: Dubbo进程级别:提供一些进程的勾子事件,如:DubboShutdownHookRegisteredEvent、DubboShutdownHookUnregisteredEvent、DubboServiceDestroyedEvent; 和服务发现组件生命周期有关:ServiceDiscoveryInitializingEvent、ServiceDiscoveryInitializedEvent、ServiceDiscoveryExceptionEvent、ServiceDiscoveryDestroyingEvent、ServiceDiscoveryDestroyedEvent; 和服务暴露和引用的生命周期有关:ServiceConfigExportedEvent、ServiceConfigUnexportedEvent、ReferenceConfigInitializedEvent、ReferenceConfigDestroyedEvent 和服务实例注册和注销等有关:ServiceInstancePreRegisteredEvent、ServiceInstanceRegisteredEvent、ServiceInstancePreUnregisteredEvent、ServiceInstanceUnregisteredEvent、ServiceInstancesChangedEvent; 在Dubbo源码中有一个LoggingEventListenerTest测试类可以很好的演示。 123456789101112131415161718192021222324252627282930313233343536@Test public void testOnEvent() throws Exception { URL connectionURL = URL.valueOf("file:///Users/Home"); ServiceDiscovery serviceDiscovery = new FileSystemServiceDiscovery(); serviceDiscovery.initialize(connectionURL); // ServiceDiscoveryStartingEvent listener.onEvent(new ServiceDiscoveryInitializingEvent(serviceDiscovery, serviceDiscovery)); // ServiceDiscoveryStartedEvent listener.onEvent(new ServiceDiscoveryInitializedEvent(serviceDiscovery, serviceDiscovery)); // ServiceInstancePreRegisteredEvent listener.onEvent(new ServiceInstancePreRegisteredEvent(serviceDiscovery, createInstance())); // ServiceInstanceRegisteredEvent listener.onEvent(new ServiceInstanceRegisteredEvent(serviceDiscovery, createInstance())); // ServiceInstancesChangedEvent listener.onEvent(new ServiceInstancesChangedEvent("test", singleton(createInstance()))); // ServiceInstancePreUnregisteredEvent listener.onEvent(new ServiceInstancePreUnregisteredEvent(serviceDiscovery, createInstance())); // ServiceInstanceUnregisteredEvent listener.onEvent(new ServiceInstanceUnregisteredEvent(serviceDiscovery, createInstance())); // ServiceDiscoveryStoppingEvent listener.onEvent(new ServiceDiscoveryDestroyingEvent(serviceDiscovery, serviceDiscovery)); // ServiceDiscoveryStoppedEvent listener.onEvent(new ServiceDiscoveryDestroyedEvent(serviceDiscovery, serviceDiscovery)); } LoggingEventListenerTest模拟了服务发现组件从初始化到服务注册、注销、到组件注销的整个生命周期,测试结果如下: 12345678910[28/07/20 00:13:08:661 CST] INFO logger.LoggerFactory: using logger: org.apache.dubbo.common.logger.log4j.Log4jLoggerAdapter[28/07/20 00:13:08:773 CST] INFO listener.LoggingEventListener: [DUBBO] org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18 is initializing..., dubbo version: , current host: 192.168.1.110[28/07/20 00:13:08:773 CST] INFO listener.LoggingEventListener: [DUBBO] org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18 is initialized., dubbo version: , current host: 192.168.1.110[28/07/20 00:13:08:776 CST] INFO listener.LoggingEventListener: [DUBBO] DefaultServiceInstance{id='45903280824015', serviceName='A', host='127.0.0.1', port=8080, enabled=true, healthy=true, metadata={dubbo.metadata-service.url-params={"dubbo":{"application":"dubbo-provider-demo","deprecated":"false","group":"dubbo-provider-demo","version":"1.0.0","timestamp":"1564845042651","dubbo":"2.0.2","provider.host":"192.168.0.102","provider.port":"20880"}}, dubbo.metadata-service.urls=[ "dubbo://192.168.0.102:20881/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&application=spring-cloud-alibaba-dubbo-provider&bind.ip=192.168.0.102&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=spring-cloud-alibaba-dubbo-provider&interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&pid=17134&qos.enable=false&register=true&release=2.7.3&revision=1.0.0&side=provider&timestamp=1564826098503&version=1.0.0" ]}} is registering into org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18..., dubbo version: , current host: 192.168.1.110[28/07/20 00:13:08:777 CST] INFO listener.LoggingEventListener: [DUBBO] DefaultServiceInstance{id='45903281361282', serviceName='A', host='127.0.0.1', port=8080, enabled=true, healthy=true, metadata={dubbo.metadata-service.url-params={"dubbo":{"application":"dubbo-provider-demo","deprecated":"false","group":"dubbo-provider-demo","version":"1.0.0","timestamp":"1564845042651","dubbo":"2.0.2","provider.host":"192.168.0.102","provider.port":"20880"}}, dubbo.metadata-service.urls=[ "dubbo://192.168.0.102:20881/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&application=spring-cloud-alibaba-dubbo-provider&bind.ip=192.168.0.102&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=spring-cloud-alibaba-dubbo-provider&interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&pid=17134&qos.enable=false&register=true&release=2.7.3&revision=1.0.0&side=provider&timestamp=1564826098503&version=1.0.0" ]}} has been registered into org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18., dubbo version: , current host: 192.168.1.110[28/07/20 00:13:08:777 CST] INFO listener.LoggingEventListener: [DUBBO] The services'[name : test] instances[size : 1] has been changed., dubbo version: , current host: 192.168.1.110[28/07/20 00:13:08:778 CST] INFO listener.LoggingEventListener: [DUBBO] DefaultServiceInstance{id='45903282570448', serviceName='A', host='127.0.0.1', port=8080, enabled=true, healthy=true, metadata={dubbo.metadata-service.url-params={"dubbo":{"application":"dubbo-provider-demo","deprecated":"false","group":"dubbo-provider-demo","version":"1.0.0","timestamp":"1564845042651","dubbo":"2.0.2","provider.host":"192.168.0.102","provider.port":"20880"}}, dubbo.metadata-service.urls=[ "dubbo://192.168.0.102:20881/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&application=spring-cloud-alibaba-dubbo-provider&bind.ip=192.168.0.102&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=spring-cloud-alibaba-dubbo-provider&interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&pid=17134&qos.enable=false&register=true&release=2.7.3&revision=1.0.0&side=provider&timestamp=1564826098503&version=1.0.0" ]}} is registering from org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18..., dubbo version: , current host: 192.168.1.110[28/07/20 00:13:08:778 CST] INFO listener.LoggingEventListener: [DUBBO] DefaultServiceInstance{id='45903282953719', serviceName='A', host='127.0.0.1', port=8080, enabled=true, healthy=true, metadata={dubbo.metadata-service.url-params={"dubbo":{"application":"dubbo-provider-demo","deprecated":"false","group":"dubbo-provider-demo","version":"1.0.0","timestamp":"1564845042651","dubbo":"2.0.2","provider.host":"192.168.0.102","provider.port":"20880"}}, dubbo.metadata-service.urls=[ "dubbo://192.168.0.102:20881/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&application=spring-cloud-alibaba-dubbo-provider&bind.ip=192.168.0.102&bind.port=20881&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=spring-cloud-alibaba-dubbo-provider&interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&pid=17134&qos.enable=false&register=true&release=2.7.3&revision=1.0.0&side=provider&timestamp=1564826098503&version=1.0.0" ]}} has been unregistered from org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18., dubbo version: , current host: 192.168.1.110[28/07/20 00:13:08:778 CST] INFO listener.LoggingEventListener: [DUBBO] org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18 is stopping..., dubbo version: , current host: 192.168.1.110[28/07/20 00:13:08:778 CST] INFO listener.LoggingEventListener: [DUBBO] org.apache.dubbo.registry.client.FileSystemServiceDiscovery@71d15f18 is stopped., dubbo version: , current host: 192.168.1.110 Dubbo Spring Cloud 的事件驱动机制在Spring Cloud Alibaba中也有类似的事件驱动设计 区别于Dubbo的事件驱动基于语言级别的Event抽象,Spring Cloud Alibaba则是基于Spring ApplicationContext 框架级别的事件发布机制。具体实现可自行阅读源码,在com.alibaba.cloud.dubbo.registry.event下有四个类: ServiceInstancePreRegisteredEvent:服务预注册事件; ServiceInstanceRegisteredEvent:服务注册完毕事件; ServiceInstancesChangedEvent:服务实例变更事件; SubscribedServicesChangedEvent:所订阅服务变更事件; 引用 事件驱动程序设计 Dubbo 迈出云原生重要一步 - 应用级服务发现解析 Apache-Dubbo-服务自省架构设计","categories":[{"name":"RPC","slug":"RPC","permalink":"https://lazyallen.github.io/categories/RPC/"}],"tags":[{"name":"Dubbo","slug":"Dubbo","permalink":"https://lazyallen.github.io/tags/Dubbo/"}]},{"title":"解惑:对于SPI的一些理解","slug":"java-spi-think","date":"2020-10-01T16:00:00.000Z","updated":"2022-12-07T07:04:01.086Z","comments":true,"path":"2020/10/02/java-spi-think/","link":"","permalink":"https://lazyallen.github.io/2020/10/02/java-spi-think/","excerpt":"很早就了解过SPI的概念,刚开始知道是看JDBC驱动实现的时候发现有用到,后面陆陆续续发现在Dubbo中也有Dubbo SPI的概念,希望可以把自己的理解和困惑记录下来。","text":"很早就了解过SPI的概念,刚开始知道是看JDBC驱动实现的时候发现有用到,后面陆陆续续发现在Dubbo中也有Dubbo SPI的概念,希望可以把自己的理解和困惑记录下来。 SPI解决了什么问题?一种技术的产生必然有其产生的原因,如果现有的技术可以满足解决,就不会出现一种新的解决方案。简单来说,Java SPI实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。简单理解,既然策略模式可以解决,为什么还要SPI呢?这里举一个例子。 12345678910111213interface Pay{}class AliPay implements pay{}class WeChatPay implements Pay{}class Bussiness{ //使用支付宝 Pay pay = new AliPay(); //老板跟阿里吵架了,该用微信支付了 Pay pay = new WeChat();} 对于支付方式来说,目前市场上有2种主流的支付方式。公司目前使用的是支付宝,突然有一天,老板接受了一篇专访,结果马爸爸骂老板是“三姓家奴”,老板一怒之下把支付宝给下了,改用微信支付。这时,对于开发者来说的改动看起来简单只是new 一个新的实现而已。直接改动原来的代码不是说不可以,只是违背了开闭原则的设计原则,对扩展开发,对修改关闭。换一个角度,你永远无法直接需求会怎么变动,万一哪天老板要求改回来怎么办。 SPI的实现原理对于上一节的例子,可以采用SPI的方式,具体Java SPI的使用这里不做过多解释。可以简单看下JDBC中的例子。 12ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator(); 在进入代码之前,我们可以猜测一下实现方式如何。在功能表象而言,SPI提供的能力其实就是在应用的运行期,加载并实例化 META-INF/services配置中定义的类。进入load方法发现也差不多,这里给出关键的代码片段。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else //从配置文件找到类的全限定名 configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { //触发类的加载 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } JDBC中的SPI机制JDK制定了JDBC规范,落实到代码层面可简单认为JDK提供了JDBC interface接口,各个数据库厂商根据这一份规范做各自的实现。当我们需要使用MySql时,就可以引入相应的依赖,同时找到MySql的实现去使用,正好符合SPI 的应用场景。简单分析一下,JDBC的SPI的使用逻辑。入口在conn = DriverManager.getConnection(url, username, password);,当执行到这一句代码时,会触发DriverManager的加载和初始化,进入DriverManager可以看到下面的static的静态代码块,我们知道JVM在加载类时会触发static代码块的执行。 12345678/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */ static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } 从代码块的注释可以看出,loadInitialDrivers() 做的事情仅仅是加载初始化的JDBC驱动,这里会首先去检查系统配置中的jdbc.properties配置,之后再会使用ServiceLoader去加载。 可以看到,具体的实现类为com.mysql.cj.jdbc.Driver,也就是说此时会触发com.mysql.cj.jdbc.Driver的加载初始化,我们在看一下com.mysql.cj.jdbc.Driver的代码。 123456789101112public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } }} com.mysql.cj.jdbc.Driver 的做的事情非常简单,就是把自己注册到DriverManager中,注意,此时com.mysql.cj.jdbc.Driver 可以看作已经完成了加载初始化,并new出了一个实例注册到了DriverManager中。依旧从conn = DriverManager.getConnection(url, username, password);的方法可以看到DriverManager是如何使用registeredDrivers的,从注释中可以看到会从已注册的registeredDrivers逐个尝试进行一次连接,拿到正确的Connection就返回。 123456789101112131415161718192021222324252627// Walk through the loaded registeredDrivers attempting to make a connection. // Remember the first exception that gets raised so we can reraise it. SQLException reason = null; for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } 另外多提一点,Java SPI在这里还打破了类的双亲委托机制。我们知道,JVM在加载类时,会遵循双亲委托机制,同时对于一个Class而言,其中的依赖类也会使用加载该Class的类加载器去加载。比如说,Class A 中有一个 Class B的依赖,在JVM 加载 A时,假如用的是启动类加载器,此时也只能用启动类加载器去加载B (前提是 B 还没被加载到JVM中,我们知道JVM在加载Class时首先会检查该Class是否已经加载到JVM中,如果没有被加载,则使用双亲委托机制去加载)。为什么这样做呢?反面去想的话,如果A是启动类加载器加载的,A必定属于JDK的核心类,倘若A中的依赖不由启动类加载器去加载,而使用应用类加载器去加载,此时如果此时在应用目录伪造一个 核心的类 ,比如说Object类,让应用类加载器去加载Object类到JVM,此时必定带来安全的风险。那么类比于JDBC,DriverManager 属于JDK的核心包,可知加载DriverManager必定是启动类加载器,那么DriverManager中的依赖应该也是由启动类加载去加载,但我们知道Driver接口的实现类是第三方厂商自定义的,这些实现类必然不会被启动类加载器去加载。怎么解决这个问题呢?只要启动类加载器加载DriverManager时,提前把第三方厂商实现的Driver实现类加载到JVM中就行。所以在DriverManager 的static静态代码块中的loadInitialDrivers()的做事情就是提前加载。 RPC框架、可扩展性在后续看Dubbo 和 soft-rpc等 RPC框架中,发现均存在类似SPI的设计,例如Dubbo自己实现类一套Dubbo SPI的机制,比Java SPI更加优雅,并且可以做到按需加载。这里引用何小锋老师的一段解释: 在 RPC 框架里面,我们是怎么支持插件化架构的呢?我们可以将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离,并提供接口的默认实现。加上了插件功能之后,我们的 RPC 框架就包含了两大核心体系——核心功能体系与插件体系 这时,整个架构就变成了一个微内核架构,我们将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离并提供接口的默认实现。这样的架构相比之前的架构,有很多优势。首先它的可扩展性很好,实现了开闭原则,用户可以非常方便地通过插件扩展实现自己的功能,而且不需要修改核心功能的本身;其次就是保持了核心包的精简,依赖外部包少,这样可以有效减少开发人员引入 RPC 导致的包版本冲突问题。 一点个人理解SPI最大的好处就是,对于使用方(调用方)而言,屏蔽了变化性,服务的提供方可以动态去提供各种各样的服务(接口实现类),相比于传统的设计,由程序员自己去手动编写静态的代码逻辑去维护这种变化,一方面不符合开闭原则,一方面维护也是一个成本(如果服务很多的话,例如像Dubbo各种各样的SPI接口实现)。这种可插拔的设计,和Spring IOC设计 很像,变化时只需要改变实现方 使用方可无感知使用,对使用方来说无侵入性。","categories":[{"name":"SPI","slug":"SPI","permalink":"https://lazyallen.github.io/categories/SPI/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://lazyallen.github.io/tags/Java/"}]},{"title":"Groovy Script Console:如何找出Jenkins中所有授信失效的项目","slug":"jenkins-groovy-script-console","date":"2020-07-01T16:00:00.000Z","updated":"2022-12-07T07:06:26.376Z","comments":true,"path":"2020/07/02/jenkins-groovy-script-console/","link":"","permalink":"https://lazyallen.github.io/2020/07/02/jenkins-groovy-script-console/","excerpt":"本文来源于在一次Jenkins培训当中,有用户提问:我怎么知道自己的授信失效了。于是通过Jenkins提供的Groovy Script Console能力,编写脚本找出Jenkins中所有授信失效的项目。","text":"本文来源于在一次Jenkins培训当中,有用户提问:我怎么知道自己的授信失效了。于是通过Jenkins提供的Groovy Script Console能力,编写脚本找出Jenkins中所有授信失效的项目。 背景公司自建的CI平台基于Jenkins的,为了方便解答用户使用时碰到的问题,团队直接建立内部沟通群直接对接到用户。时间一长,发现用户经常遇到无法clone代码的问题,日志类似如下。 仔细看会发现是由Authentication failed导致的,往往是由于用户密码修改后,没有同步在CI平台及时更新从而授信失效导致的。 现象由于公司网络安全管控,每个员工均要在90天周期更换一次密码。而更新密码之后,又没有手动同步到CI平台。最直接的现象就是,点进项目编辑页,会明显看到stderr: remote: HTTP Basic: Access denied的error信息。 能想到的几种解决方案当沟通群一旦有人丢出来拉不到代码的问题,绝大部分都是由这个问题导致的,而我们也群里也不厌其烦帮用户解释了一次又一次,只要更新一下对应的授信密码就行。回过头来想,如何尽可能减少重复的事情发生呢? 90天周期更新密码时,自动同步更新平台授信:可以彻底解决,但目前无法实现 主动找出平台中哪些授信是已经失效了的,主动通知用户去更新:由被动用户找变成了主动告知用户,当前最切实的方案 建议用户使用SSH方式的授信,密码过期了也没事:对于新项目可以主动引导用户使用SSH类型授信,由于历史问题,平台上已有项目绝大部分是UserPassword类型授信 以上看来,如果可以每天定时轮询平台上所有的项目的授信,找出已失效的授信的项目,发送提醒邮件给用户及时更新密码,并建议用户换成SSH授信,是目前最切实际的做法。那么问题第一步就是,如何找到失效授信的任务?个人觉得有两种思路: 遍历所有上一次运行失败的项目,查看日志中是否包含Access denied关键字 遍历所有项目,模拟调用check url请求,判断当前项目是否授信失效 对于第2点,需要补充说明:当在项目编辑页面,所看到的error提示,是Jenkins调用了check url 请求返回的校验结果。Request如下: 123Request URL: http://{host}:8081/job/test-fail-job-cased-by-credential-failed/descriptorByName/hudson.plugins.git.UserRemoteConfig/checkUrlvalue: http://git.midea.com/paas/customize-xxl-job.gitcredentialsId: c5axxx10-xxxx-4192-xxxx-25b6xx8e00b Response如下: 12<div class=error><img src='/static/18ef55c1/images/none.gif' height=16 width=1>Failed to connect to repository : Command &quot;/usr/local/bin/git ls-remote -h http://git.midea.com/paas/customize-xxl-job.git HEAD&quot; returned status code 128:<br>stdout: <br>stderr: remote: HTTP Basic: Access denied<br>fatal: Authentication failed for &#039;http://git.midea.com/paas/customize-xxl-job.git/&#039;<br></div> 接下来,我们将用Script Console 去实现以上两种思路。提前说明两点: 本次实验环境:Jenkins version 2.89.3 实验脚本不建议直接在生产环境上使用 Groovy Script Console 介绍Jenkins 提供了一个 Groovy 脚本控制台,允许用户在 Jenkins 主运行时或代理上的运行时中运行任意的 Groovy 脚本。它提供了可以做很多事情的能力: 创建子进程,并在 Jenkins master 和agents上执行任意命令; 它甚至可以读取 Jenkins master上拥有访问权限的文件(比如 /etc/passwd); 甚至可以解密Jenkins配置凭据; 普通用户若拥有使用Groovy Script Console 权限等同于拥有管理员权限; Groovy Script Console之所以如此强大,是因为它最初是为 Jenkins 开发人员设计的一个调试界面,但后来发展成为 Jenkins Admin 用来配置 Jenkins 和调试 Jenkins runtime问题的一个界面; 由于Groovy Script Console提供了强大的功能,Jenkins 及其Agents不应该以 root 用户身份在 Linux 上运行,在任何操作系统上也不应该以 root 用户身份运行; 确保您的 Jenkins instance的安全; 在Agents上运行Groovy Script可以在节点管理侧边栏菜单,点击脚本控制台即可。除此之外,也可以在Master的Groovy Script Console运行脚本执行到Agents上 1234567891011121314151617import hudson.util.RemotingDiagnosticsimport jenkins.model.JenkinsString agent_name = 'your agent name'//groovy script you want executed on an agentgroovy_script = '''println System.getenv("PATH")println "uname -a".execute().text'''.trim()String resultJenkins.instance.slaves.find { agent -> agent.name == agent_name}.with { agent -> result = RemotingDiagnostics.executeGroovy(groovy_script, agent.channel)}println result 支持远程访问通过Bash提交Groovy文件 12curl --user 'username:api-token' --data-urlencode \\ "script=$(< ./somescript.groovy)" https://jenkins/scriptText 示例案例以下仓库中有许多可以作为参考的Groovy Script,基本上涉及大部分用户需要的场景: Cloudbees jenkins-脚本库 Sam Gleske 的 jenkins-script-console-scripts 存储库由于 Groovy 脚本直接访问 Jenkins 的接口,通常已有脚本很容易因为 Jenkins 版本升级后过时,从而导致在运行脚本时返回异常,因为 Jenkins core或 Jenkins 插件中的公共方法和接口已经更改。 在试用例子的时候要记住这一点。 举个栗子:一行代码,禁用所有Jobs1Jenkins.instance.getAllItems(hudson.model.AbstractProject.class).each {i -> i.setDisabled(true); i.save() } 参照Jenkins 2.89 接口文档,这行代码做的事情是获取所有的项目,一一设置为Disabled保存。 遍历所有上一次运行失败的项目,且日志中包含Access denied关键字123456789101112131415jobs = Jenkins.instance.getAllItems() jobs.each { job -> if (job instanceof com.cloudbees.hudson.plugins.folder.AbstractFolder) { return } buildNums = job.getBuilds().size() if(buildNums>0){ lastBuild = job.getLastBuild() if(lastBuild && lastBuild.result == Result.FAILURE){ isAccessDenied = lastBuild.getLog().contains('Access denied') if(isAccessDenied){ println 'JOB: ' + job.fullName + ' TimestampString: ' + lastBuild.getTimestampString2() } } } } 遍历所有的Jobs,这里忽略了folder 统计每个Job的运行次数 对于运行过的Job获取最后一次Build,并判断对应的log中是否存在Access denied关键字 打印符合条件的JOB信息 遍历所有项目,模拟调用check url请求,判断当前项目是否授信失效对于这个思路,要解决的问题是如何模拟调用check url请求,我一开始的思路是,获取到每个任务对应的仓库信息和授信信息,从授信信息中获取明文信息后,然后直接使用HTTP请求gitlab判断,太麻烦了,还不如直接请求Jenkins的check url请求。仔细观察请求URL``http://{host}:8081/job/test-fail-job-cased-by-credential-failed/descriptorByName/hudson.plugins.git.UserRemoteConfig/checkUrl猜测,Jenkins应该也是直接调用的git插件的checkUrl方法,于是找到git插件源码的UserRemoteConfig类,果然找到对应的doCheckUrl方法。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950@RequirePOST public FormValidation doCheckUrl(@AncestorInPath Item item, @QueryParameter String credentialsId, @QueryParameter String value) throws IOException, InterruptedException { // Normally this permission is hidden and implied by Item.CONFIGURE, so from a view-only form you will not be able to use this check. // (TODO under certain circumstances being granted only USE_OWN might suffice, though this presumes a fix of JENKINS-31870.) if (item == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER) || item != null && !item.hasPermission(CredentialsProvider.USE_ITEM)) { return FormValidation.ok(); } String url = Util.fixEmptyAndTrim(value); if (url == null) return FormValidation.error(Messages.UserRemoteConfig_CheckUrl_UrlIsNull()); if (url.indexOf('$') >= 0) // set by variable, can't validate return FormValidation.ok(); // get git executable on master EnvVars environment; Jenkins jenkins = Jenkins.get(); if (item instanceof Job) { environment = ((Job) item).getEnvironment(jenkins, TaskListener.NULL); } else { Computer computer = jenkins.toComputer(); environment = computer == null ? new EnvVars() : computer.buildEnvironment(TaskListener.NULL); } GitClient git = Git.with(TaskListener.NULL, environment) .using(GitTool.getDefaultInstallation().getGitExe()) .getClient(); StandardCredentials credential = lookupCredentials(item, credentialsId, url); git.addDefaultCredentials(credential); // Should not track credentials use in any checkURL method, rather should track // credentials use at the point where the credential is used to perform an // action (like poll the repository, clone the repository, publish a change // to the repository). // attempt to connect the provided URL try { git.getHeadRev(url, "HEAD"); } catch (GitException e) { return FormValidation.error(Messages.UserRemoteConfig_FailedToConnect(e.getMessage())); } return FormValidation.ok(); } 所以只要调用UserRemoteConfig.DescriptorImpl内部类的doCheckUrl方法即可,最后实现的脚本如下: 12345678910111213141516171819202122232425262728293031323334import hudson.plugins.git.*UserRemoteConfig.DescriptorImpl descriptor = Jenkins.instance.getDescriptorByType(hudson.plugins.git.UserRemoteConfig.DescriptorImpl.class)Jenkins.instance.getAllItems().each{job -> if (job instanceof com.cloudbees.hudson.plugins.folder.AbstractFolder) { return } if (job instanceof hudson.model.ExternalJob) { return } def scmlist=[] if(job instanceof org.jenkinsci.plugins.workflow.job.WorkflowJob){ scms = job.getSCMs() scmlist.addAll(scms) }else { scmlist.add(job.scm) } if(scmlist){ def errorInfo='' scmlist.each{scm -> if(scm instanceof GitSCM){ scm.userRemoteConfigs.each{ urc -> //println urc.name +' '+ urc.url +' '+ urc.credentialsId formValidation = descriptor.doCheckUrl(job,urc.credentialsId,urc.url) //println 'ERROR'==formValidation.kind.name()?formValidation:'ok' if('ERROR'==formValidation.kind.name()){ errorInfo += "url: $urc.url, reason: $formValidation" //println job.getAbsoluteUrl() + 'check git url failed,reason : '+formValidation } } } } if(errorInfo?.trim()){ println job.getAbsoluteUrl() + 'check git url failed,error info : '+errorInfo } } } 需要特别import插件的包hudson.plugins.git.* 通过Jenkins.instance.getDescriptorByType()获取UserRemoteConfig.DescriptorImpl的实例 遍历所有项目,每个项目获取对应的SCM信息,其中包含了credentialsId和url信息 调用descriptor.doCheckUrl(job,urc.credentialsId,urc.url)方法验证授信是否失效 打印所有失效的项目信息 彩蛋:解密Jenkins授信123456789101112131415161718192021import com.cloudbees.plugins.credentials.CredentialsSet<Credentials> allCredentials = new HashSet<Credentials>();def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials( com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class);allCredentials.addAll(creds)Jenkins.instance.getAllItems(com.cloudbees.hudson.plugins.folder.Folder.class).each{ f -> creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials( com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class, f) allCredentials.addAll(creds) }for (c in allCredentials) { println( ( c.properties.privateKeySource ? "ID: " + c.id + ", UserName: " + c.username + ", Private Key: " + c.getPrivateKey() : "")) println( ( c.properties.password ? "ID: " + c.id + ", UserName: " + c.username + ", Password: " + c.password : ""))} 总结感想如下: 脚本调试成本很高:将逻辑拆分为多个步骤,按照步骤一步一步完善代码,上一步跑通后再实现下一步; 对照Jenkins文档:建议参照对应版本Jenkins的Javadoc,减少出错; 经常出现“无该方法签名”的错误:通常是脚本中的字段或方法在当前版本的Jenkins中不存在,请检查Javadoc的版本和Jenkins版本是否一致; 观察Jenkins是如何实现的:别着急实现,可以看看Jenkins自己是如何实现的,不然可能会走弯路; 之前实现过一个功能,实时获取Jenkins正在执行和正在队列中的Job信息,当时还不知道有Script Console这种东西,实现方案是调用Jenkins 的HTTP API,获取XML的内容,再用特定的解析语法获取到Job信息。能获取到的Job字段内容很少,且还要处理XML解析逻辑。现在看来使用Script Console实现会更加简单快捷。 引用 Script Console git-plugin","categories":[{"name":"工具","slug":"工具","permalink":"https://lazyallen.github.io/categories/%E5%B7%A5%E5%85%B7/"}],"tags":[{"name":"Jenkins","slug":"Jenkins","permalink":"https://lazyallen.github.io/tags/Jenkins/"},{"name":"Groovy","slug":"Groovy","permalink":"https://lazyallen.github.io/tags/Groovy/"},{"name":"CICD","slug":"CICD","permalink":"https://lazyallen.github.io/tags/CICD/"}]},{"title":"深入浅出MyBatis 源码:配置文件解析","slug":"read-code-mybatis-configuration","date":"2020-05-11T16:00:00.000Z","updated":"2022-12-07T07:04:26.940Z","comments":true,"path":"2020/05/12/read-code-mybatis-configuration/","link":"","permalink":"https://lazyallen.github.io/2020/05/12/read-code-mybatis-configuration/","excerpt":"本文将会介绍MyBatis配置文件解析部分的代码解读,从创建一个SqlSessionFactory作为入口,引入MyBatis配置文件的说明。简要说明配置文件中常用标签的用法和说明,根据每个标签,详细介绍MyBatis是如何解析这些标签的。","text":"本文将会介绍MyBatis配置文件解析部分的代码解读,从创建一个SqlSessionFactory作为入口,引入MyBatis配置文件的说明。简要说明配置文件中常用标签的用法和说明,根据每个标签,详细介绍MyBatis是如何解析这些标签的。 创建一个SqlSessionFactory的几种方式 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。 123String resource = "org/mybatis/example/mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); 从上文的实例代码可以看出,sqlSessionFactory 实例是通过SqlSessionFactoryBuilder 的build 方法构建出来的。我们进入SqlSessionFactoryBuilder 中可以看到。 12345678910111213141516public class SqlSessionFactoryBuilder { public SqlSessionFactory build(Reader reader){...}; public SqlSessionFactory build(Reader reader, String environment){...}; public SqlSessionFactory build(Reader reader, Properties properties){...}; public SqlSessionFactory build(Reader reader, String environment, Properties properties){...}; public SqlSessionFactory build(InputStream inputStream){...}; public SqlSessionFactory build(InputStream inputStream, String environment){...}; public SqlSessionFactory build(InputStream inputStream, Properties properties){...}; public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties){...}; public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); };} SqlSessionFactoryBuilder拥有9个build 重载方法,大概可以分为两类: 以XML配置文件输入流的方式,如Read 字符流或 InputStream字节流 预先实例化一个Configuration 实例 SqlSessionFactoryBuilder 从命名可以看出就是SqlSessionFactory构建器,功能是去构建出SqlSessionFactory实例,而SqlSessionFactory 再去构建出SqlSession , 这个SqlSession 可以理解为数据库客户端连接服务端的会话。在现实生活中,我们是知道数据库服务器的主机地址,端口,用户名,密码等信息的,对应用而言,也应该有个「地方」去记录这些配置信息,在MyBatis中,这个「地方」就是「配置文件」,一般命名为mybatis-config.xml。XML文件是文件层面的「配置」,而Configuration 类是Java Class层面上的「配置」。 MyBatis 配置文件的使用一般MyBatis 的配置文件是一个XML文件, 1234567891011121314151617181920<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="org/mybatis/example/BlogMapper.xml"/> </mappers></configuration> 大概可以分为两部分: XML头部声明DTD文件(Document Type Definition):用于验证格式的正确性,关于DTD的介绍可以查看这里。 以configuration为头节点的配置节点树:用于设置MyBatis的行为和属性 这里稍微多说一句,配置文件的头部声明是HTTP协议的,那是不是意味着校验XML合法性时必须请求网络一次?在初始化XMLConfigBuilder时,发现也同时传入了一个XMLMapperEntityResolver实例。 123public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); } 可以看到XMLMapperEntityResolver#resolveEntity方法中,会直接读取存在本地的dtd文件,这样保证了就算处于离线环境依旧可以成功校验配置文件。 123456789101112131415161718192021222324252627/** * 本地 mybatis-config.dtd 文件 */ private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd"; /** * 本地 mybatis-mapper.dtd 文件 */ private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd"; @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException { try { if (systemId != null) { String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH); // 本地 mybatis-config.dtd 文件 if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) { return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId); // 本地 mybatis-mapper.dtd 文件 } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) { return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId); } } return null; } catch (Exception e) { throw new SAXException(e.toString()); } } 从文档得知全部配置如下,关于配置的解释和使用这里不作具体说明,文档的解释比较详细。 configuration(配置) properties(属性) settings(设置) typeAliases(类型别名) typeHandlers(类型处理器) objectFactory(对象工厂) plugins(插件) environments(环境配置) environment(环境变量) transactionManager(事务管理器) dataSource(数据源) databaseIdProvider(数据库厂商标识) mappers(映射器) MyBatis 配置文件的使用的parse 过程MyBatis 配置文件的解析就是将XML配置转换为Configuration实例的过程。可以分为两步: 第一步:通过XPathParser将XML转换为org.w3c.dom.Document对象 第二步:通过eval 节点属性,设置Configuration属性 123456789101112131415161718public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { // 创建 XMLConfigBuilder 对象,执行 XML 解析 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); // 创建 DefaultSqlSessionFactory 对象 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } 回过头再看SqlSessionFactoryBuilder#build方法,创建 XMLConfigBuilder 对象时就完成转换为Document对象的过程,parser.parse()做的就是构造Configuration实例。 创建 org.w3c.dom.Document 对象XMLConfigBuilder持有XPathParser,XPathParser持有Document,在构造XMLConfigBuilder时,同时也会构造XPathParser,在构造XPathParser时通过调用createDocument方法设置了该属性。 1234public XPathParser(InputStream inputStream, boolean validation, Properties variables) { commonConstructor(validation, variables, null); this.document = createDocument(new InputSource(inputStream)); } 构造Configuration XMLConfigBuilder的parse方法会返回一个Configuration实例。 12345678910111213141516/** * 解析 XML 成 Configuration 对象。 * * @return Configuration 对象 */ public Configuration parse() { // 若已解析,抛出 BuilderException 异常 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 标记已解析 parsed = true; // 解析 XML configuration 节点 parseConfiguration(parser.evalNode("/configuration")); return configuration; } 对于配置文件的解析全部体现在parseConfiguration(parser.evalNode("/configuration"));,进入到该方法中可以看到,这个方法做的事情就是去一一解析Document对象中的标签,然后将解析后的标签值设置到configuration实例中。其中的每一个方法都代表了对一种配置解析。 12345678910111213141516171819202122232425262728293031323334private void parseConfiguration(XNode root) { try { //issue #117 read properties first // 解析 <properties /> 标签 propertiesElement(root.evalNode("properties")); // 解析 <settings /> 标签 Properties settings = settingsAsProperties(root.evalNode("settings")); // 加载自定义的 VFS 实现类 loadCustomVfs(settings); // 解析 <typeAliases /> 标签 typeAliasesElement(root.evalNode("typeAliases")); // 解析 <plugins /> 标签 pluginElement(root.evalNode("plugins")); // 解析 <objectFactory /> 标签 objectFactoryElement(root.evalNode("objectFactory")); // 解析 <objectWrapperFactory /> 标签 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); // 解析 <reflectorFactory /> 标签 reflectorFactoryElement(root.evalNode("reflectorFactory")); // 赋值 <settings /> 到 Configuration 属性 settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 // 解析 <environments /> 标签 environmentsElement(root.evalNode("environments")); // 解析 <databaseIdProvider /> 标签 databaseIdProviderElement(root.evalNode("databaseIdProvider")); // 解析 <typeHandlers /> 标签 typeHandlerElement(root.evalNode("typeHandlers")); // 解析 <mappers /> 标签 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } 由于配置项过多,我们这对其中几个比较常用的配置解析进行说明。 解析properties标签1234567891011<properties resource="org/mybatis/example/config.properties"> <property name="username" value="dev_user"/> <property name="password" value="F2Fa3!33TYyg"/></properties><dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/></dataSource> properties的作用简单说就是去动态替换属性的值。properties基本和Java语言中的Properties是一个意思,其本质就体现Configuration中的variables字段。 123456/** * 变量 Properties 对象。 * * 参见 {@link org.apache.ibatis.builder.xml.XMLConfigBuilder#propertiesElement(XNode context)} 方法 */ protected Properties variables = new Properties(); 细心一点就会发现,其实Properties不仅可以在XML配置中定义,而且还可以通过读取Properties方法,以及通过XMLConfigBuilder(reader, environment, properties)的方式传入到variables中。 XML配置中的Properties 应用中的Properties文件 作为XMLConfigBuilder构造方法参数传递的Properties当以上3个地方都有相同名字的Properties时,那么MyBatis会用哪一个呢?对于这一点,文档中有特别解释。 如果属性在不只一个地方进行了配置,那么 MyBatis 将按照下面的顺序来加载:在 properties 元素体内指定的属性首先被读取。然后根据 properties 元素中的 resource 属性读取类路径下属性文件或根据 url 属性指定的路径读取属性文件,并覆盖已读取的同名属性。最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的是 properties 属性中指定的属性。 12345678910111213141516171819202122232425262728private void propertiesElement(XNode context) throws Exception { if (context != null) { // 读取XML子标签们,为 Properties 对象 Properties defaults = context.getChildrenAsProperties(); // 读取Properties文件 resource 和 url 属性 String resource = context.getStringAttribute("resource"); String url = context.getStringAttribute("url"); if (resource != null && url != null) { // resource 和 url 都存在的情况下,抛出 BuilderException 异常 throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); } // 读取本地 Properties 配置文件到 defaults 中。 if (resource != null) { defaults.putAll(Resources.getResourceAsProperties(resource)); // 读取远程 Properties 配置文件到 defaults 中。 } else if (url != null) { defaults.putAll(Resources.getUrlAsProperties(url)); } // 作为XMLConfigBuilder构造方法参数传递的Properties // 覆盖 configuration 中的 Properties 对象到 defaults 中。 Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } // 设置 defaults 到 parser 和 configuration 中。 parser.setVariables(defaults); configuration.setVariables(defaults); } } 以上做了两件事: 依次分别读取XML配置的Properties标签、读取本地文件系统或远程网络的Properties配置文件(取决于<properties>节点的 resource 和 url 是否为空)、作为XMLConfigBuilder构造方法的Properties参数,将其全部putAll到defaults中。 设置 defaults 到 parser 和 configuration 中所以,优先级最高的是作为XMLConfigBuilder构造方法的Properties参数、其次是本地文件系统或远程网络的Properties配置文件、优先级最低的是XML配置的Properties标签。 解析settings标签settings是对MyBatis 行为和属性的定义,比如useGeneratedKeys这个setting代表的含义就是允许 JDBC 支持自动生成主键。 12345678910111213141516private Properties settingsAsProperties(XNode context) { // 将子标签,解析成 Properties 对象 if (context == null) { return new Properties(); } Properties props = context.getChildrenAsProperties(); // Check that all settings are known to the configuration class // 校验每个属性,在 Configuration 中,有相应的 setting 方法,否则抛出 BuilderException 异常 MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); for (Object key : props.keySet()) { if (!metaConfig.hasSetter(String.valueOf(key))) { throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); } } return props; } 以上做了两件事: 解析setting标签到props 反射Configuration,循环props,调用metaConfig.hasSetter方法检测Configuration中是否持有对应的setter方法,否则抛出异常。 为什么需要这样做,我的理解是在对settings设置到Configuration前端,需要先进行安全检查,从而保证用于填写的settings标签都是正确的。 解析typeAliases标签typeAliases中文直译就是「类型别名」,解决的问题在于简化类的全限名的冗余。 123456789<typeAliases> <typeAlias alias="Author" type="domain.blog.Author"/> <typeAlias alias="Blog" type="domain.blog.Blog"/> <typeAlias alias="Comment" type="domain.blog.Comment"/> <typeAlias alias="Post" type="domain.blog.Post"/> <typeAlias alias="Section" type="domain.blog.Section"/> <typeAlias alias="Tag" type="domain.blog.Tag"/> <package name="domain.blog"/></typeAliases> 现在当前的XML文件中,如果之后在resultType中要用到domain.blog.Author类型引用,就只要用Author就可以代替。在typeAliases标签中支持两种注册别名的方式: 准确注册:typeAlias标签,定义alias为别名,type为别名引用,用于为准确的一个类注册别名; 包注册:package标签,name属性,将整个包中的类都注册为别名,除非类中已经声明@Alias注解,否则别名默认为类的小写。 123456789101112131415161718192021222324252627private void typeAliasesElement(XNode parent) { if (parent != null) { // 遍历子节点 for (XNode child : parent.getChildren()) { // 指定为包的情况下,注册包下的每个类 if ("package".equals(child.getName())) { String typeAliasPackage = child.getStringAttribute("name"); configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); // 指定为类的情况下,直接注册类和别名 } else { String alias = child.getStringAttribute("alias"); String type = child.getStringAttribute("type"); try { Class<?> clazz = Resources.classForName(type); // 获得类是否存在 // 注册到 typeAliasRegistry 中 if (alias == null) { typeAliasRegistry.registerAlias(clazz); } else { typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { // 若类不存在,则抛出 BuilderException 异常 throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } } 以上做了3件事: 遍历typeAlias标签 匹配到package时,将整个包的类都注册为别名 否则直接注册类和别名,注册之前需要先检查一下类是否存在,不存在就跑出异常 123456789101112131415161718192021222324252627public class TypeAliasRegistry { /** * 类型与别名的映射。 */ private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<>(); /** * 初始化默认的类型与别名 * * 另外,在 {@link org.apache.ibatis.session.Configuration} 构造方法中,也有默认的注册 */ public TypeAliasRegistry() { registerAlias("string", String.class); registerAlias("byte", Byte.class); registerAlias("long", Long.class); registerAlias("short", Short.class); registerAlias("int", Integer.class); registerAlias("integer", Integer.class); registerAlias("double", Double.class); registerAlias("float", Float.class); registerAlias("boolean", Boolean.class); ... ... }} 可以看到,注册到MyBatis中的别名和类都存在一个叫typeAliasRegistry字段中,打开TypeAliasRegistry这个类,发现其持有一个以String为key,Class<?>为value的hashmap,而在构造方法中也初始化了一系列的JDK原生的对象类型,这也解释了我们不用自己动手再去注册这个基础对象类型。在注释中,说到在Configuration的构造方法中也有类似的注册,其中注册大多都是业务对象的别名。 1234567public Configuration() { // 注册到 typeAliasRegistry 中 begin ~~~~ typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class); ... ... } 这里还有个问题没解决,MyBatis时怎么处理没有声明alias这种情况的呢?比如下面这种场景 123<typeAliases> <typeAlias type="domain.blog.Author"/></typeAliases> 点进TypeAliasRegistry#registerAlias方法则解释了对该情况的处理 12345678910111213141516171819202122232425//alias为null的情况public void registerAlias(Class<?> type) { // 默认为,简单类名,获取全路径类名的简称,比如, 全限定类名 xyz.coolblog.model.Author 的别名为 author。 String alias = type.getSimpleName(); // 如果有注解,使用注册上的名字 Alias aliasAnnotation = type.getAnnotation(Alias.class); if (aliasAnnotation != null) { alias = aliasAnnotation.value(); } // 注册类型与别名的注册表 registerAlias(alias, type); } public void registerAlias(String alias, Class<?> value) { if (alias == null) { throw new TypeException("The parameter alias cannot be null"); } // issue #748 // 转换成小写 String key = alias.toLowerCase(Locale.ENGLISH); if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) { // 冲突,抛出 TypeException 异常 throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'."); } TYPE_ALIASES.put(key, value); } MyBatis对于alias为null的情况的处理方式是直接获取注册的这个类的SimpleName,之后再检查这个类中是否存在alias注解,有的话直接用声明的注解中的值,注册到typeAliasRegistry之前需要将alias全部小写。 解析environments标签一般而言,我们通常在配置文件中定义environment 12345678910111213<environments default="development"> <environment id="development"> <transactionManager type="JDBC"> <property name="..." value="..."/> </transactionManager> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment></environments> environments的中文解释是「环境配置」,在MyBatis的文档中特别强调了,SqlSessionFactory和environment是一对一关系,也就是说,对于一个数据库环境比如MySQL环境,需要一个SqlSessionFactory实例,但如果这时需要增加一个数据库环境(比如Oracle环境或者另外一台MySQL主机环境),则需要再添加一个environment,再添加一个SqlSessionFactory实例。所以不会出现说,一个SqlSessionFactory实例同时选择多个environment的情况。 尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。每个数据库对应一个 SqlSessionFactory 实例 这样就解释了,在创建SqlSessionFactory实例时,可以通过直接将environment作为入参的方式。 12SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties); 而environment在MyBatis 中便是org.apache.ibatis.mapping.Environment这个类,environmentsElement方法做的事情就是,先找出default环境,遍历environments节点找出对应的环境配置,然后初始化TransactionFactory、DataSourceFactory、DataSource 这些实例,最后再构造出该环境并设置到configuration中。 1234567891011121314151617181920212223242526private void environmentsElement(XNode context) throws Exception { if (context != null) { // environment 属性非空,从 default 属性获得 if (environment == null) { environment = context.getStringAttribute("default"); } // 遍历 XNode 节点 for (XNode child : context.getChildren()) { // 判断 environment 是否匹配 String id = child.getStringAttribute("id"); if (isSpecifiedEnvironment(id)) { // 解析 `<transactionManager />` 标签,返回 TransactionFactory 对象 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); // 解析 `<dataSource />` 标签,返回 DataSourceFactory 对象 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); // 创建 Environment.Builder 对象 Environment.Builder environmentBuilder = new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); // 构造 Environment 对象,并设置到 configuration 中 configuration.setEnvironment(environmentBuilder.build()); } } } } 解析mappers标签在MyBatis 中有两种配置文件: 全局配置的Configuration文件 用于记录SQL语句的Mapper文件 在Configuration 文件中,mappers标签的用处就是去声明具体执行的SQL语句记录在哪里。mappers标签支持4种声明方式: 使用相对于类路径的资源引用:<mapper resource="abc.xml"/> 使用完全限定资源定位符(URL):<mapper url="file:///abc.xml"/> 使用映射器接口实现类的完全限定类名:<mapperclass="abcMapper"/> 将包内的映射器接口实现全部注册为映射器:<package name="org.mybatis.builder"/> 我的理解以上的声明方式可以分为两类:一类是引用XML文件,一类是引用Mapper Interface。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647private void mapperElement(XNode parent) throws Exception { if (parent != null) { // 遍历子节点 for (XNode child : parent.getChildren()) { // 如果是 package 标签,则扫描该包 if ("package".equals(child.getName())) { // 获得包名 String mapperPackage = child.getStringAttribute("name"); // 添加到 configuration 中 configuration.addMappers(mapperPackage); // 如果是 mapper 标签, } else { // 获得 resource、url、class 属性 String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); // 使用相对于类路径的资源引用 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); // 获得 resource 的 InputStream 对象 InputStream inputStream = Resources.getResourceAsStream(resource); // 创建 XMLMapperBuilder 对象 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); // 执行解析 mapperParser.parse(); // 使用完全限定资源定位符(URL) } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); // 获得 url 的 InputStream 对象 InputStream inputStream = Resources.getUrlAsStream(url); // 创建 XMLMapperBuilder 对象 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); // 执行解析 mapperParser.parse(); // 使用映射器接口实现类的完全限定类名 } else if (resource == null && url == null && mapperClass != null) { // 获得 Mapper 接口 Class<?> mapperInterface = Resources.classForName(mapperClass); // 添加到 configuration 中 configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } } MyBatis解析mappers标签时,会便利每一个mapper的节点,首先区分是否为package,若属性不为package则检测其他3种情况,根据每种情况再具体处理。可以跟进package的代码往下看。 123456789101112131415161718192021222324252627public <T> void addMapper(Class<T> type) { // 判断,必须是接口。 if (type.isInterface()) { // 已经添加过,则抛出 BindingException 异常 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // 添加到 knownMappers 中 knownMappers.put(type, new MapperProxyFactory<>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. // 解析 Mapper 的注解配置 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); // 标记加载完成 loadCompleted = true; } finally { // 若加载未完成,从 knownMappers 中移除 if (!loadCompleted) { knownMappers.remove(type); } } } } 以上做的事情时将package下的interface全部注册到MapperRegistry中,在MapperRegistry中持有knownMappers去存储这些interface的信息。到现在为止,只是处理了interface,那么真实映射的那些XML配置文件时在哪里处理的呢。可以跟进到parser.parse();中。 1234567891011121314151617181920212223242526272829303132public void parse() { // 判断当前 Mapper 接口是否应加载过。 String resource = type.toString(); if (!configuration.isResourceLoaded(resource)) { // 加载对应的 XML Mapper 文件 loadXmlResource(); // 标记该 Mapper 接口已经加载过 configuration.addLoadedResource(resource); // 设置 namespace 属性 assistant.setCurrentNamespace(type.getName()); // 解析 @CacheNamespace 注解 parseCache(); // 解析 @CacheNamespaceRef 注解 parseCacheRef(); // 遍历每个方法,解析其上的注解 Method[] methods = type.getMethods(); for (Method method : methods) { try { // issue #237 if (!method.isBridge()) { // 执行解析 parseStatement(method); } } catch (IncompleteElementException e) { // 解析失败,添加到 configuration 中 configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } // 解析待定的方法 parsePendingMethods(); } loadXmlResource() 做的事情主要是对于Mapper XML文件的处理,具体怎么解析在下一篇文章中介绍。loadedResources用来存储已加载资源的信息。最后解析后的语句信息保存在mappedStatements中,mappedStatements是一个HashMap,key是namespace.id 的字符,具体执行的SQL语句信息就是value,在实际debug中,发现同时也存储了相同一份为id的Entry。不知道MyBatis为什么要这样做。 总结回过头来,我们再看如何去生成一个SqlSessionFactory,从SqlSessionFactoryBuilder的build方法中我们可以找到答案。 123456789101112131415161718public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { // 创建 XMLConfigBuilder 对象 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); // 执行 XML 解析 // 创建 DefaultSqlSessionFactory 对象 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } build方法做的事情就是读取配置文件XML转换为Configuration实例。其中最关键的两句代码代表来这个过程的两个阶段。 创建 XMLConfigBuilder 对象,就是读取全局配置XML文件到DOM对象的过程,这个过程中包含对XML配置的校验。 从DOM对象到Configuration对象的过程,解析全局配置XML各个标签,以及解析mapper标签中声明的mapper.xml文件。 参考引用MyBatis 文档","categories":[{"name":"框架","slug":"框架","permalink":"https://lazyallen.github.io/categories/%E6%A1%86%E6%9E%B6/"}],"tags":[{"name":"MyBatis","slug":"MyBatis","permalink":"https://lazyallen.github.io/tags/MyBatis/"},{"name":"源码","slug":"源码","permalink":"https://lazyallen.github.io/tags/%E6%BA%90%E7%A0%81/"}]},{"title":"当执行“Java -jar springbootapp.jar --server.port=8081” 命令后会发生什么","slug":"spring-boot-properties-binder","date":"2019-11-20T16:00:00.000Z","updated":"2022-12-07T07:04:47.585Z","comments":true,"path":"2019/11/21/spring-boot-properties-binder/","link":"","permalink":"https://lazyallen.github.io/2019/11/21/spring-boot-properties-binder/","excerpt":"本篇文章的起因是同事问我下面的Jar的启动命令中,- 和 –的参数有什么区别。下意识觉得区别是 在 xxx.jar 之前的参数叫做 VM 参数 ,传入JVM中,而后面的参数 Program 参数,是传入jar中的,对应的就是main(String[] args)中的args数组,但是说到有什么别的明显区别我倒是说不上来。","text":"本篇文章的起因是同事问我下面的Jar的启动命令中,- 和 –的参数有什么区别。下意识觉得区别是 在 xxx.jar 之前的参数叫做 VM 参数 ,传入JVM中,而后面的参数 Program 参数,是传入jar中的,对应的就是main(String[] args)中的args数组,但是说到有什么别的明显区别我倒是说不上来。 1java -Xloggc:/logs/governor-service-gc.log -verbose.gc -XX:+PrintGCDateStamps -javaagent:/agent/apm-javaagent.jar -Dskywalking.collector.backend_service=11.11.11.11:1111 -Dskywalking.agent.service_name=uat-mgp-governor-xxxx -Dskywalking.agent.authentication=c8fxxxx17c46bd -jar xxx-microservice.jar --ENV_PROFILE=uat --LIFE_CIRCLE=dev --server.port=11111 --eureka.instance.ip-address=${NODE_IP} --eureka.instance.non-secure-port=${NODE_PORT_11111} --jasypt.encryptor.password=xxx -Dsun.net.inetaddr.ttl=3 由此,我心中也有了疑问: 为什么 Program 参数的写法是 – 假如都是配置server.port,VM 参数 和 Program 参数的优先级是? Spring Boot是如何处理两者的呢? VM args 和 Program args 有什么区别如何启动一个Java的应用?首先我们可以在Oracle的文档中找到答案。 java [options] -jar filename [args] options:Command-line options separated by spaces. See Options. args: The arguments passed to the main() method separated by spaces. JVM提供了Standard Options、Non-Standard Options 等6种不同的Options用作不同的场景。其中Standard Options是最常见的Options,例如 -jar filename、-version、-help 等Options。-Dproperty=value 也是Standard Options,文档中的用法解释是: Sets a system property value. The property variable is a string with no spaces that represents the name of the property. The value variable is a string that represents the value of the property. If value is a string with spaces, then enclose it in quotation marks (for example -Dfoo=”foo bar”). 设置一个系统属性值。属性变量是一个没有空格的字符串,代表属性的名称。value变量是一个表示属性值的字符串。如果value是一个带空格的字符串,则用引号括起来(例如-Dfoo=”foo bar”) 对此,明确了一点,类似-Dkey=value的VM参数最后会成为System properties。 可以看到,在文档中并没有规定 args 的写法,那么为什么在案例中需要在参数前加上--呢。由于启动的是一个Spring Boot项目,尝试去Spring Boot文档中寻找答案。在4.2.2. Accessing Command Line Properties中有说明,Spring Boot的参数均以-- 开头,成为 Command Line Properties,作为外部化参数的一种。说到外部化参数,那是不是Spring Boot还有其他的外部化参数?Spring Boot 允许外部化配置,以便在不同的环境中使用相同的应用程序代码。可以使用属性文件、 YAML 文件、环境变量和命令行参数来外部化配置,也可以使用@value注释将属性值直接注入 bean,可以通过 Spring 的 Environment 抽象访问属性值,也可以通过@configurationproperties绑定到结构化对象。对于这么多的外部化配置,就会存在相同的属性覆盖的情况,而Spring Boot按照17种不同的外部化配置规约了不同的优先级,这里引用和本篇文章相关的参数。 4.Command line arguments9.Java System properties(System.getProperties()).10.OS environment variables.15.Application properties packaged inside your jar (application.properties and YAML variants).17.Default properties (specified by setting SpringApplication.setDefaultProperties). 到这边答案就出来了,对于java -Dkey=value -jar springboot.jar --key=value这句启动命令: 前者-Dkey=value会将key:value键值对写入System properties 后者--key=value属于Spring Boot特定的Command line arguments 以上两种参数对Spring Boot而言都是外置化参数,通常情况下,Command line arguments 的优先级显然比 System properties 和 配置文件 的优先级高 举个栗子我们可以尝试去配置server.port这个参数去验证 application.properties:server.port=8088 VM options= -Dserver.port=8081 Program arguments= –server.port=8082 启动类如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243package com.lazyallen.player.command.demo;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.core.env.Environment;@SpringBootApplicationpublic class CommandDemoApplication implements ApplicationRunner { private static final Logger logger = LoggerFactory.getLogger(CommandDemoApplication.class); private static final String SERVER_PORT = "server.port"; @Autowired Environment env; public static void main(String[] args) { //Customizing SpringApplication disable command args// SpringApplication app = new SpringApplication(CommandDemoApplication.class);// app.setAddCommandLineProperties(false);// app.run(args); SpringApplication.run(CommandDemoApplication.class, args); } @Override public void run(ApplicationArguments args) throws Exception { logger.info("COMMAND ARGS:args:{}",args); logger.info("COMMAND ARGS:SourceArgs:{}",args.getSourceArgs()); logger.info("COMMAND ARGS:OptionNames:{}",args.getOptionNames()); logger.info("COMMAND ARGS:NonOptionArgs:{}",args.getNonOptionArgs()); logger.info("COMMAND ARGS:OptionValues for server.port :{}",args.getOptionValues(SERVER_PORT)); logger.info("---------------------------------------"); logger.info("SYSTEM PROPERTIES: server.port:{}",System.getProperty(SERVER_PORT)); logger.info("---------------------------------------"); logger.info("SPRING ENV:server.port:{}",env.getProperty(SERVER_PORT)); }} 这里我们通过实现ApplicationRunner传入的ApplicationArguments args 就是Program arguments,启动后查看日志,可见最后应用使用的port为8082。 1234567891011122020-09-17 23:34:02.392 INFO 6774 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8082 (http) with context path ''2020-09-17 23:34:02.400 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : Started CommandDemoApplication in 1.988 seconds (JVM running for 2.659)2020-09-17 23:34:02.401 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:args:org.springframework.boot.DefaultApplicationArguments@456abb662020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:SourceArgs:--server.port=80822020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:OptionNames:[server.port]2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:NonOptionArgs:[]2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : COMMAND ARGS:OptionValues for server.port :[8082]2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : ---------------------------------------2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : SYSTEM PROPERTIES: server.port:80812020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : ---------------------------------------2020-09-17 23:34:02.403 INFO 6774 --- [ main] c.l.p.c.demo.CommandDemoApplication : SPRING ENV:server.port:80822020-09-17 23:34:06.942 INFO 6774 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor' Think more…如果仔细看开头的案例,会发现-Dsun.net.inetaddr.ttl=3是作为Program arguments 传入应用的,那么这个参数会生效吗?按以上的说法,Spring Boot虽然可以接收这样的Program arguments,但由于其使用的是 -D 的写法,Spring Boot应该是不认识这个参数的。那么如果传入的是--sun.net.inetaddr.ttl=3会不会生效呢,笔者通过实验,发现的确通过获取Spring的环境参数,的确能获取到这个值,但从System properties 中则是null的。在文档中,对于sun.net.inetaddr.ttl的描述是This is a sun private system property,这是一个私有的System properties,笔者认为虽然能从Spring 的环境参数中获取到这个值,但这个值应该是不会生效的,因为在System properties中这个值依旧是null。 所以,不能简单的认为--key=value和-Dkey=value是差不多的,在Spring Boot中,除了优先级不同之外,--key=value并不能去变相的代替-Dkey=value,比如说企图在Program arguments中传入类似 --gc=xxx 的GC参数的骚操作肯定是不行的。 那么问题又来了,为什么--server.port就可以去代替并覆盖-Dserver.port呢?解答这个问题,我们可以尝试去找出,Spring Boot内置web容器中的port是从哪里来的? Spring Boot是如何处理 –args 参数的通过Debug发现入口在SpringApplication的run()方法中,这里我们只展示关键入口和生效步骤。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869public ConfigurableApplicationContext run(String... args) { ... //在启动时,根据Program arguments 构造出DefaultApplicationArguments ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); //准备环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); ... } protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) { MutablePropertySources sources = environment.getPropertySources(); if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) { //可以看到,defaultProperties是放在最后的,优先级最低 sources.addLast(new MapPropertySource("defaultProperties", this.defaultProperties)); } //this.addCommandLineProperties默认为true,但也可以配置为false,这时command line 就会失效 if (this.addCommandLineProperties && args.length > 0) { String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME; if (sources.contains(name)) { PropertySource<?> source = sources.get(name); CompositePropertySource composite = new CompositePropertySource(name); composite.addPropertySource( new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args)); composite.addPropertySource(source); sources.replace(name, composite); } else { sources.addFirst(new SimpleCommandLinePropertySource(args)); } } } public SimpleCommandLinePropertySource(String... args) { //可以看到SimpleCommandLineArgsParser是用来解析command line 的 super(new SimpleCommandLineArgsParser().parse(args)); }//解析command line的逻辑public CommandLineArgs parse(String... args) { CommandLineArgs commandLineArgs = new CommandLineArgs(); for (String arg : args) { if (arg.startsWith("--")) { String optionText = arg.substring(2); String optionName; String optionValue = null; int indexOfEqualsSign = optionText.indexOf('='); if (indexOfEqualsSign > -1) { optionName = optionText.substring(0, indexOfEqualsSign); optionValue = optionText.substring(indexOfEqualsSign + 1); } else { optionName = optionText; } if (optionName.isEmpty()) { throw new IllegalArgumentException("Invalid argument syntax: " + arg); } commandLineArgs.addOptionArg(optionName, optionValue); } else { commandLineArgs.addNonOptionArg(arg); } } return commandLineArgs; } 大概流程是,在应用启动时,Spring Boot会根据默认的规则去解析command line,构造成SimpleCommandLinePropertySource,最后加入Spring Environment的propertySources 中。这里的PropertySource很重要,PropertySource 可以简单理解为配置数据源的抽象,上文所讲的各种外部配置都可以看作为数据源。 Tomcat的port是从哪里获取的当启动Spring Web Application时,控制台通常都会打印出当前容器的port端口。例如:o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8082 (http) ,所以可以直接从TomcatWebServer这个类入手。 1234567891011121314151617181920212223242526private void initialize() throws WebServerException { //从getPortsDescription(false)获得port logger.info("Tomcat initialized with port(s): " + getPortsDescription(false)); ... } public int getPort() { // 可以看到是从protocolHandler中拿出来的,实际通常是Http11NioProtocol,而它又委托给了AbstractEndpoint if (protocolHandler instanceof AbstractProtocol<?>) { return ((AbstractProtocol<?>) protocolHandler).getPort(); } // Fall back for custom protocol handlers not based on AbstractProtocol Object port = getProperty("port"); if (port instanceof Integer) { return ((Integer) port).intValue(); } // Usually means an invalid protocol has been configured return -1; } /** * Server socket port. */ private int port = -1; public int getPort() { return port; } public void setPort(int port ) { this.port=port; } 可以看到这里的port的默认值是-1,在Spring Boot中,当你想维持一个WebApplicationContext,但又不想处理任何连接,就可以通过将port设置为-1去实现。看到这里笔者发现并没有其他地方引用了setPort()这个方法去设置这个值,这里有个技巧是可以在setPort()上打个断点,然后通过调用栈查看调用的上下文。发现这个参数是从ServerProperties中获取的。 Spring Boot的配置绑定ServerProperties 从字面含义是服务参数配置,这个类存在于Spring Boot autoconfigure 项目中,在这个项目中有许多其他的Properties类。Spring Boot特性之一就是为了简化开发,为此提供了一系列的通用配置给到开发者配置,真正做到开箱即用,在Common Application properties可以查看具体配置列表。到这里,我们只要关注的问题就变成了,ServerProperties 中port参数是如何设置的,答案是Spring Boot的配置绑定过程。Spring Boot提供了一系列的参数配置,开发者只需要简单在配置文件中配置一行配置,这行配置的含义就可以生效,肯定其中Spring Boot在背后做了一些事情,而这个过程就是配置绑定。通俗的可以认为,这个过程就是Spring Boot帮你把配置文件或其他外部化配置的参数 一一映射绑定到 对应的Properties中,比如server.port对应的就是ServerProperties的port参数。这里笔者简要分析入口和关键部分逻辑,感兴趣可以自行Debug。首先在ConfigurationProperties的注解上可以猜到入口是ConfigurationPropertiesBindingPostProcessor。ConfigurationPropertiesBindingPostProcessor实现了BeanPostProcessor接口,重写了postProcessBeforeInitialization(),这个方法就是配置绑定的入口。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950@Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName)); return bean; } private Object bindDataObject(ConfigurationPropertyName name, Bindable<?> target, BindHandler handler, Context context, boolean allowRecursiveBinding) { if (isUnbindableBean(name, target, context)) { return null; } Class<?> type = target.getType().resolve(Object.class); if (!allowRecursiveBinding && context.isBindingDataObject(type)) { return null; } //函数式接口,延迟执行 DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), propertyTarget, handler, context, false, false); return context.withDataObject(type, () -> { for (DataObjectBinder dataObjectBinder : this.dataObjectBinders) { // 真正执行的逻辑,可以看到上面的函数也传入进来了,这里 Object instance = dataObjectBinder.bind(name, target, context, propertyBinder); if (instance != null) { return instance; } } return null; }); } private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder propertyBinder, BeanProperty property) { String propertyName = property.getName(); ResolvableType type = property.getType(); Supplier<Object> value = property.getValue(beanSupplier); Annotation[] annotations = property.getAnnotations(); //真正执行绑定的逻辑 Object bound = propertyBinder.bindProperty(propertyName, Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations)); if (bound == null) { return false; } if (property.isSettable()) { property.setValue(beanSupplier, bound); } else if (value == null || !bound.equals(value.get())) { throw new IllegalStateException("No setter found for property: " + property.getName()); } return true; } DataObjectBinder接口有两个实现,其中跟进到JavaBeanBinder#bind()方法中,这里的逻辑不通过Debug根本发现不了执行逻辑。通过Debug发现在bind()方法中传入的propertyBinder中间接持有PropertySources参数,走到这边大概能猜到其实配置绑定的数据源就是PropertySources,会根据优先级从PropertySources中设置对应的配置参数。在引用中有一篇更为详细的Debug博客值得阅读。 总结通过以上,可以确定几点: Spring Boot在应用启动时会维护一个PropertySources 变量存储所有外部化配置的信息; 同时在配置绑定的过程中,会从PropertySources 中根据优先级获取对应的配置参数,作为有效的配置,最后绑定在*Properties类的属性。 内嵌的tomcat容器在启动时,port信息也是从ServerProperties中获取的,理所当然使用的也是根据优先级顺序得到的有效port值。 引用 Launches a Java application. Externalized Configuration Common Application properties Spring Boot 2.0源码解析-配置绑定","categories":[{"name":"源码","slug":"源码","permalink":"https://lazyallen.github.io/categories/%E6%BA%90%E7%A0%81/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"https://lazyallen.github.io/tags/Spring-Boot/"}]},{"title":"从String类的不可变谈到分布式一致性","slug":"state-in-code-design-and-service-design","date":"2019-10-10T16:00:00.000Z","updated":"2022-12-07T07:05:00.139Z","comments":true,"path":"2019/10/11/state-in-code-design-and-service-design/","link":"","permalink":"https://lazyallen.github.io/2019/10/11/state-in-code-design-and-service-design/","excerpt":"本篇文章记录的是,在程序设计和服务层面,我对于「状态」的理解,状态就像「资本主义」,口里说的不想要,现实中又摆脱不掉。","text":"本篇文章记录的是,在程序设计和服务层面,我对于「状态」的理解,状态就像「资本主义」,口里说的不想要,现实中又摆脱不掉。 有状态对象和无状态对象:String是否线程安全通常,我们会讨论StringBuffer是线程安全的,而StringBuilder是线程不安全的。那么String本身是否是线程安全呢?我认为String类是线程安全的,因为String是一个不可变对象(Immutable Object),不可变对象天生支持线程安全。在「Effective Java」中,对于不可变对象的解释如下: 不可变对象(Immutable Object):对象一旦被创建后,对象所有的状态及属性在其生命周期内不会发生任何变化。 那么String是如何体现它的不可变的呢,因为String的本质是一个不可变的char数组。从定义可以看出: 123456public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; ... } 也就是说,每次new一个String的时候,总是会在JVM的堆中开辟一块内存去存一个不可变的数组。那么对String进行写操作时,是如何处理的呢。比如在String的substring方法可以看到。 12345678910public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); } 调用substring方法最后返回的是new了一个新的String实例。 12345678910111213141516171819public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); } 在String的构造方法可以看到,每次new一个String实例,调用的是Arrays.copyOfRange方法,这是一个native方法,作用就是拷贝内存块。String里面的写操作最后返回的都是在内存中重新开辟了一块内存地址,而并不是在原有的地址上进行写操作。 通俗一点讲就是,把String实例比作是一块画板,写操作比作是在画板上画画,对于String而言,每次写操作都是重新换了一块画板并在新画板上画画,而上一块画板永远保持它的样子,写操作时在新画板上发生的。每一块画板永远保持的一个样子,不会改变,这就是String的不可变性。反过来说,假如我们所有的写操作都是在同一块画板上进行的,那么它就是可变的,因为在它的生命周期里面,变化随时都会发生。当说一个东西不可变的时,也可以说它是没有状态的(Stateless)在线程安全及不可变性中提供了一个不可变类的例子。 1234567891011public class ImmutableValue{ private int value = 0; public ImmutableValue(int value){ this.value = value; } public int getValue(){ return this.value; }} 这个不可变类有几个特点: 成员变量value是通过构造函数赋值的 没有set方法以上保证了实例化的对象是没有公开的方法去修改它本身的。 当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件多个线程同时读同一个资源不会产生竞态条件。关键点在于对资源进行写操作后会改变状态,比如自增操作,在并发环境下,很难保证线程读到的永远是最新的状态,有可能由于线程切换和缓存不一致,而共享资源也是有状态的,就会出现两个线程读到的值是一样的情况(类似画板,两个画家在画画的时候均以为当前的画板是最新的,在同一个画板上同时画了两笔,恰好两笔都画在了一个地方,但在外人看来画板上只是添加了一笔而已),这就是线程不安全。那如何解决这个问题呢,只要解决一个关键问题就可以:保证画家画画的时候永远看到的是最新的画作,有两种方案: 线程同步共享资源的状态:从画家入手,保证同一时刻只有一位画家在画画 将共享资源去状态化:画家得到的画板永远都是基于上一份画板拷贝而成的,对于所有的画家而言,他们任何时刻看到的画板都是最新的而我们以上讲的不可变对象,采用的就是方案2,方案2在计算机设计领域有另外一个名字:写时复制(Copy-on-write,简称COW)。 写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。 String 类的设计包含了COW的思想,除此之外,在Java语言中,安全失败的本质其实也是COW思想。要说到安全失败,我们先谈什么是快速失败,两者均是Collection集合类中的概念: fail-fast机制:当遍历一个集合对象时,如果集合对象的结构被修改了,就会抛出ConcurrentModificationExcetion异常。 以ArrayList为例,简单说ArrayList继承自AbstractList类,AbstractList内部有一个字段modCount,代表修改的次数。ArrayList类的add、remove操作都会使得modCount自增。当使用ArrayList.iterator()返回一个迭代器对象时。迭代器对象有一个属性expectedModCount,它被赋值为该方法调用时modCount的值。这意味着,这个值是modCount在这个时间点的快照值,expectedModCount值在iterator对象内部不会再发送变化,具体可以阅读这篇文章。调用next()迭代会比照两个值是否一致,否则丢出异常。显然,modCount就是ArrayList的状态,而expectedModCount就是ArrayList的状态快照,在并发环境上,我们希望共享资源是不变的(共享资源有状态会带来线程不安全)。为什么在并发环境下推荐使用iterator.remove()而非list.remove()的原因就是,iterator.remove()会同步更新expectedModCount的值与modCount保持一致,而list.remove()只会更新modCount的值,expectedModCount没有同步更新,所以才会丢出异常。除此之外,你可能会想到,那我把COW思想运用在容器里面,是不是也能保证容器的并发安全。是的,fail-safe安全失败概念本质就是COW思想,具体我们不在这里展开。 我们再回来不可变对象的讨论上来,至此,可以得出一个结论:不可变对象是天生支持线程安全的。 有状态服务和无状态服务:购物车功能的实现方案 无状态服务(stateless service)对单次请求的处理,不依赖其他请求,也就是说,处理一次请求所需的全部信息,要么都包含在这个请求里,要么可以从外部获取到(比如说数据库),服务器本身不存储任何信息有状态服务(stateful service)则相反,它会在自身保存一些数据,先后的请求是有关联的. 为了好理解,举一个购物车功能的设计例子:在电商网站购物时,用户可以把自己想买的物品放入购物车。之后在某一个时间统一下单结账。购物车是有状态的,这一秒我把一本书加入了购物车,下一秒我可能就不要了。实现这个功能最常见的两种方案就是Seesion和Cookies去存储状态,那么这两种方案有什么区别呢。我们假设Cookes永远不过期(现实一般不会这样做),在单机架构下,两者并无明显差异。将购物车的信息存储在服务端,那么这个服务就是有状态的,购物车信息存储在Client端,服务并不保存任何状态,称为无状态服务。 但在集群架构中,对于无状态服务,横向扩展非常方便,而有状态服务则需要考虑同步状态的问题。如上图,当stateful service2 扩展到集群中时,需要将其他两台机的session同步到stateful service2 中,不然就可能出现购物车有东西的用户请求到stateful service2后,购物车被「清空」的情况。当然,实际情况中对有状态服务进行扩展也有其他的方式,比如在负载均衡时将用户的IP和目标服务器绑定,用户所有的请求都由一台机器处理;或者使用「共享seesion」,其本质也只是将状态剥离到了其他服务中。所以,无状态服务对横行扩展是友好的。 CAP理论但现实世界中,几乎所有的场景都是有状态的: 买火车票:一个人买到车票的时段是应该完全分开的; 银行存钱:存进去1块钱,账户余额就必须加1; 秒杀场景:商品数量卖光了,不能出现超卖情况; 假如以上场景只是在一台单机上运行,状态维持在一个地方。而如果是分布式集群架构,状态的一致性则必须保证。在理论计算机科学中,CAP理论指出,对于一个分布式系统来说,不能同时满足三个性质: 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本) 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据) 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择[3]。) 比如有一个服务在全球有5个节点,正常情况下,节点互相通信保持状态一致。 突然有一天由于不可抗拒力,中美之间的网络不可互相通信了。 这是,对于整个系统来说,就是发生了分区,为了保证系统可以继续提供服务,有两个选择: 保证一致性:比如停掉China的两个节点,牺牲了中国地区用户的可用性; 保证可用性:继续提供服务,但这时两个分区的数据会出现不一致的情况; CAP理论指出的观点是:对于一个分布式系统,如果一致性如果达到100%,那么可用性只能接近100%,反之亦然。 引用 深入理解Java中的不可变对象 线程安全及不可变性 写入时复制 快速失败与安全失败 精通有状态vs无状态(Stateful vs Stateless)—Immutable模式之姐妹篇 理解有状态服务和无状态服务 一致性状态机 状态机复制 Raft 一致性算法论文译文","categories":[{"name":"杂谈","slug":"杂谈","permalink":"https://lazyallen.github.io/categories/%E6%9D%82%E8%B0%88/"}],"tags":[{"name":"分布式","slug":"分布式","permalink":"https://lazyallen.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"}]},{"title":"XXL-JOB 分布式任务调度平台调研使用分析","slug":"xxl-job-survey","date":"2019-09-23T16:00:00.000Z","updated":"2022-12-07T07:05:53.739Z","comments":true,"path":"2019/09/24/xxl-job-survey/","link":"","permalink":"https://lazyallen.github.io/2019/09/24/xxl-job-survey/","excerpt":"对于XXL-JOB 的使用和功能介绍在其文档中非常清晰,且非常容易上手,本篇文章是把官方文档众多内容简单梳理后而成,目的在于尽可能简单清晰用通俗化的语言把XXL-JOB 工具介绍给一个小白用户。","text":"对于XXL-JOB 的使用和功能介绍在其文档中非常清晰,且非常容易上手,本篇文章是把官方文档众多内容简单梳理后而成,目的在于尽可能简单清晰用通俗化的语言把XXL-JOB 工具介绍给一个小白用户。 什么是分布式任务调度在日常开发中,可能会碰到类似「每天定时推送一些业务数据通知到用户」的场景,这时能想到最简单的解决方案时用JDK定时任务的Timer。但随业务发展,单 体应用不仅有挂掉的风险,且不一定能Hold大量的业务数据。这时最简单的方法就是加机器(横向扩展),这也带来了很多问题。 分布式:怎么保证任务正确的执行:比如5台机器,同一时间只要1台机器执行 故障处理:机器在执行任务的时候挂掉了怎么办? 任务管理:如果有几千个任务该如何管理? 日志监控:怎么知道任务的执行情况?等等多个问题。而分布式调度平台的产生就是去解决以上问题。目前比较知名的几个分布式任务调度平台有:Quartz、ElasticJob、LTS(Light Task Scheduler)、XXL-JOB.XXL-JOB 简介 XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。该工具的开发者是来自美团点评的许雪里,XXL是作者名的缩写而成。 亮点文档介绍几十个特性,这里我摘选几个比较重要的亮点特性: 中心化架构:和ElasticJob和LTS去中心化架构不同,XXL-JOB是中心化架构,由调度中心和执行器组成。 后台管理WEB:管理页面可以管理执行器、CRUD任务、查看调度日志和任务日志 Rolling实时日志:支持在线查看调度结果,并且支持以Rolling方式实时查看执行器输出的完整的执行日志; GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。支持30个版本的历史版本回溯。 任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔; 路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等; 阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度; 全异步:任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰,理论上支持任意时长任务的运行;运行部署的几个步骤文档中的「快速入门」已经非常具体了。大概分为 初始化调度数据库 部署调度中心集群:xxl-job-admin 部署执行器集群:xxl-job-core。可以直接使用,也可以将现有项目改造为执行器,只要引入xxl-job-core的Maven依赖即可。 登录调度中心访问地址,管理配置。注意:本篇运行部署的版本是v2.12架构从架构图上理解,XXL-JOB分为两个部分,同时也对应两个项目。 调度中心(xxl-job-admin) 执行器管理 任务管理 日志管理 其他 执行器(xxl-job-core) 主动注册 任务执行 任务回调 日志服务同时,从架构图也可以看出来基本的工作执行流程如下: 任务执行器根据配置的调度中心的地址,自动注册到调度中心 达到任务触发条件,调度中心下发任务 执行器基于线程池执行任务,并把执行结果放入内存队列中、把执行日志写入日志文件中 执行器的回调线程消费内存队列中的执行结果,主动上报给调度中心 当用户在调度中心查看任务日志,调度中心请求任务执行器,任务执行器读取任务日志文件并返回日志详情统一术语 调度中心:负责管理、调度、监控任务的全周期。可以理解为所有执行器的老大,所有的执行器都有老大调遣; 执行器:可以理解为具体干活的一类小弟,小弟要主动去认老大(自动注册),老大发出指令后执行(任务执行),执行后反馈任务执行结果(回调线程),同时也提供给老大查询任务执行过程记录的能力(日志服务); 任务:一件事情,由一个或一组执行器执行; 调度:老大交给小弟办一件事的具体描述; 注册方式:自动注册和手动录入在调度中心的执行器管理页面,提供了两种执行器注册方式。这里需要明白4个字段的含义: 执行器:这里的执行器实际指的是「执行器集群」的概念,并非单个机器节点,同时这也是执行任务的基本单位,也就是说调度中心并不能「直接」指定某台机器去执行任务,但可以「间接」通过定义任务路由的方式实现; AppName:「执行器集群」的唯一标志,每个执行器机器集群的唯一标示, 任务注册以 “执行器” 为最小粒度进行注册; 每个任务通过其绑定的执行器可感知对应的执行器机器列表; 注册方式:分为自动和手动,如果是自动就不用填机器地址,同时执行器配置要必填调度中心地址和AppName用于主动注册;如果是手动,需要填写机器地址; 机器地址:具体执行任务的机器节点。一个AppName维护着包含多个机器节点的列表;无论是自动注册还是手动注册,都需要用户在执行器管理页面声明一个执行器,这里我们着重说一下自动注册的配置和流程。主动注册配置说明: 首先需要在执行器管理页面新建一个注册方式为「自动注册」的执行器; 部署执行器时需要填写xxl.job.admin.addresses和xxl.job.executor.appname两个配置字段表明开启自动注册,这两个字段时选填的,如果为空则表明关闭自动注册;执行器注册和摘除流程说明:任务注册的心跳周期Beat默认为30s。 执行器节点启动之后会以一倍Beat进行执行器注册, 调度中心以一倍Beat进行动态任务发现; “执行器” 在进行任务注册时将会周期性维护一条注册记录,即机器地址和AppName的绑定关系; “调度中心” 从而可以动态感知每个AppName在线的机器列表,具体信息见xxl_job_registry表。 执行器摘除分为主动摘除和过期摘除: 过期摘除:注册信息的失效时间为三倍Beat,也就是90s,执行器销毁时,将会主动上报调度中心并摘除对应的执行器机器信息,提高心跳注册的实时性; 主动摘除:在WEB页面主动删除执行器。新建一个任务新建好执行器后,接下来就是定义一个任务分配给执行器执行。运行模式:Bean模式和Glue模式如果把业务代码比作奔驰在路上的跑车,如果我们要给轮胎加一个零件,有两种方案,一是把车停下来把零件加上去后再启动,这就是Bean模式,二是不停车,车在跑的同时直接在论坛上把零件「粘」上去,这也是Glue(中文含义就是胶水)的意义来源。两者区别: Bean模式的业务代码保存在执行器,Glue模式的业务代码保存在调度中心,运行在执行器; Bean模式更新代码需呀停机,Glue模式支持通过Web IDE在线更新,实时编译和生效; Glue模式提供版本回滚功能,Glue模式每次执行前都要判断为最新版本,否则需要冲洗构造任务线程; Glue模式支持跨语言,Shell、Python、Nodejs Glue Java 模式是通过 GLUE Class loader 加载源码的方式,加载源码可以注入 spring 当中其他的一些 server 组件,很方便的接触 spring 其他服务。你修改的时候,下次任务执行,会是你当前这份源码重新实例化,注入一些新的服务进行执行。 开发一个JOB类级别:在XXL-JOB中,所以的JOB都需要继承IJobHandler并重写execute方法。 1234567891011121314151617181920212223242526272829303132333435363738public abstract class IJobHandler { /** success */ public static final ReturnT<String> SUCCESS = new ReturnT<String>(200, null); /** fail */ public static final ReturnT<String> FAIL = new ReturnT<String>(500, null); /** fail timeout */ public static final ReturnT<String> FAIL_TIMEOUT = new ReturnT<String>(502, null); /** * execute handler, invoked when executor receives a scheduling request * * @param param * @return * @throws Exception */ public abstract ReturnT<String> execute(String param) throws Exception; /** * init handler, invoked when JobThread init */ public void init() throws InvocationTargetException, IllegalAccessException { // do something } /** * destroy handler, invoked when JobThread destroy */ public void destroy() throws InvocationTargetException, IllegalAccessException { // do something }} 方法级别:前提是Spring容器,直接在方法上添加@XxlJob("demoJobHandler")就行。无论是类级别还是方法级别,执行方式格式要求为 public ReturnT<String> execute(String param)。 官方提供的几种示例任务demoJobHandler:简单示例任务,任务内部模拟耗时任务逻辑,用户可在线体验Rolling Log等功能;12345678910111213/** * 1、简单任务示例(Bean模式) */ @XxlJob("demoJobHandler") public ReturnT<String> demoJobHandler(String param) throws Exception { XxlJobLogger.log("XXL-JOB, Hello World."+param); for (int i = 0; i < 5; i++) { XxlJobLogger.log("beat at:" + i); TimeUnit.SECONDS.sleep(2); } return ReturnT.SUCCESS; } shardingJobHandler:分片示例任务,任务内部模拟处理分片参数,可参考熟悉分片任务;123456789101112131415161718192021/** * 2、分片广播任务 */ @XxlJob("shardingJobHandler") public ReturnT<String> shardingJobHandler(String param) throws Exception { // 分片参数 ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo(); XxlJobLogger.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVO.getTotal()); // 业务逻辑 for (int i = 0; i < shardingVO.getTotal(); i++) { if (i == shardingVO.getIndex()) { XxlJobLogger.log("第 {} 片, 命中分片开始处理", i); } else { XxlJobLogger.log("第 {} 片, 忽略", i); } } return ReturnT.SUCCESS; } httpJobHandler:通用HTTP任务Handler;业务方只需要提供HTTP链接即可,不限制语言、平台;12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364/** * 4、跨平台Http任务 */ @XxlJob("httpJobHandler") public ReturnT<String> httpJobHandler(String param) throws Exception { // request HttpURLConnection connection = null; BufferedReader bufferedReader = null; try { // connection URL realUrl = new URL(param); connection = (HttpURLConnection) realUrl.openConnection(); // connection setting connection.setRequestMethod("GET"); connection.setDoOutput(true); connection.setDoInput(true); connection.setUseCaches(false); connection.setReadTimeout(5 * 1000); connection.setConnectTimeout(3 * 1000); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8"); // do connection connection.connect(); //Map<String, List<String>> map = connection.getHeaderFields(); // valid StatusCode int statusCode = connection.getResponseCode(); if (statusCode != 200) { throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid."); } // result bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); StringBuilder result = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { result.append(line); } String responseMsg = result.toString(); XxlJobLogger.log(responseMsg); return ReturnT.SUCCESS; } catch (Exception e) { XxlJobLogger.log(e); return ReturnT.FAIL; } finally { try { if (bufferedReader != null) { bufferedReader.close(); } if (connection != null) { connection.disconnect(); } } catch (Exception e2) { XxlJobLogger.log(e2); } } } commandJobHandler:通用命令行任务Handler;业务方只需要提供命令行即可;如 “pwd”命令;1234567891011121314151617181920212223242526272829303132333435363738/** * 3、命令行任务 */ @XxlJob("commandJobHandler") public ReturnT<String> commandJobHandler(String param) throws Exception { String command = param; int exitValue = -1; BufferedReader bufferedReader = null; try { // command process Process process = Runtime.getRuntime().exec(command); BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream()); bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream)); // command log String line; while ((line = bufferedReader.readLine()) != null) { XxlJobLogger.log(line); } // command exit process.waitFor(); exitValue = process.exitValue(); } catch (Exception e) { XxlJobLogger.log(e); } finally { if (bufferedReader != null) { bufferedReader.close(); } } if (exitValue == 0) { return IJobHandler.SUCCESS; } else { return new ReturnT<String>(IJobHandler.FAIL.getCode(), "command exit value("+exitValue+") is failed"); } } 路由策略当执行器集群部署时,提供丰富的路由策略,包括; FIRST(第一个):固定选择第一个机器; LAST(最后一个):固定选择最后一个机器; ROUND(轮询) RANDOM(随机):随机选择在线的机器; CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。 LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举; LEAST_RECENTLY_USED(最近最久未使用):最久为使用的机器优先被选举; FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度; BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度; SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务; 从任务触发到任务执行在调度中心阶段有几个步骤 匹配执行器 从注册中心加载在线机器节点列表 根据路由策略去匹配一组机器 通知这个机器去执行任务基本前面几个路由策略比较容易理解,这里我们着重说一下后面三个路由策略。故障转移和忙碌转移调度器在调度时会请求执行器是否处于处于故障或者忙碌状态,对应着ExecutorBiz的开放出来的两个方法。 123456789101112131415161718public interface ExecutorBiz { /** * beat * @return */ public ReturnT<String> beat(); /** * idle beat * * @param jobId * @return */ public ReturnT<String> idleBeat(int jobId);} 分片广播有时候一台机器处理不够,需要多台机器分片处理。可以参照上面的分片广播任务查看如何使用。 阻塞处理策略 当我们有一些耗时任务,触发的频率超过它的执行器所执行的那些速度的时候,如上图,红色的触发请求进来,但是前面的还在堆积着执行,这时候怎么办?第一条就是默认的单机串行,会把请求入队列,等前面的执行完了之后,挨个把所有的触发的任务全都执行掉。第二个就是丢弃后续的调度,红色的进来了,发现前面已经有了,或者是当前已经 JOB 运行了,直接把后面的标记失败,不进行后面的执行了。最后一个就是覆盖之前调度的,它发现前面队列里面的数据或者任务执行的情况下,把队列清空,把清空的数据全都标记失败,然后把执行的 JOB 也标记失败,让自己来运行。 触发规则现在提供的触发规则主要有 3 种。 第一种是 Cron 表达式,每一个任务需要配一个 Cron 表达式。 第二种是任务依赖,你可以为每一个任务配置一个子任务,当副任务执行完成之后,可以触发子任务,这样关联的方式进行触发执行。 第三种就是事件触发,其实就是类似于 Mq 的场景,代码里面有一个业务逻辑,触发了一个任务执行。任务状态 成功(SUCCESS) 失败 (FAIL) 超时 (FAIL_TIMEOUT) 进行中调度日志和任务日志调度日志:调度平台调度任务的日志,如果任务失败会保留出失败的日志。 任务日志:具体任务执行的日志,下面那张图是一个日志页面展示(貌似是读不到日志),真实应该展示的应该是下面的日志内容。 12345672020-02-05 23:29:54 [com.xxl.job.core.thread.JobThread#run]-[124]-[Thread-6] <br>----------- xxl-job job execute start -----------<br>----------- Param:2020-02-05 23:29:54 [com.xuxueli.executor.sample.frameless.jobhandler.ShardingJobHandler#execute]-[20]-[Thread-6] 分片参数:当前分片序号 = 0, 总分片数 = 22020-02-05 23:29:54 [com.xuxueli.executor.sample.frameless.jobhandler.ShardingJobHandler#execute]-[25]-[Thread-6] 第 0 片, 命中分片开始处理2020-02-05 23:29:54 [com.xuxueli.executor.sample.frameless.jobhandler.ShardingJobHandler#execute]-[27]-[Thread-6] 第 1 片, 忽略2020-02-05 23:29:54 [com.xxl.job.core.thread.JobThread#run]-[164]-[Thread-6] <br>----------- xxl-job job execute end(finish) -----------<br>----------- ReturnT:ReturnT [code=200, msg=null, content=null]2020-02-05 23:29:55 [com.xxl.job.core.thread.TriggerCallbackThread#callbackLog]-[190]-[xxl-job, executor TriggerCallbackThread] <br>----------- xxl-job job callback finish. 用户管理两种角色: 管理员:拥有所有的执行器的权限 普通用户:由管理分配指定的执行器的权限目前发现的几个小问题报表页面统计简单运行报表页面只有任务数量、调度次数、执行器数量的统计,无法根据不同执行器展现特定执行器的数据报表。不同用户的报表页面数据没有隔离 对于allen用户,报表页面的数据和管理员登录后所展示的数据一样。但执行器管理和任务管理以及调度日志,是做了数据隔离的。 手动输入执行器总显示Online对于手动录入的执行器,总显示Online。这样的好处是,用户可以手动控制上线的机器,只要修改机器列表即可。 是否可以覆盖需求 是否可以指定节点运行?A:XXL-JOB运行的基本单位是一个执行器集群,但是可以通过指定不同的路由策略达到指定机器节点的目的。 JOB可以被高频调用A:参照压测报告XXLJOB有过持续20小时,80W次调度100%成功率的测试报告。 支持大数据量的任务调度A:XXL-JOB支持分片广播,是支持大数据量的任务调度的,数据量的多少应该取决于执行器集群的数量吧 调度策略简单明了?A:简单明了 提供APIAPI接口提供了调度中心和执行器的API。","categories":[{"name":"中间件","slug":"中间件","permalink":"https://lazyallen.github.io/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"}],"tags":[{"name":"分布式","slug":"分布式","permalink":"https://lazyallen.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"},{"name":"任务调度","slug":"任务调度","permalink":"https://lazyallen.github.io/tags/%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6/"}]},{"title":"单元测试:从Junit到Powermock","slug":"unit-test-littile-share","date":"2019-08-10T16:00:00.000Z","updated":"2022-12-07T07:05:13.216Z","comments":true,"path":"2019/08/11/unit-test-littile-share/","link":"","permalink":"https://lazyallen.github.io/2019/08/11/unit-test-littile-share/","excerpt":"本文介绍的是Java语言的单元测试框架,分别介绍Junit、Mockito、Powermock三种工具的特点,并附上了用于演示的Demo案例。","text":"本文介绍的是Java语言的单元测试框架,分别介绍Junit、Mockito、Powermock三种工具的特点,并附上了用于演示的Demo案例。 什么是单元测试单元测试是指,对软件中的最⼩可测试单元在与程序其他部分相隔离的情况下进⾏检查和验证的⼯作,这⾥的最⼩可测试单元通常是指函数或者类。 单元测试的好处单元测试通常由开发⼯程师完成,⼀般会伴随开发代码⼀起递交⾄代码库。单元测试属于最严格的软件测试⼿段,是最接近代码底层实现的验证⼿段,可以在软件开发的早期以最⼩的成本保证局部代码的质量。 如何做好单元测试需要测试哪些东西: 结果是否正确 边界条件 空值或者不完整的值 格式错误的数据 完全伪造或者不一致的输入数据 意料之外的值 检查反向关联 为了检查数据是否插入成功,检查能否查询出来 检查异常:强制检查异常情况 性能特性 什么是好的单元测试: 自动化 独立性 可重复 单元测试的三个步骤 准备数据、行为 测试目标模块 验证测试结果 在Spring中使用Junit进行单元测试阿里代码规约手册中几条关于单元测试的强制规范 不允许使用syetem.out进行人肉验证,必须使用assert进行验证 保持单元测试的独立性,每一个测试案例互不影响 核心业务,核心代码,核心模块的新增代码必须保证单元测试通过 Junit介绍和入门Junit是一套框架(用于JAVA语言),由 Erich Gamma和 Kent Beck 编写的一个回归测试框架(regression testing framework),即用于白盒测试。现阶段的最新版本号是4.12,JUnit5目前正在测试中,所以这里还是以JUnit4为准。 使用assertThat语法JUnit4.4引入了Hamcrest框架(匹配器框架),Hamcest提供了一套匹配符Matcher,这些匹配符更接近自然语言,可读性高,更加灵活。使用全新的断言语法:assertThat,结合Hamcest提供的匹配符,只用这一个方法,就可以实现所有的测试。 assertThat语法如下: 12assertThat(T actual, Matcher<T> matcher);assertThat(String reason, T actual, Matcher<T> matcher); 其中actual为需要测试的变量,matcher为使用Hamcrest的匹配符来表达变量actual期望值的声明。 12345678910assertThat( testedNumber, allOf( greaterThan(8), lessThan(16) ) );//allOf匹配符表明如果接下来的所有条件必须都成立测试才通过,相当于“与”(&&)2、assertThat( testedNumber, anyOf( greaterThan(16), lessThan(8) ) );//anyOf匹配符表明如果接下来的所有条件只要有一个成立则测试通过,相当于“或”(||)assertThat( testedString, containsString( "developerWorks" ) );//containsString匹配符表明如果测试的字符串testedString包含子字符串"developerWorks"则测试通过assertThat( testedNumber, greaterThan(16.0) );//greaterThan匹配符表明如果所测试的数值testedNumber大于16.0则测试通过assertThat( iterableObject, hasItem ( "element" ) );//hasItem匹配符表明如果测试的迭代对象iterableObject含有元素“element”项则测试通过 常见注解介绍引入依赖: 123456789101112<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope></dependency><dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> <version>1.3</version> <scope>test</scope></dependency> 新建一个测试用的类:Calculator 123456789101112131415161718192021222324252627282930313233343536package com.lazyallen.blog;import java.util.Objects;/** * @author allen * @Date 2019-06-09 */public class Calculator { public Double addition(Double x,Double y){ if (Objects.isNull(x) || Objects.isNull(y)) { throw new NullPointerException("参数不能为空"); } return x+y; } public Double division(Double x, int y){ if (0==y){ throw new IllegalArgumentException("除数y不能为0"); } return x/y; } public Double multiplication(Double x, Double y){ if (Objects.isNull(x) || Objects.isNull(y)) { throw new NullPointerException("参数不能为空"); } return x*y; } public void version(){ System.out.printf("v1.0"); }} 生成对应的测试类:CalculatorTest。 小技巧:在IDEA中,使用ctrl + shift + T 的快捷键可以快速生成测试类。 12345678910111213141516171819202122232425262728293031323334353637383940package com.lazyallen.blog;import org.junit.Before;import org.junit.Test;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.MockitoAnnotations;import static org.mockito.Mockito.*;import static org.junit.Assert.*;import static org.hamcrest.Matchers.is;/** * @author allen * @Date 2019-06-09 */public class AccountantTest { @Mock Calculator calculator; @InjectMocks Accountant accountant; @Before public void setUp() throws Exception { // 初始化测试用例类中由Mockito的注解标注的所有模拟对象 MockitoAnnotations.initMocks(this); } @Test public void testCalculateSalary() { when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0); when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(16.0); Double salary = accountant.calculateSalary(10.0,10.0); assertThat(salary,is(16.0)); }} @Test注解有两个可选的参数,分别为timeout和expectd。除@Test注解之外,还有如下注解: @Before 注解的作用是使被标记的方法在测试类里每个方法执行前调用;同理 After 使被标记方法在当前测试类里每个方法执行后调用。 @BeforeClass 注解的作用是使被标记的方法在当前测试类被实例化前调用;同理 @AfterClass 使被标记的方法在测试类被实例化后调用。 @Ignore 注解的作用是使被标记方法暂时不执行。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455package com.lazyallen.blog;import org.junit.*;import static org.hamcrest.Matchers.is;import static org.junit.Assert.assertThat;/** * @author allen * @Date 2019-06-09 */public class Calculatortest2 { public Calculatortest2() { System.out.println("Constructor"); } @BeforeClass public static void beforeThis() throws Exception { System.out.println("BeforeClass"); } @AfterClass public static void afterThis() throws Exception { System.out.println("AfterClass"); } @Before public void setUp() throws Exception { System.out.println("Before"); } @After public void tearDown() throws Exception { System.out.println("After"); } @Test public void evaluate() throws Exception { Calculator calculator = new Calculator(); int sum = calculator.addition(1,1); assertThat(sum, is(2)); System.out.println("Test evaluate"); } @Test public void idiot() throws Exception { System.out.println("Test idiot"); } @Ignore public void ignoreMe() throws Exception { System.out.println("Ignore"); }} 结果如下: 123456789101112BeforeClassConstructorBeforeTest idiotAfterConstructorBeforeTest evaluateAfterAfterClassProcess finished with exit code 0 从控制台输出可以得到以下两点信息: 测试类在测试每一个case时,会重新实例化一次测试类,为的是保证每个case是独立隔离的。 BeforeClass是在测试类初始化之前执行,Before是在每一个case运行前执行,我们可以用该注解在测试类初始化的时候准备一些测试的数据。 使用Mockito模拟来Mock对象为什么需要Mock在做单元测试的时候,经常出现这样的情况,在需要测试中模块中包含其他依赖的模块,有时候去对这个依赖的模块做单元测试比较高,或者无法进行单元测试。例如,在Java Web项目中通常是分层的,我们需要对Service层进行单元测试,由于Service需要依赖Dao层,我们又不希望对Service的同时又为Dao层做一些初始化的工作从而保证测试通过,就可以将Dao层Mock出来。Mock出来的对象不是真实的对象,而是具备和真实对象相同行为的对象。比如List mockList = mock(List.class),这个mockList并不是真实的List,但同样又add(),clear(),size()等方法。 Mockito介绍和入门Mockito 是 Mock 数据的测试框架,简化了对有外部依赖的类的单元测试。 常用的几个注解:123456<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.23.0</version> <scope>test</scope></dependency> 这里我们写一个会计师类:Accountant 12345678910111213141516171819202122232425262728293031323334353637383940414243package com.lazyallen.blog;/** * @author allen * @Date 2019-06-09 */public class Accountant { private static final Double tax = 0.2; Calculator calculator; public Double calculateSalary(Double a, Double b){ Double entireSalary = calculator.addition(a,b); Double deservedSalary = calculator.multiplication(entireSalary,(1-tax)); return deservedSalary; } public Double calculateOddMonthSalary(Double a, Double b){ int month = DateUtils.getCurrentMonth(); if(month%2!=0){ Double entireSalary = calculator.addition(a,b); Double deservedSalary = calculator.multiplication(entireSalary,(1-tax)); return deservedSalary; } return 0.0; } private String sayHello(){ System.out.println("hello"); return "hello"; } public void printSayHello(){ String hello = this.sayHello(); System.out.println(hello); } public Accountant(Calculator calculator) { this.calculator = calculator; } public Accountant() { }} 对应的测试类: 小技巧:静态导入 org.mockito.Mockito.*; 1234567891011121314151617181920212223242526272829303132333435363738package com.lazyallen.blog;import org.junit.Before;import org.junit.Test;import org.mockito.InjectMocks;import org.mockito.MockitoAnnotations;import static org.hamcrest.Matchers.is;import static org.junit.Assert.assertThat;import static org.mockito.ArgumentMatchers.anyDouble;import static org.mockito.Mockito.mock;import static org.mockito.Mockito.when;/** * @author allen * @Date 2019-06-09 */public class AccountantTest2 { Calculator calculator; Accountant accountant; @Before public void setUp() throws Exception { calculator = mock(Calculator.class); accountant = new Accountant(calculator); } @Test public void testCalculateSalaryUseMockMethod() { when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0); when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(80.0); Double salary = accountant.calculateSalary(10.0,10.0); assertThat(salary,is(80.0)); }} Mockito 支持通过静态方法mock() 来 Mock 对象,或者通过 @Mock 注解,来创建 Mock 对象。 假如说,你需要使用@Mock注解,则需要初始化测试用例类中由Mockito的注解标注的所有模拟对象,代码如下: 12345678910111213141516171819202122232425262728293031323334353637383940package com.lazyallen.blog;import org.junit.Before;import org.junit.Test;import org.mockito.InjectMocks;import org.mockito.Mock;import org.mockito.MockitoAnnotations;import static org.mockito.Mockito.*;import static org.junit.Assert.*;import static org.hamcrest.Matchers.is;/** * @author allen * @Date 2019-06-09 */public class AccountantTest { @Mock Calculator calculator; @InjectMocks Accountant accountant; @Before public void setUp() throws Exception { // 初始化测试用例类中由Mockito的注解标注的所有模拟对象 MockitoAnnotations.initMocks(this); } @Test public void testCalculateSalary() { when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0); when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(16.0); Double salary = accountant.calculateSalary(10.0,10.0); assertThat(salary,is(16.0)); }} 你也可以使用@RunWith(MockitoJUnitRunner.class)去代替MockitoAnnotations.initMocks(this); 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950package com.lazyallen.blog;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.mockito.InjectMocks;import org.mockito.Matchers;import org.mockito.Mock;import org.mockito.MockitoAnnotations;import org.mockito.junit.MockitoJUnitRunner;import java.util.ArrayList;import java.util.Iterator;import java.util.List;import static org.hamcrest.Matchers.is;import static org.junit.Assert.assertThat;import static org.mockito.ArgumentMatchers.anyDouble;import static org.mockito.Mockito.*;/** * @author allen * @Date 2019-06-09 */@RunWith(MockitoJUnitRunner.class)public class AccountantTest3 { @Mock Calculator calculator; @InjectMocks Accountant accountant; @Test public void testCalculateSalary() { when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0); when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(16.0); Double salary = accountant.calculateSalary(10.0,10.0); assertThat(salary,is(16.0)); } @Test(expected = NullPointerException.class) public void testCalculateSalary1(){ when(calculator.addition(anyDouble(),anyDouble())).thenReturn(100.0).thenThrow(new NullPointerException()); Double mockResult = calculator.addition(10.0,10.0); System.out.println("mockResult"+mockResult); calculator.addition(10.0,10.0); }} 通常建议使用@RunWith(MockitoJUnitRunner.class)的方式去初始化测试用例类中由Mockito的注解标注的所有模拟对象。 mock 出来的对象拥有和源对象同样的方法和属性,when() 和 thenReturn() 方法是对源对象的配置,怎么理解,就是说在第一步 mock() 时,mock 出来的对象还不具备被 Mock 对象实例的行为特征,而 when(...).thenReturn(...) 就是根据条件去配置源对象的预期行为。 有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。典型的运用就是使用mock迭代器。 12345678@Test public void testMockIterator() { Iterator i = mock(Iterator.class); when(i.next()).thenReturn("hello","world"); //when(i.next()).thenReturn("hello").thenReturn("world"); String result = i.next() + " " + i.next(); assertThat(result,is("hello world")); } 除了对方法调用结果是否正确的测试,有时还需要验证一些方法的行为,比如验证方法被调用的次数,验证方法的入参等,Mockito 通过 verify() 方法实现这些场景的测试需求。这被称为“行为测试”。 12345678910111213141516@Test public void testVerify() { Calculator mock = mock(Calculator.class); when(mock.addition(anyDouble(),anyDouble())).thenReturn(2.0); mock.addition(1.0,1.0); mock.version(); mock.version(); //比如说写Controller的单元测试,验证service方法是否调用 verify(mock, times(2)).version(); verify(mock, never()).multiplication(anyDouble(),anyDouble()); verify(mock, atLeastOnce()).version(); verify(mock, atLeast(2)).version(); verify(mock, atMost(3)).version();; } Mockito 支持通过 @Spy 注解或 spy() 方法包裹实际对象,除非明确指定对象,否则都会调用包裹后的对象。这种方式实现了对实际对象的部分自定义修改。 123456789101112131415@Test public void testDifferMockSpy() { List mock = mock(ArrayList.class); mock.add("one"); verify(mock).add("one"); System.out.println("mock[0]:"+mock.get(0)); List spy = spy(new ArrayList()); spy.add("one"); verify(spy).add("one"); System.out.println("spy[0]:"+spy.get(0)); when(spy.size()).thenReturn(100); System.out.println("spy.size:"+spy.size()); } PowerMockito解决了什么问题Mockito 因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是Mock 工具不可以实现对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟,PowerMock 是在 EasyMock 以及 Mockito 基础上的扩展,通过定制类加载器等技术,PowerMock 实现了之前提到的所有模拟功能,使其成为大型系统上单元测试中的必备工具。 Maven依赖123456789101112<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.0-RC.3</version> <scope>test</scope></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.0-RC.3</version> <scope>test</scope></dependency> mock静态方法通常,在一些工具类中会又很多静态方法,比如,我们新建一个工具类: 12345678910111213141516package com.lazyallen.blog;import java.time.LocalDate;import java.time.LocalDateTime;/** * @author allen * @Date 2019-06-09 */public class DateUtils { public static int getCurrentMonth(){ LocalDate now = LocalDate.now(); return now.getMonthValue(); }} 相应的测试类: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546package com.lazyallen.blog;import javafx.beans.binding.When;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.mockito.InjectMocks;import org.mockito.Mock;import org.powermock.api.mockito.PowerMockito;import org.powermock.core.classloader.annotations.PrepareForTest;import org.powermock.modules.junit4.PowerMockRunner;import static org.hamcrest.Matchers.is;import static org.junit.Assert.assertThat;import static org.mockito.ArgumentMatchers.anyDouble;import static org.mockito.Mockito.*;/** * @author allen * @Date 2019-06-09 */@RunWith(PowerMockRunner.class)@PrepareForTest({DateUtils.class,Accountant.class})public class AccountantTest4 { @Mock Calculator calculator; @InjectMocks Accountant accountant; @Before public void init(){ PowerMockito.mockStatic(DateUtils.class); } @Test public void testCalculateOddMonthSalary(){ PowerMockito.when(DateUtils.getCurrentMonth()).thenReturn(7); when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0); when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(16.0); Double salary = accountant.calculateOddMonthSalary(10.0,10.0); assertThat(salary,is(16.0)); }} 只需要注意两点: @RunWith(PowerMockRunner.class) @PrepareForTest({DateUtils.class,Accountant.class}) mock私有方法123456@Test public void testPrivate() throws Exception { Accountant accountant1 = PowerMockito.spy(new Accountant()); PowerMockito.when(accountant1, "sayHello").thenReturn("你好"); accountant1.printSayHello(); } 引用 Mockito框架中文文档 代码示例","categories":[{"name":"工具","slug":"工具","permalink":"https://lazyallen.github.io/categories/%E5%B7%A5%E5%85%B7/"}],"tags":[{"name":"Java","slug":"Java","permalink":"https://lazyallen.github.io/tags/Java/"},{"name":"UnitTest","slug":"UnitTest","permalink":"https://lazyallen.github.io/tags/UnitTest/"}]},{"title":"浅析Docker 独立容器的网络通信模式","slug":"use-docker-bridge-network-driver","date":"2019-04-16T16:00:00.000Z","updated":"2022-12-07T07:05:25.896Z","comments":true,"path":"2019/04/17/use-docker-bridge-network-driver/","link":"","permalink":"https://lazyallen.github.io/2019/04/17/use-docker-bridge-network-driver/","excerpt":"很早就接触过Docker容器相关的概念,之前还使用过Jenkins提供的k8s插件进行容器编排。但说实话,自己工作还是生活中使用的比较少,基础处于我知道是怎么样的,但缺乏实践经验。","text":"很早就接触过Docker容器相关的概念,之前还使用过Jenkins提供的k8s插件进行容器编排。但说实话,自己工作还是生活中使用的比较少,基础处于我知道是怎么样的,但缺乏实践经验。 搭建xxl-job-admin docker container 遇到的疑问在阅读xxl-job的文档时,发现其提供Docker容器部署的方式,于是就在本机装好了Docker环境,实践一番。提前说明一下,本次实验使用的Docker版本为19.03.8。xxl-job-admin的容器部署很简单,一共就分两步: 先部署一个MySql,部署完需要执行好xxl-job相应的SQL脚本 再部署xxl-job-admin,其中MySql链接指向刚刚部署好的MySql实例 部署命令也很简单,一共就3条: 123456docker network create simple-networkdocker run --name xxl-mysql --network simple-network -e MYSQL_ROOT_PASSWORD=123456 -d mysql docker run --network simple-network -e PARAMS="--spring.datasource.url=jdbc:mysql://xxl-mysql:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&useSSL=false --spring.datasource.password=123456" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:2.1.2 我们在结尾再解释为什么是这样写的,我先提出我的困惑。和单机部署应用进程都处在同一个网络环境中不同的是,在同一台主机上的两个容器,他们的网络环境是不是隔离的?如果不是隔离的,容器之间是怎么通信的,容器和宿主机之间又是怎么通行的呢? Docker network drivers 介绍在解答疑惑之前,我们需要先了解一下Docker的network驱动,通常情况下,默认是以下几种: bridge:默认网络驱动模式,通常如果不特别指定别的网络驱动模式,一般默认就是bridge。 host:在独立容器中,使用host网络驱动会移除容器和容器主机之间的网络隔离性。也就是说,这个容器和主机的网络环境完全一样。不过目前只在Docker 17.06以上版本支持,且不支持Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server. overlay:overlay 网络将多个 Docker 守护进程连接在一起,并使集群服务能够相互通信。您还可以使用 overlay 网络来实现 swarm 集群和独立容器之间的通信,或者不同 Docker 守护进程上的两个独立容器之间的通信。该策略实现了在这些容器之间进行操作系统级别路由的需求。[^cnkirito_footnote] macvlan:Macvlan 网络允许为容器分配 MAC 地址,使其显示为网络上的物理设备。 Docker 守护进程通过其 MAC 地址将流量路由到容器。对于希望直连到物理网络的传统应用程序而言,使用 macvlan 模式一般是最佳选择,而不应该通过 Docker 宿主机的网络进行路由。[^cnkirito_footnote] none:对于此容器,禁用所有联网。通常与自定义网络驱动程序一起使用。none 模式不适用于集群服务。[^cnkirito_footnote] 对于运行在一台主机的单独容器而言,只需要关心以上的bridge、host、none。查看当前容器下已有的网络驱动 123456➜ ~ docker network lsNETWORK ID NAME DRIVER SCOPE04675183a24d bridge bridge local245b36b452af host host local43a8d2311d38 none null local 可以看到,默认在一个Docker容器中包含三种网络驱动,其中SCOPE指的是网络范围,可以是local或swarm范围。区别在于local是在主机范围内提供连接和网络服务(例如DNS或IPAM)。swarm可跨群群集提供连接和网络服务。swarm网络在整个群集中具有相同的网络ID,而local范围网络在每个主机上具有唯一的网络ID。 接下来我们主要介绍host并着重介绍bridge网络驱动。 Docker Host Network Driver如果在创建容器时指定--net=host,host网络中的所有容器都可以在宿主机上相互通信。从网络角度来看,这等于在没有容器的主机上运行的多个进程。因为它们使用相同的主机环境,所以没有两个容器能够绑定到相同的TCP端口,如果在同一主机上调度了多个容器,则可能出现端口冲突的情况。 1234567891011#Create containers on the host networkhost $ docker run --rm -itd --net host --name C1 alpine sh#Show host eth0host $ ip -o -4 address show dev eth0 |cut -d’ ‘ -f1-72: eth0 inet 172.31.21.213/20#Show eth0 from C1host $ docker exec -it C1 shC1 # ip -o -4 address show dev eth0 |cut -d' ' -f1-72: eth0 inet 172.31.21.213/20 可见,C1和宿主机的公用一套网络环境,所以eth0网卡的IP均为172.31.21.213。在host网络下,容器网络环境和宿主机网络没有任何隔离性,容器和容器之间,容器和宿主机之间的网络通信不存在任何障碍。 Docker Bridge Network Driver容器创建时倘若没有特别指定其他网络驱动,则默认使用的是Bridge 桥接网络驱动。而在docker中,bridge分为系统默认和用户自定义的两种; Default Docker Bridge Network可以看到上面执行执行docker network ls 时,第一行展示的network就是一个名叫Bridge的Bridge网络驱动。比如启动一个名叫bb-default-bridge的容器,在容器内部,其IP为172.17.0.2 123456789➜ ~ docker run --rm -it --name bb-default-bridge busybox sh/ # ifconfigeth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02 inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:10 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:828 (828.0 B) TX bytes:0 (0.0 B) 那如何反映,该容器使用的是默认的Bridge网络驱动呢,docker提供了docker network inspect命令展现使用该网络驱动下的所有容器。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546➜ ~ docker network inspect bridge[ { "Name": "bridge", "Id": "04675183a24d5f702da598aa3fc03c3a68684ff66b45e4c9eddbbf80c94031d7", "Created": "2020-03-30T14:00:38.974673641Z", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "3af6ea18328e0414ecaac61756ce4e658b24ea89ccd182808a73b424f36ab01d": { "Name": "bb-default-bridge", "EndpointID": "6e4cc1b42cd4c0ffd52bfcbe9e69ac4169a3e41a4e013ec5bb9aa30b82ec1ce5", "MacAddress": "02:42:ac:11:00:02", "IPv4Address": "172.17.0.2/16", "IPv6Address": "" } }, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} }] 可见,你同时可以发现,在Containers.IPv4Address的内容和容器内获取到的IP是一致的,且com.docker.network.bridge.name所指是网桥名称为docker0。 实验宿主机的IP为192.168.1.10,那在bb-default-bridge中能不能访问到宿主机呢 12345/ # ping 192.168.1.105PING 192.168.1.105 (192.168.1.105): 56 data bytes64 bytes from 192.168.1.105: seq=0 ttl=37 time=4.473 ms64 bytes from 192.168.1.105: seq=1 ttl=37 time=0.773 ms64 bytes from 192.168.1.105: seq=2 ttl=37 time=1.157 ms 结果发现是可以的,反过来宿主机ping容器则ping不通(是不是有点类似Windows中的NAT网络类型)。如果宿主机需要访问容器时,可以在创建容器时使用 -p 参数将宿主机端口和容器端口映射。假如再创建一个bb-default-bridge-1,依旧使用的是默认的Bridge网络驱动。 12345678910111213141516"Containers": { "3af6ea18328e0414ecaac61756ce4e658b24ea89ccd182808a73b424f36ab01d": { "Name": "bb-default-bridge", "EndpointID": "6e4cc1b42cd4c0ffd52bfcbe9e69ac4169a3e41a4e013ec5bb9aa30b82ec1ce5", "MacAddress": "02:42:ac:11:00:02", "IPv4Address": "172.17.0.2/16", "IPv6Address": "" }, "453703548c1416316e776d9dab9a87afb5d006fe23da87ca25416cdad390bffc": { "Name": "bb-default-bridge-1", "EndpointID": "e18409cabc6a10510529c8ded1a6c7f0ceba057473585418b36e1c7406e67af7", "MacAddress": "02:42:ac:11:00:03", "IPv4Address": "172.17.0.3/16", "IPv6Address": "" } }, 会发现,两者均可以互相ping通。也就说,在同一个宿主机的不同容器之间,使用默认Bridge network driver 是可以通过IP相互访问对方的。 User-Defined Bridge Networks用户自定的Bridge网络驱动和默认的Bridge网络驱动类似,那么如何创建一个用户自定义的Bridge网络驱动呢。最简单的方法就是docker network create 语句,更多用法可以参照docker network Command-line reference。比如创建一个叫lazyallen的User-Defined Bridge Network,其中-d是drive的缩写,且是可选项,若不指定默认创建的是Bridge类型的网络驱动。 12345678➜ ~ docker network create -d bridge lazyallen75561be3d1901b8213b795c3c9113392375fe6d57aea2eb1e9b097e2bdc0c0bd➜ ~ docker network lsNETWORK ID NAME DRIVER SCOPE04675183a24d bridge bridge local245b36b452af host host local75561be3d190 lazyallen bridge local43a8d2311d38 none null local 可见,的确出现了一个叫lazyallen的Bridge Network。和默认的Bridge网络驱动相比,在用法上的差别就是 User-Defined Bridge Networks 提供了容器间自动的DNS解析。什么意思呢?我们同样创建两个容器,让他们都使用lazyallen Bridge Driver,使用 –network 可指定network driver 12docker run --rm -it --name bb-user-defined-bridge-0 --network lazyallen busybox shdocker run --rm -it --name bb-user-defined-bridge-1 --network lazyallen busybox sh 容器在网络通信时,会将容器名直接DNS解析为目标容器IP。 1234PING bb-user-defined-bridge-0 (172.18.0.2): 56 data bytes64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.176 ms64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.175 ms64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.185 ms 这样最大的好处就是,容器之间不用再使用IP作为hardcode嵌入到配置中,只要使用容器名称作为引用即可,就算因为网络环境导致容器的IP动态变化,也不用再修改配置。 同样,下面这张图片就可以很好解释Bridge network driver 回过头解释123456docker network create simple-networkdocker run --name xxl-mysql --network simple-network -e MYSQL_ROOT_PASSWORD=123456 -d mysql docker run --network simple-network -e PARAMS="--spring.datasource.url=jdbc:mysql://xxl-mysql:3306/xxl_job?Unicode=true&characterEncoding=UTF-8&useSSL=false --spring.datasource.password=123456" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:2.1.2 通过以上解释,这里就显而易见地做了3件事 创建了一个叫simple-network的自定义Bridge 网络驱动 创建两个容器时均指定simple-network网络驱动 后端应用的配置文件将MySql的地址使用xxl-mysql引用 参考链接 Use bridge networks Docker Swarm Reference Architecture: Exploring Scalable, Portable Docker Container Networks Docker Network—Bridge 模式 [^cnkirito_footnote]: 引用自文章「Docker Network—Bridge 模式」的翻译","categories":[{"name":"Docker","slug":"Docker","permalink":"https://lazyallen.github.io/categories/Docker/"}],"tags":[{"name":"Bridge","slug":"Bridge","permalink":"https://lazyallen.github.io/tags/Bridge/"},{"name":"Network","slug":"Network","permalink":"https://lazyallen.github.io/tags/Network/"}]},{"title":"解惑:究竟什么是IOC(控制反转)?","slug":"what-is-ioc","date":"2019-04-09T16:00:00.000Z","updated":"2022-12-07T07:05:38.406Z","comments":true,"path":"2019/04/10/what-is-ioc/","link":"","permalink":"https://lazyallen.github.io/2019/04/10/what-is-ioc/","excerpt":"本文是我对于控制反转这个概念的理解,从接触IOC到很长的一段时间,我都以为自己理解了控制反转的概念,但实际回过头问自己究竟什么是控制反转,又很难清晰解释。通过不断反问自己问题,搜寻相关的文章,最后才找到了本文的答案。其中另外一个感触是,对于技术当中的概念术语,应该尽可能参考英文原文的解释才是最直接有效的理解方式,而很多中文解释其实大多参杂了那个人对于这个概念的理解,吃别人嘴里吐出来的东西营养价值就不多了。","text":"本文是我对于控制反转这个概念的理解,从接触IOC到很长的一段时间,我都以为自己理解了控制反转的概念,但实际回过头问自己究竟什么是控制反转,又很难清晰解释。通过不断反问自己问题,搜寻相关的文章,最后才找到了本文的答案。其中另外一个感触是,对于技术当中的概念术语,应该尽可能参考英文原文的解释才是最直接有效的理解方式,而很多中文解释其实大多参杂了那个人对于这个概念的理解,吃别人嘴里吐出来的东西营养价值就不多了。 IOC就是把对象的控制权交给容器?IOC作为Spring的特性之一,经常会拿来和Spring一起讨论。正如我第一次接触到IOC的时候,也是在学习Spring的时候。很长一段时间我对于IOC的理解,和搜索引擎的结果的都差不多。比如,在你真的思考过IOC容器吗?这片文章中,作者对于IOC的理解如下: Ioc是把对象的控制权交给框架或容器,容器中存储了众多我们需要的对象,然后我们就无需再手动的在代码中创建对象。需要什么对象就直接告诉容器我们需要什么对象,容器会把对象根据一定的方式注入到我们的代码中。注入的过程被称为DI。 通过对于这段话的理解,IOC做的事情就是无需再手动的在代码中创建对象。需要什么对象就直接告诉容器我们需要什么对象 。貌似听起来挺有道理,控制反转,就是将创建对象的控制权从对象本身反转到了Spring IOC容器。可是,我解释不来我心中的另外一个疑问:难道这样做的目的仅仅就是帮助开发者减少一些创建对象的代码吗?我对此表示否定,感觉以上文章更多介绍的是IOC容器,而并非IOC 控制反转 概念本身。 脱离Spring:从Martin Flower的文章找答案IOC概念出自于Martin Flower的一篇文章,Inversion of Control Containers and the Dependency Injection pattern,我找到了一篇ThoughtWorks的译文,IoC容器和Dependency Injection模式。在这篇文章中,作者用一个MovieFinder的例子介绍了控制反转这个概念: 123456class MovieLister... private MovieFinder finder; public MovieLister() { finder = new ColonDelimitedMovieFinder("movies1.txt"); } 作者同时提出一个问题: 这个实现类的名字就说明:我将要从一个逗号分隔的文本文件中获得影片列表。你不必操心具体的实现细节,只要设想这样一个实现类就可以了。如果这个类只由我自己使用,一切都没问题。但是,如果我的朋友叹服于这个精彩的功能,也想使用我的程序,那又会怎么样呢?如果他们也把影片清单保存在一个逗号分隔的文本文件中,并且也把这个文件命名为” movie1.txt “,那么一切还是没问题。如果他们只是给这个文件改改名,我也可以从一个配置文件获得文件名,这也很容易。但是,如果他们用完全不同的方式——例如SQL 数据库、XML 文件、web service,或者另一种格式的文本文件——来存储影片清单呢?在这种情况下,我们需要用另一个类来获取数据。 12345class MovieLister... public MovieLister(MovieFinder finder) { this.finder = finder; } Dependency Injection模式的基本思想是:用一个单独的对象(装配器)来获得MovieFinder的一个合适的实现,并将其实例赋给MovieLister类的一个字段。 在WIKI 中,我找到关于控制反转最贴切的解释: Class A中用到了Class B的对象b,一般情况下,需要在A的代码中显式的new一个B的对象。采用依赖注入技术之后,A的代码只需要定义一个私有的B对象,不需要直接new来获得这个对象,而是通过相关的容器控制程序来将B对象在外部new出来并注入到A类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如XML)来指定。 IOC的目的:依赖解耦与其回答什么是控制反转,更好的出发点是,控制反转有什么好处,能带来什么?答案就是:解耦我们举这篇回答中的例子来说明。 假如我们需要在文本框对输入的文本进行拼写校验,如下代码可以满足我们的需求。 12345678public class TextEditor { private SpellChecker checker; public TextEditor() { this.checker = new SpellChecker(); }} 但现实情况中,文本框中的文本并非一定是英文,有可能是中文,也有可能是其他语言或者混合语言。那么以上逻辑就需要扩展出新的checker,可是当前的checker是在TextEditor中创建出来,我们需要换一个check的话需要新写一个类。到这里,checker作为TextEditor的依赖,关系就耦合在一起,一旦下层依赖需要发生变动,就一定会影响到上层的代码。暂且不论这样写好不好,类比于现实生活,如同在输出框中输出的不一定是英文,强行将checker和TextEditor绑定在一起就是不合适的(不是说不对,只能说不合适)。那么最简单的方式当然就是,把创建依赖的逻辑放置到外层,通过注入的方式将依赖设置到属性当中。 12345678public class TextEditor { private IocSpellChecker checker; public TextEditor(IocSpellChecker checker) { this.checker = checker; }} 这样做的好处是,极大方便了checker的扩展,也解除了TextEditor和checker的耦合关系。和主动创建相比,这时依赖必须先创建再注入到对象属性当中。这时,我们只要针对不同的语言环境,new出不同的SpellChecker,而不用改写TextEditor里面的逻辑。 12SpellChecker sc = new SpellChecker; // dependencyTextEditor textEditor = new TextEditor(sc); 总结那么我对于控制反转的理解是:控制反转,反转的是依赖的创建权,由本来自己的依赖自己主动创建,反转成了依赖在外部创建,创建好了再注入进来的模式,其中,DI(依赖注入)是实现控制反转的方式而已。所以,这里解答了我对于之前关于IOC理解的一个误区,我以前一直以为IOC就如文章前头说,反转的实例创建的创建权,讲创建权交给了容器而已,并没有理解到更本质的含义。IOC其实更多可以解释为一种模式或者写法,这种写法带来的好处就是解除依赖的耦合。而Spring IOC只是套用了这种模式,简化了依赖创建的部分,将对象和对象的依赖统一管理起来,更方便开发者使用。 相关引用 IoC容器和Dependency Injection模式 WIKI-控制反转 What is Inversion of Control?","categories":[{"name":"解惑","slug":"解惑","permalink":"https://lazyallen.github.io/categories/%E8%A7%A3%E6%83%91/"}],"tags":[{"name":"IOC","slug":"IOC","permalink":"https://lazyallen.github.io/tags/IOC/"}]}]}