Java代码审计-设计模式-访问者模式

Java设计模式-访问者模式(Visitor Pattern)

目录

  • 什么是访问者模式
  • 访问者模式的实现
  • JavaSE中访问者模式的使用
  • Struts2访问者模式的应用

数据对象相对稳定,但对数据的分析使用多变的情况下,可以采用访问者模式

一、什么是访问者模式

1.1 什么是访问者模式?

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素新的操作

对数据结构的元素操作,很容易想到前面讲的**组合模式**,在组合模式中,模拟了部门的组织结构,但如果现在想添加统计所有人员工资的功能、以报表方式展示员工信息,应该怎么做?

在Leaf和Branch增加方法吗?很显然不符合开闭原则

image-20230324105528101

访问者模式就是为了解决这样的场景,当前例子中,部门的数据是非常稳定的,一个树状结构,每个叶子节点都有固定的属性,使用一个List维护。现在我们对Map内的数据进行数据的分析,但分析方法是多变的,OK,那现在抽象下:

  1. 稳定的内容:数据源
  2. 变化的内容:分析方法

所以访问者模式是将稳定的部分进行封装,变化的部分进行解耦,分析方法(Visitor)中持有数据源对象,那便可以获取数据源的所有状态和数据,便可以完成分析工作。看下图

image-20230324094458160

IVisitor是抽象的访问者类,数据源对象Component,数据源对象通过accept(Visitor)方法运行访问者访问自己的内部数据。其实这个地方也违反了开闭原则,但对Component侵入足够小。简化下UML类图,如下

image-20230324110458869

看下类图的主要对象:

  • Visitor——抽象访问者

    抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。

    ● ConcreteVisitor——具体访问者

    它影响访问者访问到一个类后该怎么干,要做什么事情。例如ReportVisitor、TotalVisitor

    ● Element——抽象元素

    接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。

    ● ConcreteElement——具体元素

    实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式了。

    ● ObjectStruture——结构对象

    元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目中,一般很少抽象出这个角色。例如部门组织架构的List

二、访问者模式的实现

实现下面的代码,分析公司组织架构中所有员工的工资、以及人员信息

image-20230324094458160

代码如下,有点长,慢慢读

package org.visitor.version1;

import java.util.ArrayList;
import java.util.List;
// 访问者接口
interface Visitor{
    public void visit(Leaf leaf);
    public void visit(Branch branch);
}
// 报表接口
interface IReportVisitor extends Visitor {
    public void report();
}
// 统计工资的接口
interface ITotalVisitor extends Visitor {
    public void totalSalary();
}

class TotalVisitor implements ITotalVisitor{
    private int salary = 0;
    private List<Component> components = new ArrayList<>();

    // 访问员工
    @Override
    public void visit(Leaf leaf) {
        System.out.println("正在计算【"+ leaf.getName() +"】工资");
        components.add(leaf);
    }
    // 访问管理者,管理者也有工资
    @Override
    public void visit(Branch branch) {
        System.out.println("正在计算【"+ branch.getName() +"】工资");
        components.add(branch);
    }
    // 计算工资
    @Override
    public void totalSalary() {
        for (Component component:components){
            this.salary = this.salary + component.getSalary();
        }
        System.out.println("所有员工工资为:" + this.salary);
    }
}

class ReportVisitor implements IReportVisitor{
    String report = "";
    String leafView = "----";
    String branchView = "--";
    String RN = "\n";
    @Override
    public void visit(Leaf leaf) {
        report = report + leafView + leaf.getName() + RN;
    }

    @Override
    public void visit(Branch branch) {
        report = report + branchView + branch.getName() + RN;
    }

    @Override
    public void report() {
        System.out.println(report);
    }
}

/**
 * 树枝与树叶的抽象类
 */
abstract class Component{
    private String name;
    private String position;
    private int salary;

    public Component(String name, String position, int salary){
        this.name = name;
        this.position = position;
        this.salary = salary;
    }

    public String getInfo(){
        String res = "姓名:" + this.name + "," + "职位:" + this.position + "," + "薪水:" + this.salary;
        System.out.println(res);
        return res;
    }

    public abstract void accept(Visitor visitor);

    public String getName() {
        return name;
    }

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

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }
}

class Leaf extends Component{

    public Leaf(String name, String position, int salary) {
        super(name, position, salary);
    }

    /**
     * 允许观察者进入 --新增方法
     * @param visitor
     */
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

}

/**
 * 树枝类,也就是节点类,内部有添加树叶,获取树叶信息的方法
 */
class Branch extends Component{
    List<Component> staffList = new ArrayList<>();

    public Branch(String name, String position, int salary) {
        super(name, position, salary);
    }

    public void addStaff(Component component){
        this.staffList.add(component);
    }

    /**
     * 允许观察者进入 --新增方法
     * @param visitor
     */
    @Override
    public void accept(Visitor visitor) {
        // 将自己传给观察者
        visitor.visit(this);
    }

