spring profile 与 maven profile 多环境管理

实际开发中一个项目至少对应开发、测试、生成三种环境,如何方便的管理多环境是今天的要讨论的主题

Spring Profile

Spring Profile 是 Spring 提供的多环境管理方案。

如图,每种环境都对应一个 properties 文件,然后在application.properties中配置一下要使用的环境

上面配置匹配的是 application-dev.properties,如果写的是test,则匹配 application-test.properties。也就是说,Spring Profile 对配置文件的命名有要求,必须是 application- 开头

除了配置环境外,一些不随环境而变化的配置也应该放到 application.properties中,application-.properties最好只存放与环境相关的配置项

以上就是 Spring Profile 给出的多环境管理方案。通过改变 spring.profiles.active的值来切换不同的环境。这种方法简单易懂,但有两个问题。

  1. 每次切换环境要手动修改 spring.profiles.active 的值
  2. 打包的时候,要手动删除其它环境的配置文件,不然其它环境的敏感信息就都打包进去了

为了解决这两个问题,我们需要 maven profile 的配合

maven profile

  maven 的 profile 可以让我们定义多套配置信息,并指定其激活条件,然后在不同的环境下使用不同的profile配置。   

profile 的定义位置

在maven中有两个地方可以配置 profile

  1. pom.xml中:这里面定义的 profile 作用范围是当前项目
  2. {user}/.m2/settings.xml中:这里面定义的 profile 作用范围是所有使用了该配置文件的项目

settings.xml中的 profile

不同的地方 profile 中能定义的信息也不相同

  • 由于settings.xml作用范围宽泛, profile 中只能定义一些公共信息,如下
  • id:该 profile 的唯一标识
  • activation:在哪些情况下激活 profile,这里面有多种策略可供选择,只要满足其中一个条件就激活
  • repositories:远程仓库

由于能配置的东西有限,一般都会将 maven profile 配置在pom.xml

pom.xml中 的profile

pom.xml中:profile 能定义的东西就非常多了,如下

当然我们的目的也不是把它配全,而是解决 Spring Profile 遗留下来的两个问题。

首先看第一个问题

问题1

“每次切换环境要手动修改spring.profiles.active的值”

这个问题就可以通过配置 profile 解决,在pom的根节点下添加

如上,定义了三套环境,其中id为dev的是默认环境,三套环境中定义了叫 env的“变量”

如果你用的是idea编辑器,添加好后,maven控件窗口应该会多出一个 Profiles,其中默认值就是上面配置的dev

最小化的 profiles 已经配置好了,通过勾选上图中的Profiles,就可以快速切换 maven的 profile 环境。

现在 maven profile 可以通过 勾选上图中的Profiles 快速切换环境

Spring Profile 还得通过 手动修改spring.profiles.active的值来切环境

现在的问题是怎样让 maven profile的环境与Spring Profile一一对应,达到切换maven profile环境时,Spring Profile环境也被切换了

还记得maven profile 中定义的 env “变量”吗,现在只需要把

改成

就将maven profile 与 Spring Profile 环境关联起来了

当maven profile 将 环境切换成 test 时,在pom中定义的id为test的profile环境将被激活,在该环境下env的值是test,maven插件会将 @env@ 替换为 test,这样Spring Profile的环境也随之发生了改变。从上面可以看出,自定义的”变量”env的值还不能乱写,要与Spring Profile的环境相对应。

总结

  • 第一步,在pom文件中配置 profiles
  • 第二步,在application.properties配置文件中添加 spring.profiles.active=@env@

问题2

打包的时候,要手动删除其它环境的配置文件,不然其它环境的敏感信息就都打包进去了

解决这个问题需要在pom根节点下中配置 build 信息

  • directory:资源文件所在目录
  • includes:需要包含的文件列表
  • excludes:需要排除的文件列表

如上,配置了两个 <resource>,第一个先排除了src/main/resources目录下所有 application 开头是配置文件,第二个在第一个的基础上添加了所需的配置文件。注意 application-${env}.yml,它是一个动态变化的值,随着当前环境的改变而改变,假如当前环境是 id叫 dev的 profile,那么env的值为 dev。

