Spring框架-代理模式

提示:本篇文章一些案例和概念来自黑马程序员和尚硅谷课程


前言

提示:介绍的设计模式从java到mybatis框架到spring框架的应用

基本概念:设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

黑马程序员教程对设计模式的叙述:一个问题通常有 N 种解法,其中肯定有一种解法是最优的,这个最优的解法被总结出来,称之为设计模式。

本篇文章记录学习spring框架中所涉及的23种设计模式之一的代理模式


提示:以下是本篇文章正文内容,若有错误请指出

一、代理模式概念

代理模式(Proxy Pattern)通过引入一个代理对象来控制对原对象的访问。代理对象在客户端和目标对象之间充当中介,负责将客户端的请求转发给目标对象,同时可以在转发请求前后进行额外的处理。

代理模式解决的是在直接访问某些对象时可能遇到的问题,例如对象创建成本高、需要安全控制或远程访问等。但是根据使用场景,也可以将代理模式解决其它问题。

Java中的代理按照代理类的生成时机不同又分为静态代理和动态代理,静态代理代理类在编译期就生成,而动态代理则是在Java运行时动态生成。动态代理又有JDK代理个CGLib代理两种。

代理模式分为三个角色:
抽象主题类:通过接口或抽象类声明真实主题和代理对象实现的业务方法
真实主题类:实现了抽象主题的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
代理类:提供了真实主题相同的接口,期其内部含有真是主题的引用,它可以访问、控制或则扩展真真实主题的功能。

二、Java-多态

代理模式实现需要对Java语言中的多态概念以及反射概念有所了解,因此我们要从java语言来描述框架中所用到的设计模式,就得从最基本语法开始。

多态(Polymorphism)是面向对象编程的一个重要概念,它允许同一类型的对象在不同情况下表现出不同的行为。多态性可以通过方法重载(Overloading)和方法重写(Overriding)来实现。

黑马程序员教程对多态概念概述:多态是在继承/实现情况下的一种现象,表现为:对象多态、行为多态(属性/成员变量是不谈多态的)。

(一)多态案例

代码如下(示例):

(1)定义一个接口

public interface Animal {
    void shout();
}

(2)两个实现类

public class dog implements Animal {
    @Override
    public void shout() {
        System.out.println("在吗?在干嘛?吃饭了吗?");
    }
}
public class chicken implements Animal {
    @Override
    public void shout() {
        System.out.println("你干嘛?哎呦~");
    }
}

(3)编写测试类

public class Test {
    public static void main(String[] args) {
        Animal spareWheel = new dog();//编译看左边,运行看右边
        spareWheel.shout();

        Animal chicken = new chicken();
        chicken.shout();

        Animal animal = new Animal() {
            @Override
            public void shout() {
                System.out.println("CTRL");
            }
        };
        animal.shout();
    }
}

输出结果:
在吗?在干嘛?吃饭了吗?
你干嘛?哎呦~
CTRL

从上述这个案例中我们看到,定义一个接口实现类对象,是通过该接口的某个具体实现类或者是匿名内部类方式实现,接口本身是不能 new 一个对象出来:

Animal a = new Animal();//此代码错误代码

在我们SpringBoot项目开发过程中,我们是不是经常会用到以下类似的代码:

@Autowired
private UserMapper userMapper

在Service层的Impl实现类中经常会注入xxxMapper,但是当初Spring框架学习中,我们是不能将一个接口注入到Spring 的IOC容器中的,并且我们在Mapper层使用的是 interface 这个关键字 ,定义的是一个接口,我们并没有做任何接口的实现类,这就是个关键的疑问点。

三、Java-反射

Java 反射(Reflection)是指在运行时检查或修改类、方法、字段等程序结构的能力。它允许程序在运行时获取类的信息、调用类的方法、访问或修改类的字段,甚至可以动态创建类实例(重点:动态创建类的实例)

(一)反射案例

下面我们将通过案例来介绍一下反射的基本使用

(1)新建一个JavaBean

代码如下(示例):

public class Student {

    private Long sId;

    private String sName;

    private String sSex;

    private int sAge;

    private String sClass;

    public Long getsId() {
        return sId;
    }

    public void setsId(Long sId) {
        this.sId = sId;
    }

    public String getsName() {
        return sName;
    }

    public void setsName(String sName) {
        this.sName = sName;
    }

    public String getsSex() {
        return sSex;
    }

    public void setsSex(String sSex) {
        this.sSex = sSex;
    }

    public int getsAge() {
        return sAge;
    }