    /**
     * 获取管理者下的员工 --新增方法
     * @return
     */
    public List<Component> getStaffList() {
        return staffList;
    }

}
public class Test {
    public static void main(String[] args) {
        // CEO要知道自己所有员工的工资
        ITotalVisitor totalVisitor = new TotalVisitor();
        Branch ceo = init();
        report(ceo,totalVisitor);
        totalVisitor.totalSalary();
        // CEO要知道自己有哪些员工
        System.out.println("CEO有以下员工");
        IReportVisitor reportVisitor = new ReportVisitor();
        report(ceo,reportVisitor);
        reportVisitor.report();
    }
    // 遍历所有员工,使用访问者访问
    public static void report(Branch branch,Visitor visitor){
        for (Component component:branch.getStaffList()){
            component.accept(visitor);
            if (component instanceof Branch){
                report((Branch) component,visitor);
            }
        }
    }
    // 准备数据,一般来自数据库
    public static Branch init(){
        Leaf zhangsan = new Leaf("张三","员工",6000);
        Leaf lisi = new Leaf("李四","员工",5000);
        Leaf wangwu = new Leaf("王五","员工",4000);
        Branch dev = new Branch("张明","研发部领导",20000);
        dev.addStaff(zhangsan);
        dev.addStaff(lisi);
        dev.addStaff(wangwu);

        Leaf liuning = new Leaf("刘宁","员工",6000);
        Branch sell = new Branch("王伟","销售部领导",20000);
        sell.addStaff(liuning);

        Branch ceo = new Branch("陈真","CEO",60000);
        ceo.addStaff(dev);
        ceo.addStaff(sell);
        return ceo;
    }
}
// 运行结果
正在计算【张明】工资
正在计算【张三】工资
正在计算【李四】工资
正在计算【王五】工资
正在计算【王伟】工资
正在计算【刘宁】工资
所有员工工资为:61000
CEO有以下员工
--张明
----张三
----李四
----王五
--王伟
----刘宁

重点看下访问者接口

// 访问者接口
interface Visitor{
    public void visit(Leaf leaf);
    public void visit(Branch branch);
}

发现什么了吗?这是什么技术?是重载。再看下Leaf、Branch的accept方法

public void accept(Visitor visitor) {
    visitor.visit(this);
}

visitor对象有可能是ReportVisitor、TotalVisitor,所以这是多态

visitor传递的this对象,是动态的,在Leaf类中传的是Leaf对象,在Branch类中传的是Branch对象,accept会自动匹配visit方法,这是重载方法调用的特点。

如果忘记了重载和多态的概念,需要去回顾下,这里简单一提:

  • 多态:同一个行为具有多个不同表现形式或形态的能力,指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。

  • 重载:指一个类中可以有多个方法具有相同的名字,但这些方法的参数不同(参数的类型和个数不同)

在访问者模式中使用了重载与多态,这就牵扯出另两个概念:Java的单分派、双分派。

  • 单分派:调用哪个对象(多态)的方法,在运行期确定;调用对象的哪个方法,编译期确定。单分派根据方法参数的静态类型发生,方法重载就是单分派。
  • 双分派:调用哪个对象(多态)的方法,在运行期确定;调用对象的哪个方法(方法重载),在运行期确定。发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。也就是说即使是方法重载也可以确定要调用的方法,而不是根据参数的静态类型来调用。如果要实现双分派的机制那么就要用到访问者模式。
  • 总结一句话:在抽象类或者接口的实现类中,方法参数为抽象类或接口,肯定是双分派。

在回顾下代码,看看accept方法是不是使用到了双分派?this是运行时绑定的,这就给代码带来了极大的扩展性,想想如果想在代码基础上加一个报表显示男女性别分布,是不是只需要添加一个Visitor的实现类,修改下客户端调用代码就可以了?其它的代码完全不需要,这就是双分派的优势,也就是访问者模式的优势。

接下来总结下访问者模式的优缺点:

  • 优点:
  1. 扩展性好:在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  2. 复用性好,灵活度高:通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
  3. 分离无关行为,符合单一职责原则:通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
  • 缺点:
  1. 对象结构变化很困难:在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”,当然这也是无法避免的。
  2. 违反了依赖倒置原则:访问者模式依赖了具体类,而没有依赖抽象类。

三、JavaSE中访问者模式的使用

3.1 java.nio.file.FileVisitor

java.nio.file.FileVisitor的walkFileTree使用了访问者模式,看下图代码,实现了文件目录的遍历,非常像前面我们部门的案例。一个遍历的是目录,一个是部门人员。

image-20230324140347510

SimpleFileVisitor.visitFile方法如下,父类接口为FileVisitor,参数file为泛型,attrs为接口,符合双分派的定义。同时也是访问者模式的应用,只不过Visitor没做什么事情,只是判断file是否为空。

image-20230324140722034

分析下主要对象:

  • Visitor抽象访问者:FileVisitor

    ● ConcreteVisitor具体访问者:SampleFileVisitor

    ● Element抽象元素:File、Path

    ● ConcreteElement:File、Path的子类

    ● ObjectStruture结构对象:FileTreeWalker

ASM技术

ASM 是一款读写Java字节码的工具,可以达到跳过源码编写,编译,直接以字节码的形式创建类,修改已经存在类(或者jar中的class)的属性,方法等。 通常用来开发一些Java开发的辅助框架,其做法是在你编写的Java代码中注入一些特定代码(俗称字节码插装)达到特定目的。

先说下主要对象:

  • Visitor抽象访问者:ClassVisitor
  • ConcreteVisitor具体访问者:ClassWriter,负责将修改后的字节码输出为字节数组
  • Element抽象元素:无
  • ConcreteElement:ClassReader,它将字节数组或 class 文件读入内存中,并以树的数据结构表示
  • ObjectStruture结构对象:Java的class文件内容

ClassWrite内容如下图,构造方法中接收ClassReader,copyPool相当于前面例子的accept方法。

image-20230324145740446

对于 ASM 这种场景而言,字节码规范是非常严格且稳定的,如果随便更改可能出问题。但我们又需要对字节码进行动态修改,从而达到某些目的。在这种情况下,ASM 的设计者采用了访问者模式将变化的部分隔离开来,将不变的部分固定下来,从而达到了灵活扩展的目的。

ASM技术对于Java的自动化审计非常重要。有两种自动化审计思路,一是AST,二是ASM。前者基于java源代码,后者基于字节码。

四、Struts2访问者模式的应用

暂未发现,如果读者有发现,请联系我添加,谢谢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MarginSelf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值