手敲MyBatis(十四章)-解析含标签的动态SQL语句

1.前言

这一章主要的就是要解析动态标签里的Sql语句,然后进行条件语句的拼接,动态标签实现了trim和if标签,所以Sql节点就要加上TrimSqlNode和ifSqlNode,我们最终要获取Sql源,动态Sql语句需要一些处理,所以需要添加DynamicSqlSource来处理动态Sql语句的调用和一些业务逻辑处理。

本章节主要就是要处理如下图片的解析Sql内容,把如下图片的Sql内容更改为能够可执行的Sql语句,这个是目标。

需要注意的是,sql语句不加条件时我们叫静态SQL,当动态语句标签包含的条件语句时,除了trim和if放入到对应的节点里,if里的Sql也要放入静态节点里,最后把这些个节点集合放入到混合节点里,等使用时直接遍历混合节点数据即可,最终调度到不同的节点取出Sql进行拼接即可。

2.xml类图

动态标签我们看作是一个节点,那么我们解析的xml的Sql语句里边就有很多的不同的节点,静态的Sql节点,if节点,trim节点,那么需要把不同节点的信息存储到不同的节点里,这块的功能需要在xml脚本构建类里实现,从这里开始去一点一点构建不同的节点。

节点构建完毕需要获取不同的文本进行SQL语句拼接,拼接完毕放入到DynamicContext的sqlBuilder里, DynamicSqlSource就可以根据DynamicContext直接获取到SQL了。

3.代码

因为我们要处理的是Sql里的内容,所以在代码设计里就是要处理Xml的脚本构建,也就是XMLScriptBuilder类,我们在XMLScriptBuilder类里添加了NodeHandler接口,定义了handleNode方法。

3.1 节点处理器(NodeHandler)

包名路径:package cn.bugstack.mybatis.scripting.xmltags;

然后定义两个实现类,TrimHandler和IfHandler类,TrimHandler主要解析trim标签,IfHandler主要处理if标签内容。

最后再初始化nodeHandler把TrimHandler和IfHandler放入到Map里,留着后面从Map取出使用。

public class XMLScriptBuilder extends BaseBuilder {
   // 过滤其他

    public XMLScriptBuilder(Configuration configuration, Element element, Class<?> parameterType) {
        super(configuration);
        this.element = element;
        this.parameterType = parameterType;
        initNodeHandlerMap();
    }
    
   // step-15新增
    private void initNodeHandlerMap() {
        // 9种,实现其中2种 trim/where/set/foreach/if/choose/when/otherwise/bind
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("if", new IfHandler());
    }
   

   // 节点处理器
   private interface NodeHandler {
       void handleNode(Element nodeToHandle, List<SqlNode> targetContents);
   }


    /**
     * <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">...</trim> 解析 trim 标签信息,把字段 prefix、
     * prefixOverrides、suffixOverrides 都依次获取出来,使用 TrimSqlNode 构建后存放到 List<SqlNode>   中。
     * 得到trim的属性
     */
    // step-15新增
    private class TrimHandler implements NodeHandler {
        @Override
        public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
            List<SqlNode> contents = parseDynamicTags(nodeToHandle);
            MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
            String prefix = nodeToHandle.attributeValue("prefix");
            String prefixOverrides = nodeToHandle.attributeValue("prefixOverrides");
            String suffix = nodeToHandle.attributeValue("suffix");
            String suffixOverrides = nodeToHandle.attributeValue("suffixOverrides");
            TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
            targetContents.add(trim);
        }
    }

    /**
     * <if test="null != activityId">...</if> 解析if语句标签,与解析 trim 标签类似,
     * 获取标签配置 test 语句表达式,使用 IfSqlNode 进行构建,构建后存放到 List<SqlNode>  中。
     * 得到if标签的属性
     */
    // step-15新增
    private class IfHandler implements NodeHandler {
        @Override
        public void handleNode(Element nodeToHandle, List<SqlNode> targetContents) {
            List<SqlNode> contents = parseDynamicTags(nodeToHandle);
            MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
            // 得到test的判断语句
            String test = nodeToHandle.attributeValue("test");
            IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
            targetContents.add(ifSqlNode);
        }
    }


}