    public void setsAge(int sAge) {
        this.sAge = sAge;
    }

    public String getsClass() {
        return sClass;
    }

    public void setsClass(String sClass) {
        this.sClass = sClass;
    }

    public Student() {
    }

    public Student(Long sId, String sName, String sSex, int sAge, String sClass) {
        this.sId = sId;
        this.sName = sName;
        this.sSex = sSex;
        this.sAge = sAge;
        this.sClass = sClass;
    }

    @Override
    public String toString() {
        return "Student{" +
                "sId=" + sId +
                ", sName='" + sName + '\'' +
                ", sSex='" + sSex + '\'' +
                ", sAge=" + sAge +
                ", sClass='" + sClass + '\'' +
                '}';
    }
}

(2)新建测试类

import java.lang.reflect.Constructor;

public class Test {
    public static void main(String[] args) throws Exception {
        Class<?> student = Class.forName("Student");
        System.out.println(student.getName());
        System.out.println(student.getSimpleName());

        Constructor<?>[] cons= student.getConstructors();
        for(Constructor<?> c:cons)
        {
            System.out.println(c);
        }

        Student s = (Student) student.newInstance();
        s.setsId(1L);
        s.setsName("张三");
        s.setsSex("男");
        s.setsAge(18);
        System.out.println(s.getsName());
        System.out.println(s);
    }
}
输出结果:
Student
Student
public Student(java.lang.Long,java.lang.String,java.lang.String,int,java.lang.String)
public Student()
张三
Student{sId=1, sName='张三', sSex='男', sAge=18, sClass='null'}

从上面反射可以看出,我们通过反射获取了一个类对象,并且能通过这个类对象的getConstructors() 方法 来获取类中的全部构造器,通过 newInstance() 方法创建了一个类的实例对象(重点记住,后续文章会涉及) ,并且可以为这个这个对象属性赋值,调用类中的方法(set/get/toString)

四、Java-代理案例

(一)静态代理案例

下面将通过一个买火车票的案例进行诠释代理模式

(1)抽象主题接口

package static_proxy;

/**
 * 卖火车票的接口
 *
 * 属于抽象主题类
 */
public interface SellTickets {

    void sell();
}

(2)主题类

package static_proxy;

/**
 * 火车站类
 * 具体主题类
 */
public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}

(3)代理类

package static_proxy;

/**
 * 代售点类
 *
 * 属于 代理类
 *
 */
public class ProxyPoint implements SellTickets {

    // 声明火车站类对象
    private TrainStation trainStation = new TrainStation();
    @Override
    public void sell() {
        System.out.println("代售点收取服务费");
        trainStation.sell();
    }
}

(4)客户端测试类

package static_proxy;

public class Client {
    public static void main(String[] args) {
        // 创建对象 代售点类对象
        ProxyPoint proxyPoint = new ProxyPoint();

        //调用方法买票
        proxyPoint.sell();
    }
}

从上述代码中可以看出,测试类直接访问的是 ProxyPoint 类对象,也就是说 ProxyPoint 是作为访问对象和目标对象的中介,同时也对 sell() 方法进行了增强

(二)JDK动态代理案例

JDK动态代理:Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance 方法)来获取代理对象

(1)抽象主题类

package jdk_proxy;

/**
 * 卖火车票的接口
 *
 * 属于抽象主题类
 */
public interface SellTickets {

    void sell();
}

(2)具体主题类

package jdk_proxy;

/**
 * 火车站类
 *
 * 具体主题类
 */
public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}

(3)代理对象的工厂类

package jdk_proxy;

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

/**
 * 获取代理对象的工厂类
 *
 * 代理类也实现了对应的接口
 */
public class ProxyFactory {

