作者归档:徐承恩

Redis缓存击穿解决方案

缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

缓存击穿

key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。

解决方案

使用互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

public String get(key) {
    String value = redis.get(key);
    if (value == null) { //代表缓存值过期
        //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
        if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
            value = db.get(key);
            redis.set(key, value, expire_secs);
            redis.del(key_mutex);
        } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
            sleep(50);
            get(key);  //重试
        }
    } else {
        return value;
    }
}

Memcache

if (memcache.get(key) == null) {  
    // 3 min timeout to avoid mutex holder crash  
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
        value = db.get(key);  
        memcache.set(key, value);  
        memcache.delete(key_mutex);  
    } else {  
        sleep(50);  
        retry();  
    }  
}

 

Redis缓存穿透解决方案

缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

缓存穿透

一个一定不存在缓存及查询不到的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

方案一

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

方案二

另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

//伪代码
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";

    String cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    }

    cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    } else {
        //数据库查询不到,为空
        cacheValue = GetProductListFromDB();
        if (cacheValue == null) {
            //如果发现为空,设置个默认值,也缓存起来
            cacheValue = string.Empty;
        }
        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        return cacheValue;
    }
}

 

使用Portainer搭建你的实验靶场

Portainer简化了Docker,Swarm,Kubernetes,ACI和EDGE环境中的容器管理。工程师和DevOps团队使用它来加速软件部署,解决问题并简化迁移。

作为一名后端开发工程师苦于敲各种各样的命令行,亦或是学习中间件的时候常常要搭建各式各样的基础设施而浪费大量时间。

Portainer的可视化操作方便了开发者搭建各式各样的基础设施,使得开发者有更多的时间进行业务开发。

部署Portainer有多简单?

使用Docker安装Portainer

Portainer由两个元素组成,即Portainer服务器Portainer代理这两个元素都作为轻量级Docker容器在Docker引擎或Swarm集群中运行。由于Docker的性质,存在许多可能的部署方案,但是,我们在下面详细介绍了最常见的方案。请使用与您的配置匹配的方案(或者如果未列出您的配置,请参阅portainer.readthedocs.io以获取其他选项)。

请注意,使用Swarm时建议的部署模式是使用Portainer代理。

仅在Linux(CentOS 7和8,Ubuntu 16.04.6 LTS,18.04.4 LTS和20.04 LTS)和Windows(Win 10> 1909和Server 2019> 1909)上运行时才正式支持Portainer。Portainer未在MacOS或任何其他OS或OS系列/版本上进行测试。

独立的LINUX Docker主机 /单节​​点群集集群(或以“ Linux容器”模式运行的Windows 10 Docker主机)上部署Portainer Server 。

使用以下Docker命令部署Portainer服务器;请注意,在独立主机上不需要代理,但是如果使用代理,它会提供其他功能(请参阅下面的portainer和代理场景):

$ docker volume create portainer_data
$ docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer

您只需要使用浏览器访问运行portainer的Docker引擎的9000端口。

注意:端口9000是Portainer用于UI访问的常规端口。EDGE代理仅将端口8000用于反向隧道功能。如果您不打算使用边缘代理,则不需要公开端口8000

注意-v /var/run/docker.sock:/var/run/docker.sock选项只能在Linux环境中使用。

独立的WINDOWS Docker主机(运行Windows容器)上部署Portainer Server –注意必须是Windows 1909或更高版本。

$ docker volume create portainer_data
$ docker run -d -p 8000:8000 -p 9000:9000 --name portainer --restart always -v \\.\pipe\docker_engine:\\.\pipe\docker_engine -v portainer_data:C:\data portainer/portainer

您只需要使用浏览器访问运行portainer的Docker引擎的9000端口。

InvalidKeyException: Illegal key size异常解决方案

测试环境的出款定时任务大量抛出InvalidKeyException: Illegal key size异常,根据以往经验初步判断是加密出现问题需要安装JCE相关的包。

产生错误原因

为了数据代码在传输过程中的安全,很多时候我们都会将要传输的数据进行加密,然后等对方拿到后再解密使用。我们在使用AES加解密的时候,在遇到128位密钥加解密的时候,没有进行什么特殊处理;然而,在使用256位密钥加解密的时候,如果不进行特殊处理的话,往往会出现这个异常

java.security.InvalidKeyException: Illegal key size

com.itrus.cryptorole.CryptoException: org.bouncycastle.cms.CMSException: key invalid in message

为什么会产生这样的错误?

