什么是动态代理?

目录

一、为什么需要代理?

二、代理长什么样?

三、Java通过什么来保证代理的样子?

四、动态代理实现案例

五、动态代理在SpringBoot中的应用

导入依赖

数据库表设计

OperateLogEntity实体类

OperateLog枚举

RecordLog注解

上下文相关类

OperateLogAspect切面类

OperateLogMapper


一、为什么需要代理?

代理可以无侵入式地给对象增强其他的功能

例如以下方法

java 复制代码
public static void playGame() {
    System.out.println("玩游戏");
}

现在要对这个方法进行增强,在玩游戏之前要先吃饭,玩完游戏后要睡觉。

下面这种修改方法就是侵入式修改,对原始的方法进行修改。缺点是会使代码变得繁琐,扩展性变差。原本playGame()方法就是用来玩游戏的,吃饭和睡觉不属于玩游戏的范畴。

java 复制代码
public static void playGame() {
    System.out.println("吃饭");
    System.out.println("玩游戏");
    System.out.println("睡觉");
}

为什么需要代理?

代理就是调用playGame()方法,并且在调用之前先吃饭,玩完游戏后再睡觉。

类似于下面这种修改方式

java 复制代码
public class Test {
    public static void main(String[] args) {
        action();
    }

    public static void action() {
        System.out.println("吃饭");
        playGame();
        System.out.println("睡觉");
    }

    public static void playGame() {
        System.out.println("玩游戏");
    }
}

我们并没有直接调用playGame()方法,而是通过action()方法间接调用playGame()方法。

所以代理的目的就是在调用方法时,能在调用前或者调用后做一些事,从而达到在不修改原始方法的基础上,对原始方法进行增强。

当然我们要实现代理并不是用以上的方法,上面的案例只是解释代理的作用是什么。

二、代理长什么样?

代理里面就是对象要被代理的方法

简单理解,代理就是一个对象,这个对象里面有要被代理的方法。

被代理对象里面有playGame()方法,代理对象里面也有playGame()方法,并且是增强后的playGame()方法。

也就是说我们直接从代理对象里调用方法就行了,代理就是一个对象。

那么如何获取代理对象呢?

代理对象应该长得和被代理对象差不多才行,所以我们可以根据被代理对象来创建代理对象。

三、Java通过什么来保证代理的样子?

上面说到要根据被代理对象来创建代理对象,既然两者是相似的,可以想到如果代理对象和被代理对象继承了同一个父类,两者不就相似了吗?

因为代理对象和被代理对象虽然都有playGame()方法,但是方法的实现不同,只是方法的名称是一样的。

那么代理对象和被代理对象应该实现同一个接口,接口中的方法也就是要被代理的方法。

四、动态代理实现案例

我们先定义一个OnePerson类,其中playGame()方法就是要代理的方法,所以我们再定义一个Person接口。Person接口里面写playGame()抽象方法,代理对象也要实现Person接口并且重写playGame()方法。

java 复制代码
public class OnePerson implements Person {

    private String name;
    private String gender;
    private Integer age;

    @Override
    public void playGame() {
        System.out.println(this.name + "正在玩游戏");
    }

    public OnePerson() {
    }

    public OnePerson(String name, String gender, Integer age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "OnePerson{" +
                "name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                '}';
    }
}
java 复制代码
public interface Person {
    void playGame();
}

ProxyUtils

定义生成代理对象的工具类

这里用到反射,Method调用的方法就是原始的方法。

java 复制代码
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyUtils {
    public static Person createProxy(OnePerson onePerson) {
        return (Person) Proxy.newProxyInstance(
                ProxyUtils.class.getClassLoader(),  // 类加载器
                new Class[]{Person.class},          // 接口,指定要代理的方法
                new InvocationHandler() {           // 调用并增强原始方法
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        String name = onePerson.getName();

                        if (method.getName().equals("playGame")) {
                            System.out.println(name + "在吃饭");
                        }

                        Object object = method.invoke(onePerson, args);

                        if (method.getName().equals("playGame")) {
                            System.out.println(name + "在睡觉");
                        }

                        return object;
                    }
                }
        );
    }
}

案例测试

java 复制代码
public class Test {
    public static void main(String[] args) {
        OnePerson onePerson = new OnePerson("艾伦", "男", 15);
        Person person = ProxyUtils.createProxy(onePerson);
        person.playGame();
    }
}

测试结果

可以看到我们并没有在OnePerson类中修改playGame()方法,但是成功地对playGame()方法进行了增强。

五、动态代理在SpringBoot中的应用

动态代理在SpringBoot中就是面向切面编程(AOP),可以用来记录操作日志、公共字段自动填充等实现。

下面介绍如何实现记录操作日志

导入依赖

java 复制代码
<!--AOP起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
java 复制代码
<!--fastjson-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

数据库表设计

sql 复制代码
create table if not exists tb_operate_log
(
    id              bigint auto_increment comment '主键id'
        primary key,
    class_name      varchar(128)  not null comment '类名',
    method_name     varchar(128)  not null comment '方法名',
    method_param    varchar(1024) not null comment '方法参数',
    method_return   varchar(2048) not null comment '方法返回值',
    cost_time       bigint        not null comment '方法运行耗时;单位:ms',
    create_username varchar(16)   not null comment '方法调用者用户名',
    create_time     datetime      not null comment '创建时间',
    update_time     datetime      not null comment '更新时间',
    constraint id
        unique (id)
)
    comment '操作日志表';

OperateLogEntity实体类

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
 
import java.time.LocalDateTime;
 