这样配置后,maven在build时,就会根据配置先排除掉指定的配置文件,然后根据当前环境添加所需要的配置文件。

0

Java位运算在实际项目中的运用(开关)

之前《Java里位操作总结》这篇文章有介绍Java位运算的基本操作,这篇将介绍下Java位运算在实际项目中的运用。

位运算的性能是非常好的,相比运算流程,计算机更喜欢这种纯粹的逻辑门和移动位置的运算,但位运算在平常的业务代码里并不太常见,因为它的可读性不太好,但是我们仍然可以利用位运算来解决一些实际项目里的问题。

比如用来表示开关的功能,比如需求里经常有这种字段:是否允许xx(0不允许,1允许),是否有yy权限(0没有,1有),是否存在zz(0不存在,1存在)

上面只是举例,类似这种只有两种取值状态的属性,如果当成数据库字段放进去的话,太过浪费,如果之后又有类似的字段,又得新增数据库字段,为了只有两种取值的字段,实在是不太值得。

这个时候何不用一个字段来表示这些字段呢?你可能已经猜到要怎么做了:

一个int型或者long型的字段,让它的每一个二进制位拥有特殊含义即可,然后按照位运算将其对应的位置上的数值变成0或1,那如何将某个数的二进制位第x位上的数值变成1或0呢?其实这在位图结构里经常用到,就是利用1这个特殊的值作位移运算后再与原值进行位运算,让我们看下这个过程:

把一个数的第2位的字符变成1,现在假设这个数初始化为0,int型,我们把它当成二进制展示出来:

现在如何把这个数的第二位变成1呢?目前是这样做的:

即原值跟1左移1位后的值作或运算,先来看看1 << 1的结果:

然后拿着上图的结果,跟原数(也就是0)进行或运算:

可以看到,原数的第二位已经被置为1了,它的十进制对应2,其它位的数置为1也大同小异,例如,现在让第6位也变成1只需要:

即拿着原值(现在为2)跟1左移5位后的数做或运算,这个流程如下:

看完了把某个位置的数值置为1,那如何把某位设置为0呢?我们现在把上图里的结果的第6位重新置回0,目前的做法为:

即拿着原值(经过上面几步的运算,现在值为32)跟1左移5位按位取反后的数做与运算,来看下这个流程:

经过上面的流程,就可以把原值的第6位变成0了。

那么我们知道了让一个数的二进制位的某位变成0或1的方法,那如何知道一个数的某位上究竟是0还是1呢?毕竟我们业务代码需要知道第几位代表什么意思并且获取到对应位置上的值。

假如我现在想知道十进制int型数34的第6位是0还是1,写法如下:

即让原值(34)右移5位后跟1做与运算,来看下这个流程:

由图可以看出,想要知道一个数的第几位是1还是0,只需要将其对应位置上的值“逼”到最后一位,然后跟1相与即可,如果对应位置上的值是0,那么与1相与后的结果一定为0,反之一定为1。

总结

到这里已经说完了为什么要用一个数表示那么多开关,以及如何给一个开关位设置对应的开关值,以及如何找到对应开关位的值,有了这些操作,我们再也不需要为这种只有0和1取值的字段新增数据库字段了,因为一个int型的数字,就可以表达32个开关属性(一个int是4个字节,一个字节有8位,那么int有32个01代码二进制,去掉int类型的最左边的一个01代码,这个01代码使用来表示正负号的,那么去掉这个01代码之后,还剩下31个01代码。那么这31个01代码就可以设置成31个开关,0表示false,1表示true),如果超了,还可以扩成64位的long型。

代码

有了上面的理论总结,下面就写个Java代码实现业务,业务逻辑是本系统商品数量或属性有变化时需要有选择的通知各大电商平台。

+2

java判断数组是否为空

java中判断数组是否为空

0

Java面试题:HashMap中当key为类时的注意事项

HashMap中,当key为类时,比较key是否相等需要重写equal() 和hashCode()这两个方法。