    // 声明目标对象
    private TrainStation trainStation = new TrainStation();
    public SellTickets getProxyObject(){
        //返回代理对象即可

        /**
         * ClassLoader loader :类加载器,用于加载代理类。可以通过目标对象来获取类加载器
         * Class<?>[] interfaces: 代理类实现的接口的字节码对象
         *InvocationHandler h : 代理对象的调用处理程序
         */
        SellTickets sellTickets = (SellTickets)Proxy.newProxyInstance(
                trainStation.getClass().getClassLoader(),
                trainStation.getClass().getInterfaces(),
                new InvocationHandler() {

                    /**
                     * Object proxy : 代理对象,和proxyObject 对象时同一个对象,在invoke 方法中基本不用
                     * Method method : 对接口中的方法进行封装的 method 对象
                     * Object[] args : 调用方法时传递的实际参数
                     * @return 返回值,就是方法的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //System.out.println("invoke 方法执行了");

                        // 方法增强
                        System.out.println("代售点收取一定的服务费用(jdk 动态代理)");
                        //执行目标对象的方法
                        Object result = method.invoke(trainStation, args);
                        return result;
                    }
                }
        );
        return sellTickets;
    }
}

(4)动态代理测试类

package jdk_proxy;

public class Client {
    public static void main(String[] args)
    {
        // 获取代理对象
        // 1、创建代理工厂对象
        ProxyFactory proxyFactory = new ProxyFactory();

        // 2、使用 proxyFactory 对象方法获取代理对象
        SellTickets proxyObject = proxyFactory.getProxyObject();

        // 3、调用代理对象的方法
        proxyObject.sell();

    }
}
输出结果:
代售点收取一定的服务费用(jdk 动态代理)
火车站卖票

五、MyBatis-动态代理

MyBatis框架式我们日常开发中最常用的ORM持久层框架之一。接下来我们将使用MyBatis案例来叙述,MyBatis是如何使用动态代理来为我们创建代理对象并执行mapper接口中的方法

(一)Maven项目引入依赖

<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>8.0.33</version>
 </dependency>

 <dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis</artifactId>
   <version>3.5.14</version>
 </dependency>
 
 <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.11</version>
   <scope>test</scope>
 </dependency>

 <dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.17</version>
 </dependency>

(二)创建MyBatis配置文件

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

    <!---
    mybatis的核心配置文件中的标签必须要按照指定的顺序来配置
    The content of element type "configuration" must match
    "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,
    objectWrapperFactory?,reflectorFactory?,plugins?,environments?,
    databaseIdProvider?,mappers?)".
    ->


    <!- properties 配置外部属性文件 此后就可以在当前文件中使用${key} 方式访问value -->
    <properties resource="jdbc.properties"/>

    <!--
    typeAliases 配置别名,type属性指定需要起别名的类,alias 配置某个类型的别名
    若不设置alias属性,则默认使用类名作为别名
    typeAliasesPackage 配置包名,将包下的所有类自动注册为别名
    -->
    <typeAliases>
<!--        <typeAlias type="org.personal.example.entity.User" alias="user"/>-->
<!--        <typeAlias type="org.personal.example.entity.User"/>-->
        <package name="org.personal.example.entity"/>
    </typeAliases>


    <!-- environments 配置连接数据库的环境, default属性中指定使用环境的id-->
    <environments default="development">
        <environment id="development">
            <!--
            transactionManager 事务管理器,type 使用JDBC方式管理事务
            type="JDBC/MANAGED"
            JDBC方式:表示使用JDBC中原生的事务管理方式
            MANAGED方式:被管理,例如使用Spring容器来管理事务
            -->
            <transactionManager type="JDBC"/>
            <!--
            dataSource 设置数据源,属性type 设置数据源类型
            type="UNPOOLED/POOLED/JNDI"
            UNPOOLED方式:表示不使用数据库连接池
            POOLED方式:表示使用数据库连接池
            JNDI方式:表示使用上下文来获取数据源
            -->
            <dataSource type="POOLED">
<!--                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>-->
<!--                <property name="url" value="jdbc:mysql://47.111.178.5:3306/ssm_demo?serviceTimezone=UTC"/>-->
<!--                <property name="username" value="用户名"/>-->
<!--                <property name="password" value="密码"/>-->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>

        <!--测试环境-->
        <environment id="test">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/ssm_demo?serviceTimezone=UTC"/>
                <property name="username" value="用户名"/>
                <property name="password" value="密码"/>
            </dataSource>
        </environment>

    </environments>

    <!--引入MyBatis的映射文件-->
    <mappers>
<!--        <mapper resource="mappers/UserMapper.xml"/>-->
        <!--
        以包的形式引入配置文件,但是必须满足两个条件:
        1.mapper接口和映射文件所在的包必须一致
        2.mapper接口的名字和映射文件的名字必须一致
        -->
        <package name="org.personal.example.mapper"/>
    </mappers>

</configuration>

(三)实体类

package org.personal.example.entity;

public class User {
    private Long id;

    private String username;

    private String password;

    private String gender;

    private Integer age;

    private String email;

    public User() {
    }

    public User(String username, String password, String gender, Integer age, String email) {
        this.username = username;
        this.password = password;
        this.gender = gender;
        this.age = age;
        this.email = email;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    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;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", gender='" + gender + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                '}';
    }
}

(四)mapper接口

package org.personal.example.mapper;

import org.personal.example.entity.User;

import java.util.List;
public interface UserMapper {

    int insertUser();

    List<User> selectUserList();

    int updateUser();

    int deleteUser();

    User selectUserById();
}

(五)mapper.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="org.personal.example.mapper.UserMapper">

    <!--
       mapper接口和映射文件要保持两个一致:
       1.mapper接口的全类名和映射文件的namespace一致
       2.mapper接口方法名和映射文件中的id一致
     -->
    <insert id="insertUser">
        insert into t_user values(null,'李四','123456','1',20,'156@qq.com')
    </insert>


    <!--
       resultType:指定查询结果集的类型,即查询的数据要转换为的java类型
       resultMap:指定查询结果集的封装类型,处理一对一,或一对多的映射关系
     -->
    <select id="selectUserList" resultType="User">
        select * from t_user
    </select>


    <update id="updateUser">
        update t_user set username = 'admin123' where id = 1
    </update>

    <delete id="deleteUser">
        delete from t_user where id = 3
    </delete>

    <select id="selectUserById" resultType="org.personal.example.entity.User"></select>
</mapper>


以上代码只是方便测试类定义的连接数据库的配置文件,实体类以及接口,MyBatis代理模式看下面文章

(五)测试类

 @Test
 public void testSelectUserList() throws IOException {
     //获取核心配置文件的输入流
     InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
     //获取SqlSessionFactoryBuilder对象,工厂模式
     SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
     //获取SqlSessionFactory对象
     SqlSessionFactory sessionFactory = sqlSessionFactoryBuilder.build(is);
     //获取SqlSession对象,不会自动提交事务的,是mybatis操作数据库的对象
     SqlSession sqlSession = sessionFactory.openSession();

     //获取UserMapper的代理实现类对象
     UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
     //调用mapper接口中的方法,实现查询用户信息的功能
     List<User> users = userMapper.selectUserList();

     System.out.println("结果"+users);
     sqlSession.close();
 }
 输出结果:
 结果[User{id=1, username='admin123', password='123456', gender='1', age=18, email='456@qq.com'}, User{id=2, username='李四', password='123456', gender='1', age=20, email='156@qq.com'}, User{id=4, username='李四四', password='234567', gender='1', age=20, email='186@qq.com'}, User{id=6, username='张三', password='123456', gender='1', age=23, email='193@qq.com'}, User{id=7, username='王五', password='123', gender='0', age=24, email='null'}, User{id=12, username='赵六', password='123456', gender='1', age=23, email='193@qq.com'}]

从以上测试类中,我们最主要关注如下这两行代码即可

//获取UserMapper的代理实现类对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//调用mapper接口中的方法,实现查询用户信息的功能
 List<User> users = userMapper.selectUserList();

我们在上述关于java多态中说过,接口是不能创建一个实例对象的,只有接口的一个具体实现类才能为我们创建对象,而我们这个案例就是通过 sqlSession对象中getMapper()方法,来我们创一个接口的实例化对象。

我们传入是某一个类型的Class对象 (Class type),然后它就会为我们返回当前类型的一个实例化对象,既然是一个具体的实现类,那么它一定为我们重写了接口中的所有方法,那通过getMapper()来获取的实例化对象又是如何实现mapper接口中的方法呢?
答案:通过当前接口的全类名(传递的参数 UserMapper.class),来找到我们当前的映射文件(这就是为什么namespace = 接口全类名),然后再通过调用的方法,来找到我们当前映射的中SQL语句(这就是映射文件中的SQL id 为什么要与Mapper层方法名称一致的原因)。

接下来我们再点入getMapper() 方法中去,找到如下代码

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        } else {
            try {
                return mapperProxyFactory.newInstance(sqlSession);
            } catch (Exception var5) {
                throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
            }
        }
    }

上述代码中 return mapperProxyFactory.newInstance(sqlSession); 是不是跟我们之前说过反射案例代码有点相似,我们反射是通过加载一个类来获取一个Class<?> 对象的,然后通过类对象的 newInstance方法来创建一个对象。

这样我们就将代理模式跟java的基本语法(多态,反射)概念贯通起来,方便以后记忆和使用。

六、Spring-代理模式

Spring中的代理模式过于复杂(没学完),我之后会再更新,让这篇文章完整


总结

以上现在文章内容明显跟标题不符合,是因为我还未学明白Spring中运用的代理模式。以后会更新(未完待续)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值