3.2 SqlNode

我们原来的SqlNode有静态的和混合的实现类,这次我们还要加三个实现类,TextSqlNode和IfSqlNode以及TrimSqlNode。

3.2.1 TextSqlNode

TextSqlNode:此节点处理是否是动态Sql的判断,还有一个是${}的参数替换。

/**
 * @Author df
 * @Description: 文本SQL节点(CDATA | TEXT)
 * @Date 2023/12/22 14:09
 */
// step-15新增
public class TextSqlNode implements SqlNode {

    private String text;
    private Pattern injectionFilter;

    public TextSqlNode(String text) {
        this(text, null);
    }

    public TextSqlNode(String text, Pattern injectionFilter) {
        this.text = text;
        this.injectionFilter = injectionFilter;
    }

    /**
     * 判断是否是动态sql
     */
    public boolean isDynamic() {
        DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
        GenericTokenParser parser = createParser(checker);
        parser.parse(text);
        return checker.isDynamic();
    }

    @Override
    public boolean apply(DynamicContext context) {
        GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
        context.appendSql(parser.parse(text));
        return true;
    }

    // 处理${}替换值的情况
    private GenericTokenParser createParser(TokenHandler handler) {
        return new GenericTokenParser("${", "}", handler);
    }


    private static class BindingTokenParser implements TokenHandler {

        private DynamicContext context;
        private Pattern injectionFilter;

        public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
            this.context = context;
            this.injectionFilter = injectionFilter;
        }

        @Override
        public String handleToken(String content) {
            Object parameter = context.getBindings();
            if (parameter == null) {
                context.getBindings().put("value", null);
            } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
                context.getBindings().put("value", parameter);
            }
            // 从缓存里取得值
            Object value = OgnlCache.getValue(content, context.getBindings());
            String srtValue = (value == null ? "" : String.valueOf(value));
            checkInjection(srtValue);
            return srtValue;
        }

        // 检查是否匹配正则表达式
        private void checkInjection(String value) {
            if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
                throw new RuntimeException("Invalid input. Please conform to regex" + injectionFilter.pattern());
            }
        }

    }


    /**
     * 动态SQL检查器
     */
    private static class DynamicCheckerTokenParser implements TokenHandler {

        private boolean isDynamic;

        public DynamicCheckerTokenParser() {
            // Prevent Synthetic Access
        }

        public boolean isDynamic() {
            return isDynamic;
        }

        @Override
        public String handleToken(String content) {
            // 设置 isDynamic 为 true,即调用了这个类就必定是动态 SQL
            this.isDynamic = true;
            return null;
        }
    }
}

3.2.2 IfSqlNode

IfSqlNode:它专门就是处理test的内容判断的,如果满足判断则进入拼接Sql语句

/**
 * @Author df
 * @Description: IF SQL 节点
 * @Date 2023/12/22 15:17
 */
// step-15新增
public class IfSqlNode implements SqlNode {

    private ExpressionEvaluator evaluator;
    private String test;
    private SqlNode contents;

    public IfSqlNode(SqlNode contents, String test) {
        this.test = test;
        this.contents = contents;
        this.evaluator = new ExpressionEvaluator();
    }

    /**
     * <if test="null != activityId">
     * activity_id = #{activityId}
     * </if>
     */
    @Override
    public boolean apply(DynamicContext context) {
        // 如果满足条件,则apply,并返回true
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
            // 拼接if标签里的Sql语句
            contents.apply(context);
            return true;
        }
        return false;
    }
}

ExpressionEvaluator类:处理if的test内容判断的

public class ExpressionEvaluator {
    // 表达式求布尔值,比如 username == 'xiaofuge'
    public boolean evaluateBoolean(String expression, Object parameterObject) {
        // 非常简单,就是调用ognl
        Object value = OgnlCache.getValue(expression, parameterObject);
        if (value instanceof Boolean) {
            // 如果是Boolean
            return (Boolean) value;
        }
        if (value instanceof Number) {
            // 如果是Number,判断不为0
            return !new BigDecimal(String.valueOf(value)).equals(BigDecimal.ZERO);
        }
        // 否则判断不为null
        return value != null;
    }
}

 OgnlCache:OGNL缓存,处理if的表达式判断,并把表达式存储起来。