在HashMap中,如果key为类对象,则必须要重写hashCode() 和equal()这两个方法。Why?首先了解下未被重写的hashCode() 和equal()方法。

1.未被重写的hashCode() 和equal()方法

public int hashCode():HashCode是根类Obeject中的方法。默认情况下,Object中的hashCode() 返回对象的32位jvm内存地址。也就是说如果对象不重写该方法,则返回相应对象的32为JVM内存地址。 

public bollean equals():用于比较两个对象是否相同,它其实就是使用两个对象的内存地址在比较。Object类中的equals方法内部使用的就是==比较运算符。

2.在HashMap中,判断两个对象(key)是否相等的规则:

2.1.判断两个对象的hashCode()是否相等

如果不相等,认为两个对象也不相等,完毕

如果相等,转入2

2.2.判断两个对象的equals()是否相等

如果不相等,认为两个对象也不相等

如果相等,认为两个对象相等

3.为什么重载hashCode方法?

一般的地方不需要重载hashCode(),只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会重载hashCode(),那么为什么要重载hashCode()呢?

如果不重载hashCode()方法,由于两个对象实例的内存地址不同,根据2.1,则判为不等。

重载之后,hashCode()方法变成判断其逻辑上是否相同,根据2.1,则判为相等。

4.为什么要重载equal方法?

因为Object的equal方法默认是两个对象的引用的比较,意思就是指向同一内存,地址则相等,否则不相等;如果你现在需要利用对象里面的值来判断是否相等,则重载equal方法。

5.代码示例

假设SysConfig是要作为hashmap的key的一个类

引申扩展:为什么hashmap里的key经常使用string,并且不需要重写hashcode和equals方法?

在《Java 编程思想》中有这么一句话:设计 hashCode() 时最重要的因素就是对同一个对象调用 hashCode() 都应该产生相同的值。String 类型的对象对这个条件有着很好的支持,因为 String 对象的 hashCode() 值是根据 String 对象的内容计算的,并不是根据对象的地址计算。

下面是 String 类源码中的 hashCode() 方法:String 对象底层是一个 final 修饰的 char 类型的数组,hashCode() 的计算是根据字符数组的每个元素进行计算的,所以内容相同的 String 对象会产生相同的散列码。

我们希望的是我们在用对象作为 key 时,我们在获取的时候也依然能够根据重新定义 key 获得对应的 value。就像下面这样,但是它却不能正常工作:这两个对象产生的散列码是不同的,所以在进行 equals() 判断的时候这两个对象被认定为是不同的对象,自然也就获取不到对应的 value。

如果你想获得 map 中存储的“123”,那么你只有把 test 对象作为 key 才能获取到,我们试图用一种简单的方法就能让它工作,但是这显然与我们的初衷相背驰。所以非 String 类型的数据类型在判断 key 相同时所需要的条件太过苛刻。

输出 null

但是 String 类型的对象就不一样了,内容相同的两个 String 对象具有相同的散列码,并且经过 equals() 判断后返回值为 true,所以在进行查找的时候它可以正常工作。

输出 123

总结:在使用 String 类型的对象做 key 时我们可以只根据传入的字符串内容就能获得对应存在 map 中的 value 值,而非 String 类型的对象在获得对应的 value 时需要的条件太过苛刻,首先要保证散列码相同,并且经过 equals() 方法判断为 true 时才可以获得对应的 value。

+3

Java动态数组理解

1.Java动态数组的用法详解

Java动态数组是一种可以任意伸缩数组长度的对象,在Java中比较常用的是ArrayList,ArrayList是javaAPI中自带的java.util.ArrayList。下面介绍一下ArrayList作为Java动态数组的用法。

语法:add()是添加一个新的元素,remove()删除一个元素,size()获得ArrayList的长度。ArrayList的下标是从0开始。

2.动态数组实现(ArrayList原理)

2.1 什么是数组

同类数据元素的集合,在计算机中以连续的地址存储,编译时确定长度,无法改变。

2.2 什么是动态数组

数据结构中顺序表的物理实现,同类数据元素的集合,在计算机中以连续的地址存储,大小在创建时决定,但是可以改变。