我们做Java开发,或是Android开发,都会先在电脑上安装JDK(Java Development Kit) 并配置环境变量,JDK也就是 Java 语言的软件开发工具包,JDK中包含有JRE(Java Runtime Environment,即:Java运行环境),JRE中包括Java虚拟机(Java Virtual Machine)、Java核心类库和支持文件,而我们今天要说的主角就在Java的核心类库中。在Java的核心类库中有一个JCE(Java Cryptography Extension),JCE是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现,所以这个是实现加密解密的重要类库。

在我们安装的JRE目录下有这样一个文件夹:%JAVE_HOME%\jre\lib\security(%JAVE_HOME%是自己电脑的Java路径,一版默认是:C:\Program Files\Java,具体看自己当时安装JDK和JRE时选择的路径是什么),其中包含有两个.jar文件:“local_policy.jar ”和“US_export_policy.jar”,也就是我们平时说的jar包,再通俗一点说就是Java中包含的类库(Sun公司的程序大牛封装的类库,供使用Java开发的程序员使用),这两个jar包就是我们JCE中的核心类库了。JRE中自带的“local_policy.jar ”和“US_export_policy.jar”是支持128位密钥的加密算法,而当我们要使用256位密钥算法的时候,已经超出它的范围,无法支持,所以才会报:“java.security.InvalidKeyException: Illegal key size or default parameters”的异常。那么我们怎么解决呢?

如何解决

去官方下载JCE无限制权限策略文件。

JDK5 JCE下载

JDK6 JCE下载

JDK7 JCE下载

JDK8 JCE下载

下载后解压,可以看到local_policy.jarUS_export_policy.jar以及readme.txt

如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件。

如果安装了JDK,还要将两个jar文件也放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件。

Java的JIT即时编译及其优化

Oracle的Hotspot JVM实现,是目前OpenJDK使用的主流JVM,它采用解释与编译混合执行的模式,其JIT技术采用分层编译,极大地提升了Java的执行速度。

Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行的特别频繁时,会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器,不是Java虚拟机内必须的部分)。

要了解HotSpot虚拟机内的即时编译器的运作过程,要解决几个问题:

一、为何HotSpot虚拟机要使用解释器和编译器并存的架构?
二、为何HotSpot虚拟机要实现两个不同的即时编译器?
三、程序何时使用解释器执行?何时使用编译器执行?
四、哪些程序代码会被编译成本地代码?如何编译?
五、如何从外部观察即时编译器的编译过程和编译结果?

解释器和编译器

二者的优势:当程序需要快速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,越来越多的代码被编译成本地代码,可以获取更好的执行效率。解释器比较节约内存,编译器的效率比较高。解释器还可以作为编译器激进优化操作的“逃生门”,当激进优化的假设不成立,就退回到解释状态继续执行。

HotSpot内置了两个编译器,分别是Client Compiler和Server Complier,或者简称为C1和C2编译器。同时用到两个编译器的分层编译(Tiered Compilation)策略,使用后,C1和C2同时工作,有些代码可能多次编译,用C1获取更高的编译速度,C2获取更好的编译质量:

第0层,程序解释执行,解释器不开启性能监视功能(Profiling),可触发第1层编译。
第1层,也称为C1编译,将字节码编译成本地代码,进行简单、可靠的优化,若有必要将加入性能监控的逻辑。
第2层,也称为C2编译,也是将字节码编译成为本地代码,但是会启动一些编译耗时较长的优化,甚至会根据性能监控进行一些不可靠的激进优化。

编译对象和触发条件

在运行过程中被即时编译器编译的“热点代码”有两类,即:

一、被多次调用的方法。
二、被多次执行的循环体。

对第一种情况,由于是方法调用触发的编译,因此编译器会以整个方法作为编译对象,即标准的JIT编译方式。后一种,虽然是循环体触发的编译动作,但编译器依然按照整个方法(而不是单独的循环体)作为编译对象。这种编译方式称为栈上替换(On Stack Replacement,简称为OSR编译)。

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测(Hot Spot Detection),目前有两种方法:

一、基于采样的热点探测:采用这样的方法的虚拟机会周期性的检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。其好处就是实现简单、高效,还可以很容易的获取方法调用关系(将调用栈展开即可),缺点是很难精确的确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响。
二、基于计数器的热点探测:为每一个方法(甚至是代码块)建立计数器,统计方法的执行次数,超过一定的阈值就认为是“热点方法”。缺点是实现起来更麻烦,需要为每个方法建立并维护计数器,并且不能直接获取到方法的调用关系,优点是它的统计结果相对来说更加精确和严谨。
HotSpot虚拟机使用第二种,它为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter,用于统计一个方法中循环体代码执行的次数)。