/**
 * @Description: 操作日志表实体类
 * @Author: 翰戈.summer
 * @Date: 2023/11/21
 * @Param:
 * @Return:
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OperateLogEntity {
    private Long id;
    private String className;
    private String methodName;
    private String methodParam;
    private String methodReturn;
    private Long costTime;
    private String createUsername;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

OperateLog枚举

java 复制代码
/**
 * @Description: 日志操作类型
 * @Author: 翰戈.summer
 * @Date: 2023/11/21
 * @Param:
 * @Return:
 */
public enum OperateLog {
 
    //记录操作
    RECORD
 
}

RecordLog注解

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * @Description: 注解,用于标识需要进行记录操作日志的方法
 * @Author: 翰戈.summer
 * @Date: 2023/11/21
 * @Param:
 * @Return:
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordLog {
 
    //日志操作类型,RECORD记录操作
    OperateLog value();
 
}

上下文相关类

ThreadLocal线程局部变量,将信息放入上下文,后面要用可以直接取出。

java 复制代码
public class BaseContext {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 
    public static void setContext(String context) {
        threadLocal.set(context);
    }
 
    public static String getContext() {
        return threadLocal.get();
    }
 
    public static void removeContext() {
        threadLocal.remove();
    }
}

OperateLogAspect切面类

java 复制代码
import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
 
import java.time.LocalDateTime;
import java.util.Arrays;
 
/**
 * @Description: 切面类,实现记录操作日志
 * @Author: 翰戈.summer
 * @Date: 2023/11/21
 * @Param:
 * @Return:
 */
@Aspect
@Component
@RequiredArgsConstructor
public class OperateLogAspect {
 
    private final OperateLogMapper operateLogMapper;
 
    /**
     * @Description: 切入点
     * @Author: 翰戈.summer
     * @Date: 2023/11/21
     * @Param:
     * @Return: void
     */
    @Pointcut("execution(* com.demo.controller.*.*(..)) && @annotation(com.demo.annotation.RecordLog)")
    public void recordLogPointcut() {
    }
 
    /**
     * @Description: 环绕通知,进行记录操作日志
     * @Author: 翰戈.summer
     * @Date: 2023/11/21
     * @Param: ProceedingJoinPoint
     * @Return: Object
     */
    @Around("recordLogPointcut()")
    public Object recordLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
 
        //获取类名
        String className = proceedingJoinPoint.getTarget().getClass().getName();
 
        //获取方法名
        String methodName = proceedingJoinPoint.getSignature().getName();
 
        //获取方法参数
        Object[] args = proceedingJoinPoint.getArgs();
        String methodParam = Arrays.toString(args);
 
        Long begin = System.currentTimeMillis();// 方法运行开始时间
        Object result = proceedingJoinPoint.proceed();// 运行方法
        Long end = System.currentTimeMillis();// 方法运行结束时间
 
        //方法耗时
        Long costTime = end - begin;
 
        //获取方法返回值
        String methodReturn = JSONObject.toJSONString(result);
 
        String username = BaseContext.getContext();// 当前用户名
        LocalDateTime now = LocalDateTime.now();// 当前时间
 
        OperateLogEntity operateLog = new OperateLogEntity(null, className, methodName,
                methodParam, methodReturn, costTime, username, now, now);
 
        operateLogMapper.insertOperateLog(operateLog);
 
        return result;
    }
}

OperateLogMapper

java 复制代码
import org.apache.ibatis.annotations.Mapper;
 
/**
 * @Description: 操作日志相关的数据库操作
 * @Author: 翰戈.summer
 * @Date: 2023/11/21
 * @Param:
 * @Return:
 */
@Mapper
public interface OperateLogMapper {
    void insertOperateLog(OperateLogEntity operateLog);
}
XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
<mapper namespace="com.demo.mapper.OperateLogMapper">
    <!--记录操作日志-->
    <insert id="insertOperateLog">
        insert into tb_operate_log (id, class_name, method_name, method_param,
                                    method_return, cost_time, create_username, create_time, update_time)
        values (null, #{className}, #{methodName}, #{methodParam},
                #{methodReturn}, #{costTime}, #{createUsername}, #{createTime}, #{updateTime});
    </insert>
</mapper>

通过给controller层的接口方法加上@RecordLog(OperateLog.RECORD)注解即可实现记录操作日志,方便以后的问题排查。

《AOP如何实现公共字段自动填充》

https://blog.csdn.net/qq_74312711/article/details/134702905?spm=1001.2014.3001.5502

相关推荐
Chen-Edward33 分钟前
有了Spring为什么还有要Spring Boot?
java·spring boot·spring
magic334165631 小时前
Springboot整合MinIO文件服务(windows版本)
windows·spring boot·后端·minio·文件对象存储
开心-开心急了1 小时前
Flask入门教程——李辉 第一、二章关键知识梳理(更新一次)
后端·python·flask
掘金码甲哥1 小时前
调试grpc的哼哈二将,你值得拥有
后端
陈小桔1 小时前
idea中重新加载所有maven项目失败,但maven compile成功
java·maven
小学鸡!1 小时前
Spring Boot实现日志链路追踪
java·spring boot·后端
xiaogg36782 小时前
阿里云k8s1.33部署yaml和dockerfile配置文件
java·linux·kubernetes
逆光的July2 小时前
Hikari连接池
java
微风粼粼2 小时前
eclipse 导入javaweb项目,以及配置教程(傻瓜式教学)
java·ide·eclipse
番茄Salad2 小时前
Spring Boot临时解决循环依赖注入问题
java·spring boot·spring cloud