/**
 * @Author df
 * @Description: OGNL缓存:http://code.google.com/p/mybatis/issues/detail?id=342
 * OGNL 是 Object-Graph Navigation Language 的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL)
 * 通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。
 * 它使用相同的表达式去存取对象的属性。
 * @Date 2023/12/22 14:45
 */
// step-15新增
public class OgnlCache {

    private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();

    private OgnlCache() {
        // Prevent Instantiation of Static Class
    }

    public static Object getValue(String expression, Object root) {
        try {
            Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
            return Ognl.getValue(parseExpression(expression), context, root);
        } catch (OgnlException e) {
            throw new RuntimeException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
        }
    }

    private static Object parseExpression(String expression) throws OgnlException {
        Object node = expressionCache.get(expression);
        if (node == null) {
            node = Ognl.parseExpression(expression);
            expressionCache.put(expression, node);
        }
        return node;
    }
}

OgnlClassResolver: 自己实现个OgnlClassResolver类加载器

public class OgnlClassResolver implements ClassResolver {

    private Map<String, Class<?>> classes = new HashMap<String, Class<?>>(101);

    @Override
    public Class classForName(String className, Map map) throws ClassNotFoundException {
        Class<?> result = null;
        if ((result = classes.get(className)) == null) {
            try {
                result = Resources.classForName(className);
            } catch (ClassNotFoundException e1) {
                if (className.indexOf('.') == -1) {
                    result = Resources.classForName("java.lang." + className);
                    classes.put("java.lang." + className, result);
                }
            }
            classes.put(className, result);
        }
        return result;
    }
}


3.2.3 TrimSqlNode

TrimSqlNode:trim最主要的就是调用处理if的sql节点处理或拼接,然后得到trim属性,把trim属性前缀或后缀进行拼接Sql处理。

这里的trim的Sql拼接处理都在其内部类实现,是FilteredDynamicContext类。

/**
 * @Author df
 * @Description: trim Sql Node 节点解析
 * @Date 2023/12/22 14:57
 */
// step-15新增
public class TrimSqlNode implements SqlNode {
    private SqlNode contents;
    private String prefix;
    private String suffix;
    private List<String> prefixesToOverride;
    private List<String> suffixesToOverride;
    private Configuration configuration;

    public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
        this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
    }


    protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
        this.contents = contents;
        this.prefix = prefix;
        this.prefixesToOverride = prefixesToOverride;
        this.suffix = suffix;
        this.suffixesToOverride = suffixesToOverride;
        this.configuration = configuration;
    }


    @Override
    public boolean apply(DynamicContext context) {
        FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
        // 得到trim里的内容,进行处理,最后拼接语句
        // 例如:trim->if->条件语句
        boolean result = contents.apply(filteredDynamicContext);
        // 根据trim的属性添加前后缀
        filteredDynamicContext.applyAll();
        return result;
    }

    /**
     * <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">
     *
     * </trim>
     * 将prefixOverrides以list形式展示
     */
    private static List<String> parseOverrides(String overrides) {
        if (overrides != null) {
            final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
            final List<String> list = new ArrayList<>(parser.countTokens());
            while (parser.hasMoreTokens()) {
                list.add(parser.nextToken().toLowerCase(Locale.ENGLISH));
            }
            return list;
        }
        return Collections.emptyList();
    }

    private class FilteredDynamicContext extends DynamicContext {

        private DynamicContext delegate;
        private boolean prefixApplied;
        private boolean suffixApplied;
        private StringBuilder sqlBuffer;

        public FilteredDynamicContext(DynamicContext delegate) {
            super(configuration, null);
            this.delegate = delegate;
            this.prefixApplied = false;
            this.suffixApplied = false;
            this.sqlBuffer = new StringBuilder();
        }

        public void applyAll() {
            sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
            String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
            if (trimmedUppercaseSql.length() > 0) {
                // 动态加前缀
                applyPrefix(sqlBuffer, trimmedUppercaseSql);
                // 动态加后缀
                applySuffix(sqlBuffer, trimmedUppercaseSql);
            }
            // 添加完拼接的前后缀,继续拼接完整的Sql
            delegate.appendSql(sqlBuffer.toString());
        }

        // 获取当前属性
        @Override
        public Map<String, Object> getBindings() {
            return delegate.getBindings();
        }

        // 拼接当前Sql
        @Override
        public void appendSql(String sql) {
            sqlBuffer.append(sql);
        }

        @Override
        public String getSql() {
            return delegate.getSql();
        }


        /**
         * 拼接前缀处理
         */
        private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
            if (!prefixApplied) {
                prefixApplied = true;
                if (prefixesToOverride != null) {
                    for (String toRemove : prefixesToOverride) {
                        if (trimmedUppercaseSql.startsWith(toRemove)) {
                            sql.delete(0, toRemove.trim().length());
                            break;
                        }
                    }
                }
            }
            if (prefix != null) {
                sql.insert(0, " ");
                sql.insert(0, prefix);
            }
        }

        /**
         * 拼接后缀处理
         */
        private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
            if (!suffixApplied) {
                suffixApplied = true;
                if (suffixesToOverride != null) {
                    for (String toRemove : suffixesToOverride) {
                        if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
                            int start = sql.length() - toRemove.trim().length();
                            int end = sql.length();
                            sql.delete(start, end);
                            break;
                        }
                    }
                }
                if (suffix != null) {
                    sql.append(" ");
                    sql.append(suffix);
                }
            }
        }


    }

}