2.3 为什么使用动态数组

支持随机访问,查询速度快。但是插入和删除都需要移动元素,比起链表开销较大。如:java集合类中的ArrayList Vector等

2.4 动态数组实现代码(ArrayList原理)

0

使用spring自定义参数解析器和自定义注解实现可插拔的获取登陆用户信息

前言

Web系统里,调接口时,通常需要获取当前登陆系统的用户信息,实现方法有很多,常见的是在aop切面里拦截接口进行是否鉴权验证,然后获取当前用户信息。

本篇文章,主要介绍的是另一种方式,spring的自定义参数解析器HandlerMethodArgumentResolver,结合自定义注解实现。

使用HandlerMethodArgumentResolver可以灵活的给某个接口做统一的参数处理,废话不多说,以下是实现步骤。

一、定义一个注解

改注解是用于标记待处理的参数,这边就是我们的User。

二、自定义参数处理类SecurityUserMethodArgumentResolver

让它实现HandlerMethodArgumentResolver,需要根据自己的业务重写两个方法。

  1. supportsParameter:用于判定是否需要处理该参数分解,返回true为需要,并会去调用下面的方法resolveArgument。
  2. resolveArgument:真正用于处理参数分解的方法,返回的Object就是controller方法上的形参对象。

三、controller类里使用

0

Shiro使用之ShiroFilterFactoryBean

Shiro提供了与Web集成的支持,其通过一个ShiroFilter入口来拦截需要安全控制的URL,然后进行相应的控制。本文主要介绍在spring-boot 中用ShiroFilterFactoryBean 来创建ShiroFilter:

+2

AspectJ与Shiro不兼容和Spring二次代理错误分析

Shiro与AspectJ的配置

Shiro的配置

Shiro在使用注解的时的配置是

其基于注解的权限控制功能

  1. 根据类名可以推断是通过切面的Advisor来完成的(AuthorizationAttributeSourceAdvisor)
  2. 所以说它需要创建自己的动态代理类,是由Spring的DefaultAdvisorAutoProxyCreator动态代理创建的

SpringMVC中AspectJ的配置

根据上文,启用基于注解的AspectJ就很简单了,因为基于注解的Shiro一般也在SpringMVC的Context里,我们采用如下配置

在两者分别配置的时候,配置方法都是对的但是一旦公用,会发现AspectJ会失效

失效的解决办法

方法一

解决办法也很简单,注释掉Shiro配置中的第一句

注释之后Shiro的注解权限管理功能并不会失效,具体原因我们来细细分析

方法二

给DefaultAdvisorAutoProxyCreator加入参数proxyTargetClass为true

配置失效的原因

原因在于二次代理

  1. 由于使用了aop:aspectj-autoproxy强制了proxy-target-class
  2. 也就是说对Web层的Class(主要是Controller)使用了CGLib代理
  3. 然后在Shiro进行代理时使用DefaultAdvisorAutoProxyCreator
  4. 原本应该判断Controller,发现没有任何接口,所以使用CGLib来代理
  5. 但是由于Controller已经被CGLib代理过一次了
  6. DefaultAdvisorAutoProxyCreator拿到对不是Contoller本身,而是CGLib的代理结果
  7. CGLib的代理结果本身是有接口的,干扰了DefaultAdvisorAutoProxyCreator的内部判断
  8. 使用JDK去代理CGLib的代理结果
  9. 结果Controller的函数时去了CGLib的接口中找方法名,发现方法不存在,导致代理失败

为什么会确定是这个原因,废了我好大功夫,详情请看代码追踪。。。。没精力就别看了

代码的追踪(可以不看)

aspectj-autoproxy

在配置文件中的 aop:aspectj-autoproxy 会最终交给名为 AopNamespaceHandler 的类进行处理,进入该类(直接在工程全局搜)我们可以看到