执行有三种模式:

一、解释执行。

二、JIT编译执行。

三、JIT编译与解释混合执行(主流JVM默认执行模式)。

混合执行模式的优势在于解释器在启动时先解释执行,省去编译时间。随着时间推进,JVM通过热点代码统计分析,识别高频的方法调用、循环体、公共模块等,基于强大的JIT动态编译技术,将热点代码转换成机器码,直接交给CPU执行。JIT的作用是将Java字节码动态地编译成可以直接发送给处理器指令执行的机器码。

注意事项

注意解释执行与编译执行在线上环境微妙的辩证关系。机器在热机状态可以承受的负载要大于冷机状态(刚启动时),如果以热机状态时的流量进行切流,可能使处于冷机状态的服务器因无法承载流量而假死。在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,建议每个批次的机器数至多占到整个集群的1/8。

案例

某程序员在发布平台进行分批发布,在输入发布总批数时,误填成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前1/2发布成功的服务器马上全部宕机,此故障说明了JIT的存在。

《阿里巴巴编码规范》认证证书你值得拥有

Apsara Clouder基础技能认证:阿里巴巴编码规范

无规矩不成方圆,无规范不能协作。阿里近万名Java技术精英的经验总结,铸就了高含金量的《阿里巴巴Java开发手册》,并向业界开放,希望使团队在Java开发上更高效、容错、有协作性,提高代码质量并降低维护成本。本认证是对你的编码风格是否符合该手册的证明。

老徐趁着周末花了两天时间认真研读《阿里巴巴Java开发手册》,通过两次考试沉着应对临危不乱终于拿到认证证书。

JDK1.6报错”the trustAnchors parameter must be non-empty”解决方案

如果您使用 Mac OSX 自带的 JDK 1.6 进行加解密相关操作的时候,报了如下错:

java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty

如果你本地安装了其它 Oracle 官方的 JDK,可以先删除原来的无用 3 个软链,然后将这三个文件指向 JDK 1.7 或者 JDK 1.8 的。

cd /Library/Java/JavaVirtualMachines/1.6.0_65-b14-462.jdk/Contents/Home/lib/security
rm -f cacerts trusted.libraries blacklist
sudo ln -s /Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/security/cacerts cacerts
sudo ln -s /Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/security/trusted.libraries trusted.libraries
sudo ln -s /Library/Java/JavaVirtualMachines/jdk1.8.0_152.jdk/Contents/Home/jre/lib/security/blacklist blacklist

 

Java静态代理动态代理总结

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。

代理模式UML类图

总结

静态代理实现较简单,只要代理对象对目标对象进行包装,即可实现增强功能,但静态代理只能为一个目标对象服务,如果目标对象过多,则会产生很多代理类。

JDK动态代理需要目标对象实现业务接口,代理类只需实现InvocationHandler接口。

动态代理生成的类为 lass com.sun.proxy.$Proxy4,cglib代理生成的类为class com.cglib.UserDao$$EnhancerByCGLIB$$552188b6。

静态代理在编译时产生class字节码文件,可以直接使用,效率高。

动态代理必须实现InvocationHandler接口,通过反射代理方法,比较消耗系统性能,但可以减少代理类的数量,使用更灵活。

cglib代理无需实现接口,通过生成类字节码实现代理,比反射稍快,不存在性能问题,但cglib会继承目标对象,需要重写方法,所以目标对象不能为final类。

Java设计模式之动态代理CGLib版

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。

代理模式UML类图

动态代理CGLib版

cglib is a powerful, high performance and quality Code Generation Library. It can extend JAVA classes and implement interfaces at runtime.

cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

cglib特点

1、JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口。如果想代理没有实现接口的类,就可以使用CGLIB实现。

2、CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。

3、CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉。

cglib与动态代理最大的区别

1、使用动态代理的对象必须实现一个或多个接口

2、使用cglib代理的对象则无需实现接口,达到代理类无侵入。

使用cglib需要引入cglib的jar包,如果你已经有spring-core的jar包,则无需引入,因为spring中包含了cglib。

CGLib的Jar包Maven依赖

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

动态代理CGLib版代码示例

AccountDao接口类

package com.github.xuchengen.proxy.statics;

/**
 * 账户表数据访问层接口
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2020/4/1 2:03 下午
 */