3.3 DynamicSqlSource

在处理完Sql语句需要包装成SqlSource源,之前处理的都是静态源,这次我们需要加动态源类DynamicSqlSource类,实现SqlSource。

1.这个动态源主要是将所有的SqlNode进行处理,拼接为一个处理过的Sql然后直接返回Sql。

2.最后返回SqlSource时判断是否是动态的,是返回DynamicSqlSource,不是返回RawSqlSource

/**
 * @Author df
 * @Description: 动态SQL源码
 * @Date 2023/12/22 11:20
 */
// step-15新增
public class DynamicSqlSource implements SqlSource {

    private Configuration configuration;
    private SqlNode rootSqlNode;

    public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
        // 生成一个 DynamicContext 动态上下文
        DynamicContext context = new DynamicContext(configuration, parameterObject);
        // SqlNode.apply 将 ${} 参数替换掉,不替换 #{} 这种参数
        rootSqlNode.apply(context);

        // 调用 SqlSourceBuilder
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();

        // SqlSourceBuilder.parse 这里返回的是 StaticSqlSource,解析过程就把那些参数都替换成?了,也就是最基本的JDBC的SQL语句。
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

        // SqlSource.getBoundSql,非递归调用,而是调用 StaticSqlSource 实现类
        BoundSql boundSql = sqlSource.getBoundSql(parameterType);
        for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
            boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
        }
        return boundSql;
    }


        // 解析脚本里的动态节点
     // 更改判断是否动态调用不同的SqlSource
    public SqlSource parseScriptNode() {
        // step-15修改
        List<SqlNode> contents = parseDynamicTags(element);
        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
        SqlSource sqlSource = null;
        if (isDynamic) {
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
}

4.测试准备

dao层:

public interface IActivityDao {

    Activity queryActivityById(Activity activity);

}

 Activity_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="cn.bugstack.mybatis.test.dao.IActivityDao">
    <resultMap id="activityMap" type="cn.bugstack.mybatis.test.po.Activity">
        <id column="id" property="id"/>
        <result column="activity_id" property="activityId"/>
        <result column="activity_name" property="activityName"/>
        <result column="activity_desc" property="activityDesc"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>

    <select id="queryActivityById" parameterType="cn.bugstack.mybatis.test.po.Activity" resultMap="activityMap">
        SELECT activity_id, activity_name, activity_desc, create_time, update_time
        FROM activity
        <trim prefix="where" prefixOverrides="AND | OR" suffixOverrides="and">
            <if test="null != activityId">
                activity_id = #{activityId}
            </if>
        </trim>
    </select>
</mapper>

 单元测试

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    private SqlSession sqlSession;

    @Before
    public void init() throws IOException {
        // 1. 从SqlSessionFactory中获取SqlSession
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
        sqlSession = sqlSessionFactory.openSession();
    }

    @Test
    public void test_queryActivityById() throws IOException {
        // 2. 获取映射器对象
        IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
        // 3. 测试验证
        Activity req = new Activity();
        req.setActivityId(100001L);
        Activity res = dao.queryActivityById(req);
        logger.info("测试结果:{}", JSON.toJSONString(res));
    }
}