aspectj-autoproxy 是由 AspectJAutoProxy BeanDefinitionParser 注册的

  1. 进入AspectJAutoProxyBeanDefinitionParser
  2. 找到parse函数,看到其调用registerAspectJAnnotationAutoProxyCreatorIfNecessary来注册BeanDefinition
  3. 进入registerAspectJAnnotationAutoProxyCreatorIfNecessary可以看到内部的三个register方法
  4. 三个register方法分别对应上述代码中第一个参数的输入,但是均调用了AopConfigUtils
  5. 点击AopConfigUtils,可以发现所有的方法都最终调用registerOrEscalateApcAsRequired
  6. registerOrEscalateApcAsRequired中有个简单的if…else判断
  7. 主要控制了逻辑,如果存在internalAutoProxyCreator则不进行创建新的

由于

最终会在第七步骤的函数里,由于这个if…else判断导致不能存在两个代理,所以不能混合使用 DefaultAdvisorAutoProxyCreator 与 aop:aspectj-autoproxy这个结论是错误的,是因为DefaultAdvisorAutoProxyCreator不会调用registerAutoProxyCreatorIfNecessary,产生错误的原因是 二次代理 作者追踪错了代码,只是恰巧改对了

DefaultAdvisorAutoProxyCreator

来,跟着我找 DefaultAdvisorAutoProxyCreator 如何创建代理的代码

  1. 进入DefaultAdvisorAutoProxyCreator(直接Command或者Ctrl+点击进入)
  2. 看到其继承于AbstractAdvisorAutoProxyCreator,进入
  3. 可以看到AbstractAdvisorAutoProxyCreator继承于AbstractAutoProxyCreator继续进入
  4. AbstractAutoProxyCreator中有一个方法叫做createProxy,名字太直接了,叫做创建代理
  5. 在createProxy中有一个ProxyFactory对象,就是代理的工厂模式(工厂模式请自行学习)
  6. ProxyFactory对象继承于ProxyCreatorSupport
  7. ProxyCreatorSupport中有对象aopProxyFactory用来创建AOP代理(就是切面代理,注意切面代理是属于动态代理的一种)
  8. ProxyCreatorSupport在构造函数中new了一个DefaultAopProxyFactory给aopProxyFactory赋值
  9. 进入DefaultAopProxyFactory可以看到切面代理创建方法createAopProxy
  10. 在判断条件包含 !config.isProxyTargetClass() 时,也就是不使用针对Class的代理的时候,看下一句
  11. return new JdkDynamicAopProxy(config) 根据config配置返回JDK动态代理

这也是我们在文章一中所说的,通常情况下Spring使用针对接口的JDK代理进行动态代理,绕了这么久,我们回到第五步,看 createProxy 方法如何使用 ProxyFactory对象

  1. 可以看到createProxy 返回值 return proxyFactory.getProxy(this.getProxyClassLoader())
  2. 进入ProxyFactory对象的getProxy方法
  3. 可以看到 return this.createAopProxy().getProxy(classLoader)
  4. 首先调用了createAopProxy,发现ProxyFactory没有这个方法,所以来自父类 ProxyCreatorSupport
  5. 点击进入 看到createAopProxy 内包含this.getAopProxyFactory().createAopProxy(this)
  6. 首先拿到了上文第九步创建的DefaultAopProxyFactory,然后调用了其createAopProxy

以上完成了 DefaultAdvisorAutoProxyCreator 动态代理的创建

两次代理的内部细节

有一些文章分析给出无法确定二次代理的情况下哪个代理成功 ,但是根据我本身追踪日志发现,在Spring启动时,我的切面类日志输出成功

但是在调用接口时,开启DEBUG日志,会发现Shiro抛出的异常为

可以在日志中发现几个关键字

  1. org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain
  2. com.sun.proxy.$Proxy103
  3. InvocableHandlerMethod.doInvoke

三个关键字分别说明了

  1. 是Shiro产生了异常,而不是AOP
  2. Shiro尝试去采用了JDK代理
  3. 报错在Invoke方法时

解决方法生效的原因

方法一

因为Shiro也是基于Spring的AOP类的,如果找不到合适的配置,就是默认采用同一个Context下的AOP代理配置,我们给了其proxy-target-class为true,自然就在第二次代理的时候找得到方法

方法二

