• 设计模式学习笔记(十七)中介者模式及其应用场景


    中介者(Mediator)模式指定义了一个单独的中介对象,来封装一组对象之间的交互。即将这组对象之间的交互委派给中介对象,从而来避免对象之间的直接交互。比如我们各种设备之间的通信,就是通过服务器作为中介对象来进行交互:

    image-20220407094542645

    一、中介者模式介绍#

    中介者又叫做调停模式,是一种对象行为型模式,它降低了对象之间的耦合性,让对象易于被独立地调用,是迪米特法则的典型应用,下面就来看看中介者模式的结构和实现:

    1.1 中介者模式的结构#

    中介者模式主要通过引入用于协调其他对象或类之间相互调用的中介者类,为了让系统具有具有更好的灵活性和扩展性。其结构如下图所示:

    image-20220407115009351

    上面的类图中主要包含以下角色:

    • Mediator:抽象中介者,是中介者的接口/抽象类
    • ConcreteMeditor:中介者的具体实现,实现中介者接口,定义一个List来管理Colleague对象
    • Colleague:抽象同事类,定义同事类的接口/抽象类,保存中介者对象,实现同事类的公共方法
    • ConcreteColleague1、ConcreteColleague2:具体同事类,实现抽象同事类。通过中介者间接完成具体同事类之间的通信交互

    1.2 中介者模式的实现#

    根据上面的类图,可以实现如下代码:

    1. 抽象中介者及其实现
    /**
     * @description: 中介者抽象类
     * @author: wjw
     * @date: 2022/4/7
     */
    public abstract class Mediator {
    
        /**注册同事类*/
        public abstract void register(Colleague colleague);
    
        /**处理接收逻辑*/
        public abstract void operation(Colleague colleague);
    }
    
    /**
     * @description: 具体中介者类
     * @author: wjw
     * @date: 2022/4/7
     */
    public class ConcreteMediator extends Mediator{
    
        private List<Colleague> colleagues = new ArrayList<Colleague>();
    
        @Override
        public void register(Colleague colleague) {
            if (!colleagues.contains(colleague)) {
                colleagues.add(colleague);
                colleague.setMediator(this);
            }
        }
    
        @Override
        public void operation(Colleague colleague) {
            for (Colleague coll : colleagues) {
                if (!coll.equals(colleague)) {
                    coll.receive();
                }
            }
        }
    }
    
    1. 抽象同事类及其实现
    /**
     * @description: 抽象同事类
     * @author: wjw
     * @date: 2022/4/7
     */
    public abstract class Colleague {
    
        protected Mediator mediator;
    
        public void setMediator(Mediator mediator) {
            this.mediator = mediator;
        }
    
        public abstract void receive();
    
        public abstract void send();
    }
    
    /**
     * @description: 具体同事类1
     * @author: wjw
     * @date: 2022/4/7
     */
    public class ConcreteColleague1 extends Colleague{
    
        @Override
        public void receive() {
            System.out.println("具体同事类 ConcreteColleague1 接收请求");
        }
    
        @Override
        public void send() {
            System.out.println("具体同事类 ConcreteColleague1 发送请求");
            /*中介者进行转发*/
            mediator.operation(this);
        }
    }
    
    /**
     * @description: 具体同事类2
     * @author: wjw
     * @date: 2022/4/7
     */
    public class ConcreteColleague2 extends Colleague{
    
        @Override
        public void receive() {
            System.out.println("具体同事类 ConcreteColleague2 接收到请求");
        }
    
        @Override
        public void send() {
            System.out.println("具体同事类 ConcreteColleague2 发送请求");
            mediator.operation(this);
        }
    }
    
    1. 客户端测试类
    /**
     * @description: 客户端
     * @author: wjw
     * @date: 2022/4/7
     */
    public class Client {
        public static void main(String[] args) {
            Mediator concreteMediator = new ConcreteMediator();
            Colleague concreteColleague1 = new ConcreteColleague1();
            Colleague concreteColleague2 = new ConcreteColleague2();
            concreteMediator.register(concreteColleague1);
            concreteMediator.register(concreteColleague2);
            concreteColleague1.send();
            concreteColleague2.send();
    
        }
    }
    

    测试结果为:

    具体同事类 ConcreteColleague1 发送请求
    具体同事类 ConcreteColleague2 接收到请求
    具体同事类 ConcreteColleague2 发送请求
    具体同事类 ConcreteColleague1 接收请求
    

    二、中介者模式应用场景#

    2.1 中介者模式的适用情况#

    如果遇到以下情况可以考虑使用中介者模式:

    1. 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解
    2. 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象
    3. 需要通过一个中间类来封装多个类中的行为,但又不想生成太多的子类

    2.2 中介者模式在MVC模式中的应用#

    比如说,在MVC框架中,控制器(Controller)就是模型(Model)和视图(View)之间的中介者:

    • Model(模型):代表一个存取对象的数据,有Dao、Bean等等
    • View(视图):表示所看到的东西,比如网页、JSP等用于展示模型中的数据
    • Controller(控制器):作用于模型和视图中间,控制数据流向模型对象,在数据变化时更新视图

    image-20220407153000058

    三、中介者模式实战#

    3.1 ORM框架#

    我们知道在Java与数据库交互中JDBC可以完成对多种数据库操作,举一个利用JDBC查询的例子:

    //1.加载MySQL驱动注入到DriverManager
    Class.forName("com.mysql.cj.jdbc.Driver");
    //2.提供JDBC连接的URL、用户名和密码
    String url = "jdbc:mysql://localhost:3306/test_db?";
    String username = "root";
    String password = "root";
    //3.创建数据库的连接
    Connection connection = DriverManager.getConnection(url, username, password);
    //4.创建statement实例
    Statement statement = connection.createStatement();
    //5.1执行SQL语句,得到ResultSet对象,
    String query = "select * from test";  //查询语句,也可以换成CRUD的其他语句
    ResultSet resultSet = statement.executeQuery(query);
    while(resultSet.next()){
        //5.2通过ResultSet读取数据后,将数据转换成JavaBean对象
    } 
    //6.关闭连接对象
    connection.close();
    
    

    在上面的步骤中,步骤1~4和6都可以封装重复执行,但是在第5步中,需要完成关系模型ResultSet到对象模型JavaBean的转换,而这一部分使用通用的方式封装这种复杂的转换是比较困难的,因此有ORM(Object Relational Mapping, 对象-关系映射)框架来解决对象转换关系模型的映射问题。同时也屏蔽了之前JDBC连接中的重复代码,只提供简单的API供开发人员进行使用。

    image-20220407163737476

    3.2 利用中介者模式模仿MyBatis核心功能#

    在本案例中我们通过模仿MyBatis 中核心ORM框架功能,来使用中介者模式。首先来看看ORM框架在数据库和应用交互中的位置:

    image-20220407153504379

    从图中可以看出,ORM框架位于数据库层和应用层中间,相当于两者之间的中介。在实际MyBatis 实现过程中,不仅用到了中介者模式,还有工厂模式和建造者模式。

    在ORM框架实现的核心类中,包括加载配置文件、对XML进行解析、获取数据库session、操作数据库以及返回结果等步骤。在ORM内部的结构如下图所示(来自《重学Java设计模式》):

    image-20220407165437935

    • 左上框内是对数据库的定义和处理,包括<T> T selectOne<T> List<T> selectList等等
    • 右上是对数据库配置的开启session的工厂处理类,工厂会操作DefaultSqlSession
    • 最后是核心类SqlSessionFactoryBuilder,它可以实现处理工厂、解析文件、拿session等操作

    下面就来看看具体代码

    实战代码#

    1. 创建对应数据库、JavaBean和Dao接口

    创建数据库design-mediatro,数据表userschool

    image-20220407171710282
    创建与数据库相对应的JavaBean、Dao接口

    /**
     * @description: 学校类
     * @author: wjw
     * @date: 2022/4/7
     */
    
    public class School {
    
        private Long id;
        private String name;
        private String address;
        private Date createTime;
        private Date updateTime;
        
        //get set...
    }
    /**
     * @description: 用户类
     * @author: wjw
     * @date: 2022/4/7
     */
    
    public class User {
    
        private Long id;
        private String name;
        private Integer age;
        private Date createTime;
        private Date updateTime;
         //get set...
    }
    /**
     * @description: SchoolDao接口
     * @author: wjw
     * @date: 2022/4/7
     */
    public interface ISchoolDao {
    
        User querySchoolInfoById(Long treeId);
    }
    /**
     * @description: 用户Dao接口
     * @author: wjw
     * @date: 2022/4/7
     */
    public interface IUserDao {
    
        User queryUserInfoById(Long id);
    }
    
    
    1. mapper相关配置文件

    mybatis-config-datasource

    <?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>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://127.0.0.1:3306/design_mediator?useUnicode=true"/>
                    <property name="username" value="root"/>
                    <property name="password" value="root123"/>
                </dataSource>
            </environment>
        </environments>
    
        <mappers>
            <mapper resource="mapper/User_Mapper.xml"/>
            <mapper resource="mapper/School_Mapper.xml"/>
        </mappers>
    </configuration>
    

    School_Mapper

    <?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="dao.ISchoolDao">
        <select id="querySchoolInfoById" resultType="po.School">
            SELECT id, name, address, createTime, updateTime
            FROM school
            WHERE id = #{id}
        </select>
    </mapper>
    

    User_Mapper

    <?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="dao.IUserDao">
    
        <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="po.User">
            SELECT id, name, age, createTime, updateTime
            FROM user
            where id = #{id}
        </select>
    
        <select id="queryUserList" parameterType="po.User" resultType="po.User">
            SELECT id, name, age, createTime, updateTime
            FROM user
            WHERE age = #{age}
        </select>
    
    </mapper>
    
    1. mediator中介者部分

    SqlSession ,定义对数据库操作的查询接口,包括一个结果和多个查询结果,同时包括有参和无参的方法

    /**
     * @description: 各种查询接口
     * @author: wjw
     * @date: 2022/4/7
     */
    public interface SqlSession {
    
        <T> T selectOne(String statement);
    
        <T> T selectOne(String statement, Object parameter);
    
        <T> List<T> selectList(String statement);
    
        <T> List<T> selectList(String statement, Object parameter);
    
        void close();
    }
    

    SqlSession具体实现类DefaultSqlSession,包装jdbc层

    /**
     * @description: SqlSession具体实现类
     * @author: wjw
     * @date: 2022/4/7
     */
    public class DefaultSqlSession implements SqlSession{
    
        private Connection connection;
        private Map<String, XNode> mapperElement;
    
        public DefaultSqlSession(Connection connection, Map<String, XNode> mapperElement) {
            this.connection = connection;
            this.mapperElement = mapperElement;
        }
    
        @Override
        public <T> T selectOne(String statement) {
            try {
                XNode xNode = mapperElement.get(statement);
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
                return objects.get(0);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
            ArrayList<T> list = new ArrayList<>();
            try {
                ResultSetMetaData metaData = resultSet.getMetaData();
                int columnCount = metaData.getColumnCount();
                while (resultSet.next()) {
                    T obj = (T) clazz.newInstance();
                    for (int i = 1; i <= columnCount; i++) {
                        Object value = resultSet.getObject(i);
                        String columnName = metaData.getColumnName(i);
                        String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
                        Method method;
                        if (value instanceof Timestamp) {
                            method = clazz.getMethod(setMethod, java.util.Date.class);
                        } else {
                            method = clazz.getMethod(setMethod, value.getClass());
                        }
                        method.invoke(obj, value);
                    }
                    list.add(obj);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return list;
        }
    
        @Override
        public <T> T selectOne(String statement, Object parameter) {
            XNode xNode = mapperElement.get(statement);
            Map<Integer, String> parameterMap = xNode.getParameter();
            try {
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                buildParameter(preparedStatement, parameter, parameterMap);
                ResultSet resultSet = preparedStatement.executeQuery();
                List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
                return objects.get(0);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return null;
        }
    
        private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {
            int size = parameterMap.size();
    
            if (parameter instanceof Long) {
                for (int i = 1; i <= size; i++) {
                    preparedStatement.setLong(i, Long.parseLong(parameter.toString()));
                }
                return;
            }
    
            if (parameter instanceof Integer) {
                for (int i = 1; i <= size; i++) {
                    preparedStatement.setInt(i, Integer.parseInt(parameter.toString()));
                }
                return;
            }
    
            if (parameter instanceof String) {
                for (int i = 1; i <= size; i++) {
                    preparedStatement.setString(i, parameter.toString());
                }
                return;
            }
    
            Map<String, Object> fieldMap = new HashMap<>();
    
            Field[] declaredFields = parameter.getClass().getDeclaredFields();
            for (Field field : declaredFields) {
                String name = field.getName();
                field.setAccessible(true);
                Object obj = field.get(parameter);
                field.setAccessible(false);
                fieldMap.put(name, obj);
            }
    
            for (int i = 1; i <= size; i++) {
                String parameterDefine = parameterMap.get(i);
                Object obj = fieldMap.get(parameterDefine);
    
                if (obj instanceof Short) {
                    preparedStatement.setShort(i, Short.parseShort(obj.toString()));
                    continue;
                }
                if (obj instanceof Integer) {
                    preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
                    continue;
                }
    
                if (obj instanceof Long) {
                    preparedStatement.setLong(i, Long.parseLong(obj.toString()));
                    continue;
                }
    
                if (obj instanceof String) {
                    preparedStatement.setString(i, obj.toString());
                    continue;
                }
    
                if (obj instanceof Date) {
                    preparedStatement.setDate(i, (java.sql.Date) obj);
                }
            }
        }
    
    
        @Override
        public <T> List<T> selectList(String statement) {
            XNode xNode = mapperElement.get(statement);
            try {
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                ResultSet resultSet = preparedStatement.executeQuery();
                return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public <T> List<T> selectList(String statement, Object parameter) {
            XNode xNode = mapperElement.get(statement);
            Map<Integer, String> parameterMap = xNode.getParameter();
    
            try {
                PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
                buildParameter(preparedStatement, parameter, parameterMap);
                ResultSet resultSet = preparedStatement.executeQuery();
                return resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        @Override
        public void close() {
            if (null == connection) {
                return;
            }
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    

    SqlSessionFactory接口,利用工厂模式在每次数据库操作时都会获取每一次执行的SqlSession

    /**
     * @description: SqlSession工厂类
     * @author: wjw
     * @date: 2022/4/7
     */
    public interface SqlSessionFactory {
    
        SqlSession openSession();
    }
    

    DefaultSqlSessionFactory具体实现类

    /**
     * @description: SqlSessionFactory具体实现类
     * @author: wjw
     * @date: 2022/4/7
     */
    public class DefaultSqlSessionFactory implements SqlSessionFactory{
    
        private final Configuration configuration;
    
        public DefaultSqlSessionFactory(Configuration configuration) {
            //向下传递configuration配置文件
            this.configuration = configuration;
        }
    
        @Override
        public SqlSession openSession() {
            return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
        }
    }
    

    SqlSessionFactoryBuilder具体实现,包括build(构建实例化元素)parseConfiguration(解析配置)dataSource(获取数据库配置)connection(Map<String, String> dataSource)(链接数据库)mapperElement(解析sql语句)

    /**
     * @description: 创建解析xml文件的类,初始化SqlSession工厂类
     * @author: wjw
     * @date: 2022/4/7
     */
    public class SqlSessionFactoryBuilder {
    
        /**
         * 构建实例化元素
         * @param reader 读入
         * @return SqlSession
         */
        public DefaultSqlSessionFactory build(Reader reader) {
            SAXReader saxReader = new SAXReader();
            try {
                /**保证在不联网时一样可以解析xml,否则会从互联网中获取dtd文件*/
                saxReader.setEntityResolver(new XMLMapperEntityResolver());
                Document document = saxReader.read(new InputSource(reader));
                Configuration configuration = parseConfiguration(document.getRootElement());
                return new DefaultSqlSessionFactory(configuration);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        /**
         * 解析配置
         * @param root
         * @return
         */
        private Configuration parseConfiguration(Element root) {
            Configuration configuration = new Configuration();
            configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
            configuration.setConnection(connection(configuration.dataSource));
            configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
            return configuration;
        }
    
        private Map<String, String> dataSource(List<Element> list) {
            Map<String, String> dataSource = new HashMap<>(4);
            Element element = list.get(0);
            List content = element.content();
            for (Object o : content) {
                Element e = (Element) o;
                String name = e.attributeValue("name");
                String value = e.attributeValue("value");
                dataSource.put(name, value);
            }
            return dataSource;
        }
    
        /**
         * 连接数据库
         * @param dataSource
         * @return
         */
        private Connection connection(Map<String, String> dataSource) {
            try {
                Class.forName(dataSource.get("driver"));
                return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
        //解析sql语句
        private Map<String, XNode> mapperElement(List<Element> list) {
            Map<String, XNode> map = new HashMap<>();
    
            Element element = list.get(0);
            List content = element.content();
            for (Object o : content) {
                Element e = (Element) o;
                String resource = e.attributeValue("resource");
    
                try {
                    Reader reader = Resources.getResourceAsReader(resource);
                    SAXReader saxReader = new SAXReader();
                    Document document = saxReader.read(new InputSource(reader));
                    Element root = document.getRootElement();
    
                    String namespace = root.attributeValue("namespace");
    
                    // SELECT
                    List<Element> selectNodes = root.selectNodes("select");
                    for (Element node : selectNodes) {
                        String id = node.attributeValue("id");
                        String parameterType = node.attributeValue("parameterType");
                        String resultType = node.attributeValue("resultType");
                        String sql = node.getText();
    
                        // ? 匹配
                        Map<Integer, String> parameter = new HashMap<>();
                        Pattern pattern = Pattern.compile("(#\\{(.*?)})");
                        Matcher matcher = pattern.matcher(sql);
                        for (int i = 1; matcher.find(); i++) {
                            String g1 = matcher.group(1);
                            String g2 = matcher.group(2);
                            parameter.put(i, g2);
                            sql = sql.replace(g1, "?");
                        }
    
                        XNode xNode = new XNode();
                        xNode.setNamespace(namespace);
                        xNode.setId(id);
                        xNode.setParameterType(parameterType);
                        xNode.setResultType(resultType);
                        xNode.setSql(sql);
                        xNode.setParameter(parameter);
    
                        map.put(namespace + "." + id, xNode);
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
    
            }
            return map;
        }
    }
    
    1. 测试验证
    /**
     * @description: 测试
     * @author: wjw
     * @date: 2022/4/7
     */
    public class ApiTest1 {
    
        private Logger logger = LoggerFactory.getLogger(ApiTest1.class);
    
        @Test
        public void test_querySchoolInfoById() {
            String resource = "mybatis-config-datasource.xml";
            Reader reader;
    
            try {
                reader = Resources.getResourceAsReader(resource);
                SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
                SqlSession session = sqlMapper.openSession();
    
                try {
                    School school = session.selectOne("dao.ISchoolDao.querySchoolInfoById", 1L);
                    logger.info("测试结果:{}", JSON.toJSONString(school));
                } finally {
                    session.close();
                    reader.close();
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        @Test
        public void test_queryUserInfoById() {
            String resource = "mybatis-config-datasource.xml";
            Reader reader;
            try {
                reader = Resources.getResourceAsReader(resource);
                SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
    
                SqlSession session = sqlMapper.openSession();
                try {
                    User user = session.selectOne("dao.IUserDao.queryUserInfoById", 1L);
                    logger.info("测试结果:{}", JSON.toJSONString(user));
                } finally {
                    session.close();
                    reader.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
        @Test
        public void test_queryUserList() {
            String resource = "mybatis-config-datasource.xml";
            Reader reader;
            try {
                reader = Resources.getResourceAsReader(resource);
                SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
    
                SqlSession session = sqlMapper.openSession();
                try {
                    User req = new User();
                    req.setAge(18);
                    List<User> userList = session.selectList("dao.IUserDao.queryUserList", req);
                    logger.info("测试结果:{}", JSON.toJSONString(userList));
                } finally {
                    session.close();
                    reader.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    }
    

    测试结果:

    10:54:56.778 [main] INFO  ApiTest1 - 测试结果:{"address":"北京市海淀区颐和园路5号","createTime":1571376957000,"id":1,"name":"北京大学","updateTime":1571376957000}
    ---------------------------------------------------------------------------------
    10:56:17.654 [main] INFO  ApiTest1 - 测试结果:{"age":18,"createTime":1576944000000,"id":1,"name":"⽔⽔","updateTime":1576944000000}
    ---------------------------------------------------------------------------------
    10:56:42.983 [main] INFO  ApiTest1 - 测试结果:[{"age":18,"createTime":1576944000000,"id":1,"name":"⽔⽔","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"⾖⾖","updateTime":1576944000000}]
    

    参考资料#

    《重学Java设计模式》

    《Java设计模式》

    《MyBatis技术内幕》

    http://c.biancheng.net/view/1393.html

  • 相关阅读:
    React 入门:对比 Reac t的新旧生命周期
    SVM-SMO算法
    线程的创建和两种线程实现方式的区别
    嘉立创EDA专业版--指定位置加客编
    mysql数据库主从同步
    node.js--简介、特点、控制台常用指令、http模块、fs模块
    Matlab代码格式一键美化神器
    JVM:执行引擎
    RabbitMQ(四)
    服务端渲染和客户端渲染的区别
  • 原文地址:https://www.cnblogs.com/EthanWong/p/16114898.html