执行单元测试,结果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/287965.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI原生应用开发“三板斧”亮相WAVE SUMMIT+2023

面对AI应用创新的风口跃跃欲试&#xff0c;满脑子idea&#xff0c;却苦于缺乏技术背景&#xff0c;不得不望而却步&#xff0c;这曾是许多开发者的苦恼&#xff0c;如今正在成为过去。 12月28日&#xff0c;WAVE SUMMIT深度学习开发者大会2023在北京举办。百度AI技术生态总经理…

文件监控软件丨文件权限管理工具

文件已经成为企业最重要的资产之一。然而&#xff0c;文件的安全性和完整性经常受到威胁&#xff0c;如恶意软件感染、人为误操作、内部泄密等。 为了确保文件的安全&#xff0c;文件监控软件应运而生。本文将深入探讨文件监控软件的概念、功能、应用场景和未来发展等方面。 文…

Grafana UI 入门使用

最近项目上需要使用Grafana来做chart&#xff0c;因为server不是我在搭建&#xff0c;所以就不介绍怎么搭建grafana server&#xff0c;而是谈下怎么在UI上具体操作使用了。 DOCs 首先呢&#xff0c;贴一下官网doc的连接&#xff0c;方便查询 Grafana open source documenta…

【数据库原理】(6)关系数据库的关系操作集合

基本关系操作 关系数据操作的对象都是关系,其操作结果仍为关系,即集合式操作。关系数据库的操作可以分为两大类&#xff1a;数据查询和数据更新。这些操作都是基于数学理论&#xff0c;特别是集合理论。下面是对这些基本操作的解释和如何用不同的关系数据语言来表达这些操作的…

Objects are not valid as a React child (found: object with keys {name}).

在jsx中可以嵌套表达式&#xff0c;将表达式作为内容的一部分&#xff0c;但是要注意&#xff0c;普通对象不能作为子元素&#xff1b;但是数组&#xff0c;react元素对象是可以的 如下&#xff1a;不能将stu这个对象作为子元素放 function App() {const myCal imgStyleconst…

OSG-纹理映射(二)

2.6 Mipmap纹理映射 在一个动态的场景中&#xff0c;当一个纹理对象迅速远离视点时&#xff0c;纹理图像必须随着被投影的图像一起缩小。为了实现这种效果&#xff0c;可以通过对纹理图像进行过滤&#xff0c;适当对它进行缩小&#xff0c;以使它映射到物体的表面时不会产生抖动…

Android 串口协议

前言 本协议是 Android 应用端与主控板之间的通信协议&#xff0c;是串行通信协议。 协议要求同一时间只能有两个通讯端点在相互通讯&#xff0c;采用小端传输数据。 硬件层基于RS485协议&#xff0c;采取半双工&#xff0c;一主多从的通讯模式。Android定义为主机&#xff0c…

DataGear 4.7.0 发布,数据可视化分析平台

DataGear 4.7.0 发布&#xff0c;严重漏洞和BUG修复&#xff0c;具体更新内容如下&#xff1a; 新增&#xff1a;HTTP数据集新增【编码请求地址】支持&#xff0c;可用于解决请求地址中文乱码问题&#xff1b;新增&#xff1a;新增数据源密码加密存储支持&#xff08;开启需设…

怎么有效利用HTTPS协议

HTTPS的发展史可以追溯到早期的互联网时代&#xff0c;当时HTTP协议被广泛使用&#xff0c;但由于通信过程是明文的&#xff0c;导致用户的敏感信息容易被截取和窃取。为了解决这个问题&#xff0c;HTTPS协议应运而生。 HTTPS是在HTTP协议的基础上加入了传输层安全协议&#x…