public interface AccountDao {

    /**
     * 保存
     */
    void save();

}

AccountDaoImpl接口实现类

package com.github.xuchengen.proxy.statics;

/**
 * 账户表数据访问层接口实现
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2020/4/1 2:04 下午
 */
public class AccountDaoImpl implements AccountDao {

    @Override
    public void save() {
        System.out.println("保存账户数据");
    }

}

CGLibProxy代理工厂类

package com.github.xuchengen.proxy.statics;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * CGLib代理工厂类
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2020/4/1 4:05 下午
 */
public class CGlibProxyFactory implements MethodInterceptor {

    private Object target;

    public CGlibProxyFactory(Object target) {
        this.target = target;
    }

    /**
     * 为目标对象生成代理对象
     */
    public Object getProxyInstance() {
        //工具类
        Enhancer en = new Enhancer();
        //设置父类
        en.setSuperclass(target.getClass());
        //设置回调函数
        en.setCallback(this);
        //创建子类对象代理
        return en.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        try {
            System.out.println("开启事务");
            Object returnValue = method.invoke(target, args);
            System.out.println("关闭事务");
        } catch (Exception e) {
            System.out.println("事务回滚");
        }
        return null;
    }

}

CGLibDynamicProxyTest测试类

package com.github.xuchengen.proxy.statics;

/**
 * JDK动态代理测试
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2020/4/1 3:39 下午
 */
public class CGLibDynamicProxyTest {

    public static void main(String[] args) {
        AccountDao accountDao = new AccountDaoImpl();

        CGlibProxyFactory cGlibProxyFactory = new CGlibProxyFactory(accountDao);

        AccountDao proxyInstance = (AccountDao) cGlibProxyFactory.getProxyInstance();

        proxyInstance.save();
    }

}

控制台输出

开启事务
保存账户数据
关闭事务

Java设计模式之动态代理JDK版

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式。

代理模式UML类图

动态代理JDK版

动态代理利用了JDK标准库API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。

静态代理与动态代理的区别主要在:

1、静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件

2、动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中

特点:
动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。

JDK中生成代理对象主要涉及的类

java.lang.reflect.Proxy

/**
 * 创建代理实例
 *
 * @param loader            目标对象类加载器
 * @param interfaces        目标对象实现的接口类
 * @param invocationHandler 事件处理器
 * @return 返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序
 */
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler);

java.lang.reflect.InvocationHandler

/**
 * 代理实例上处理方法调用并返回结果
 *
 * @param proxy  代理对象
 * @param method 方法
 * @param args   方法参数
 * @return Object
 * @throws Throwable 异常
 */
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

动态代理JDK版代码示例

AccountDao接口类

package com.github.xuchengen.proxy.statics;

/**
 * 账户表数据访问层接口
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2020/4/1 2:03 下午
 */
public interface AccountDao {

    /**
     * 保存
     */
    void save();

}

AccountDaoImpl接口实现类

package com.github.xuchengen.proxy.statics;

/**
 * 账户表数据访问层接口实现
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2020/4/1 2:04 下午
 */
public class AccountDaoImpl implements AccountDao {

    @Override
    public void save() {
        System.out.println("保存账户数据");
    }

}

ProxyFactory代理工厂类

package com.github.xuchengen.proxy.statics;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

/**
 * 代理工厂类
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2020/4/1 3:33 下午
 */
public class ProxyFactory {

    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    /**
     * 为目标对象生成代理对象
     *
     * @param invocationHandler 事件处理器
     * @return Object
     */
    public Object getProxyInstance(InvocationHandler invocationHandler) {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                invocationHandler);
    }
}

JDKDynamicProxyTest测试类

package com.github.xuchengen.proxy.statics;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * JDK动态代理测试
 * 作者:徐承恩
 * 邮箱:xuchengen@gmail.com
 * 日期:2020/4/1 3:39 下午
 */
public class JDKDynamicProxyTest {

    public static void main(String[] args) {
        AccountDao accountDao = new AccountDaoImpl();

        ProxyFactory proxyFactory = new ProxyFactory(accountDao);

        AccountDao proxyInstance = (AccountDao) proxyFactory.getProxyInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                try {
                    System.out.println("开启事务");

                    method.invoke(accountDao, args);

                    System.out.println("提交事务");
                } catch (Exception e) {
                    System.out.println("事务回滚");
                }

                return null;
            }
        });

        proxyInstance.save();
    }

}

控制台输出

开启事务
保存账户数据
提交事务