方法二就更直接了,告诉DefaultAdvisorAutoProxyCreator为True就好

杂想

另外,SpringMVC和Spring公用的情况下Transactional失效的问题 很有可能底层原因也是二次代理。

0

java数组转字符串

java数组转字符串有很多方法,下面就一一列举

方法一: 遍历

方法二: 使用StringUtils的join方法

方法三: 使用ArrayUtils的toString方法

0

初步了解kubernetes

什么是kubernetes

一个可以自动部署、拓展和管理容器化的应用(比如运行在Docker容器里面的应用)的系统,开源的。英文缩写为k8s。

它可以做什么

它可以让你快速地部署分布式应用,让你在发布和升级应用的时候,保持应用正常运转,不需要停止应用。它是一个容器化应用编排器,能够帮你把各个容器化应用编排好,让它们能够有条不紊地运作。比如你在部署了一个应用之后,要进行版本升级,k8s会创建新的pod来试探性地部署新版本的应用,确保新版本的应用能正常运作之后再把旧版本的引用一个个地替换成新版本的应用,因此如果新版本的应用部署失败了,也完全不会影响就版本的应用。

服务注册与发现

当你用k8s部署多个需要相互协作的应用的时候,k8s就相当于一个注册中心(如zookeeper, consul, eureka)。使用k8s部署应用,你的应用不再需要一个相应的客户端来注册和发现服务,k8s的pod和service已经帮你实现了这些东西,除非你需要客户端的负载均衡(这里指的是客户端需要明确指定每次需要访问哪台服务器的那种负载均衡,这需要获取服务器的信息,所以要在代码层面显式地和k8s进行通信获取信息),这时候你才需要在应用的代码里面和k8s进行通信来获取负载均衡需要的信息。这点非常不错,因为如过我们使用eureka实现服务注册与发现的话,那我们就需要一个相应的客户端来和eureka服务端来进行通信,然后向zookeeper注册服务,获取服务等等。问题是,eureka是用Java实现的,它默认只有Java客户端,如果你用的其他语言,你需要自己实现一个客户端和eureka服务端进行通信。

Pod的图解

一个Pod里面可以运行一个容器化应用(这个应用里面可能跑多个程序,比如一个web后端程序和一个数据库),Pod是有生命周期的,随时可能应为应用故障被移除停止或者部署新应用而被创建,Pod在Node里面运行,一个Node能放任意个Pod。

Node的图解

Node对应一台物理主机或者虚拟机,能放任意个Pod,一个Node里面至少运行着一个kubelet进程和容器程序(比如Docker)

Service的图解

Service以一个或者多个Pod为基本逻辑单位,图上面画得很清楚了,更多细节可以查看官方文档

负载均衡

k8s的负载均衡是服务端的负载均衡,也就是当用户访问某个服务的时候,用户的请求会根据负载均衡算法被分配到应用集群的其中一个应用上面(其实是某一个pod上面),这得益于k8s的Service抽象,在k8s里面,一个应用部署之后,是运行在一个pod里面的,假设这个应用一共部署了5个实例,将会创建5个pod来运行这些应用,组成一个集群,每个pod都有相应的ip地址,但是这些ip地址是内部的ip地址,外部不能直接访问到(在同一个k8s集群的网络里面的应用是可以相互访问的),需要通过Service暴露给外部, 当用户访问这些应用提供的服务的时候,会由它们对应的Service通过Label和Selector定位到这些应用,然后决定将请求交给哪一个应用处理。那么Service如何定位呢?Service可以通过不同的配置方式来进行定位

健康检查

k8s能自动重启启动失败的应用,当一个Node挂掉后,它会使用其他可用的Node重新部署挂掉的Node里面的应用,当一个应用没有响应你自定义的健康检查的时候,它会停掉这个应用,并且不把这个应用暴露给客户端,直到这个应用恢复正常。

简单上手

要简单地上手,最快的方式其实是用官网上的tutorial里面的网页终端,但是我这里网络状况比较糟糕,只能本地安装了

安装kubectl

我的机器系统是Ubuntu18.10,按照文档输入几个命令就行了。
//TODO

0