深挖小白必会指针笔试题<一>

目录 引言 关键解决办法&#xff1a; 学会画图确定指向关系 例题一&#xff1a; 画图分析&#xff1a; 例题二&#xff1a; 画图分析&#xff1a; 例题三&#xff1a; 注&#xff1a;%x是按十六进制打印 画图分析&#xff1a; 例题四&#xff1a; 画图分析&…

基于Java+SpringMvc+Vue求职招聘系统详细设计实现

基于JavaSpringMvcVue求职招聘系统详细设计实现 &#x1f345; 作者主页 专业程序开发 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; 文章目录 基于JavaSpringMvcVue求职招聘系统详细设计实现一、前言介…

众和策略:今日,有“天地板”,也有“地天板”

今日早盘&#xff0c;A股持续坚持弱势震动&#xff0c;两市成交有进一步萎缩的趋势。 盘面上&#xff0c;煤炭、传媒娱乐、旅行、房地产等板块相对活泼&#xff0c;混合实际、PEEK材料、苹果概念、华为汽车等板块跌幅居前。 个股方面&#xff0c;神马电力连续5日涨停&#xf…

react useEffect 内存泄漏

componentWillUnmount() {this.setState (state, callback) > {return;};// 清除reactionthis.reaction();}useEffect 使用AbortController useEffect(() > { let abortController new AbortController(); // your async action is here return () > { abortCo…

TCP/IP的网络层(即IP层)之IP地址和网络掩码,在视频监控系统中的配置和应用

在给客户讲解我们的AS-V1000视频监控平台的时候&#xff0c;有的客户经常会配置错误IP地址的掩码和网关&#xff0c;导致出现一些网路问题。而在视频监控系统中&#xff0c;IP地址和子网掩码是用于标识网络中设备的重要标识符。IP地址被用来唯一地标识一个网络设备&#xff0c;…

express+mongoDB开发入门教程之mongoDB安装

系列文章 node.js express框架开发入门教程 expressmongoDB开发入门教程之mongoDB安装expressmongoDB开发入门教程之mongoose使用讲解 文章目录 系列文章前言一、mongoDB安装1.下载2.安装3. 设置全局环境变量4.启动mongoDB服务 二、可视化管理工具 前言 MongoDB是一个基于分布…

【盛况回顾】聚焦流程创新,共话科技共赢:企业“流程三驾马车”闭环主题沙龙圆满落幕

12月7日&#xff0c;由上海斯歌主办&#xff0c;博阳精讯、凡得科技协办的“流程创新科技共赢——企业流程三驾马车闭环主题沙龙”在上海召开并圆满落幕。本次沙龙&#xff0c;上海斯歌携手来自不同行业的客户与伙伴的资深业务、解决方案专家&#xff0c;围绕流程体系化建模、流…

uniCloud 云数据库(新建表、增、删、改、查)

新建表结构描述文件 todo 为自定义的表名 表结构描述文件的默认后缀为 .schema.json 设置表的操作权限 uniCloud-aliyun/database/todo.schema.json 默认的操作权限都是 false "permission": {"read": false,"create": false,"update&quo…

Spring上下文之support模块DefaultLifecycleProcessor

博主介绍:✌全网粉丝5W+,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验✌ 博主作品:《Java项目案例》主要基于SpringBoot+MyBatis/MyBatis-plus+…

Numpy基础

目录&#xff1a; 一、简介:二、array数组ndarray&#xff1a;1.array( )创建数组&#xff1a;2.数组赋值和引用的区别&#xff1a;3.arange( )创建区间数组&#xff1a;4.linspace( )创建等差数列&#xff1a;5.logspace( )创建等比数列&#xff1a;6.zeros( )创建全0数组&…

Spring源码之依赖注入(二)

书接上文 文章目录 一. Autowire底层注入逻辑1. 属性注入逻辑 一. Autowire底层注入逻辑 前面我们分析了Spring时如何找到某个目标类的所有注入点这一个核心逻辑&#xff0c;但还没又对核心注入方法inject进行详细分析&#xff0c;下面我们就来详细分析Spring拿到所有的注入点…