bizlog通用操作日志组件(代码分析篇)

news2024/11/17 1:26:56

引言

在上篇博客中介绍了通用操作日志组件的使用方法,本篇博客将从源码出发,学习一下该组件是如何实现的。

代码结构

在这里插入图片描述
该组件主要是通过AOP拦截器实现的,整体上可分为四个模块:AOP模块、日志解析模块、日志保存模块、Starter模块;另外,提供了四个扩展点:自定义函数、默认处理人、业务保存和查询。

模块介绍

AOP拦截

1. 针对@LogRecord注解分析日志,自定义注解如下:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecord {
    /**
     * @return 方法执行成功后的日志模版
     */
    String success();

    /**
     * @return 方法执行失败后的日志模版
     */
    String fail() default "";

    /**
     * @return 日志的操作人
     */
    String operator() default "";

    /**
     * @return 操作日志的类型,比如:订单类型、商品类型
     */
    String type();

    /**
     * @return 日志的子类型,比如订单的C端日志,和订单的B端日志,type都是订单类型,但是子类型不一样
     */
    String subType() default "";

    /**
     * @return 日志绑定的业务标识
     */
    String bizNo();

    /**
     * @return 日志的额外信息
     */
    String extra() default "";

    /**
     * @return 是否记录日志
     */
    String condition() default "";

    /**
     * 记录成功日志的条件
     *
     * @return 表示成功的表达式,默认为空,代表不抛异常为成功
     */
    String successCondition() default "";
}

注解的参数在上篇博客的使用中基本都有提到,这里就不再赘述了。

2. 切点通过StaticMethodMatcherPointcut匹配包含LogRecord注解的方法

public class LogRecordPointcut extends StaticMethodMatcherPointcut implements Serializable {

    //LogRecord解析类
    private LogRecordOperationSource logRecordOperationSource;

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        // 解析 这个 method 上有没有 @LogRecord 注解,有的话会解析出来注解上的各个参数
        return !CollectionUtils.isEmpty(logRecordOperationSource.computeLogRecordOperations(method, targetClass));
    }

    void setLogRecordOperationSource(LogRecordOperationSource logRecordOperationSource) {
        this.logRecordOperationSource = logRecordOperationSource;
    }
}

3. 通过实现MethodInterceptor接口实现操作日志的切面增强逻辑

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
    Method method = invocation.getMethod();
    //记录日志
    return execute(invocation, invocation.getThis(), method, invocation.getArguments());
}

private Object execute(MethodInvocation invoker, Object target, Method method, Object[] args) throws Throwable {
    //代理不拦截
    if (AopUtils.isAopProxy(target)) {
        return invoker.proceed();
    }
    StopWatch stopWatch = new StopWatch(MONITOR_NAME);
    stopWatch.start(MONITOR_TASK_BEFORE_EXECUTE);
    Class<?> targetClass = getTargetClass(target);
    Object ret = null;
    MethodExecuteResult methodExecuteResult = new MethodExecuteResult(method, args, targetClass);
    LogRecordContext.putEmptySpan();
    Collection<LogRecordOps> operations = new ArrayList<>();
    Map<String, String> functionNameAndReturnMap = new HashMap<>();
    try {
        operations = logRecordOperationSource.computeLogRecordOperations(method, targetClass);
        List<String> spElTemplates = getBeforeExecuteFunctionTemplate(operations);
        functionNameAndReturnMap = processBeforeExecuteFunctionTemplate(spElTemplates, targetClass, method, args);
    } catch (Exception e) {
        log.error("log record parse before function exception", e);
    } finally {
        stopWatch.stop();
    }

    try {
        ret = invoker.proceed();
        methodExecuteResult.setResult(ret);
        methodExecuteResult.setSuccess(true);
    } catch (Exception e) {
        methodExecuteResult.setSuccess(false);
        methodExecuteResult.setThrowable(e);
        methodExecuteResult.setErrorMsg(e.getMessage());
    }
    stopWatch.start(MONITOR_TASK_AFTER_EXECUTE);
    try {
        if (!CollectionUtils.isEmpty(operations)) {
            recordExecute(methodExecuteResult, functionNameAndReturnMap, operations);
        }
    } catch (Exception t) {
        log.error("log record parse exception", t);
        throw t;
    } finally {
        LogRecordContext.clear();
        stopWatch.stop();
        try {
            logRecordPerformanceMonitor.print(stopWatch);
        } catch (Exception e) {
            log.error("execute exception", e);
        }
    }

    if (methodExecuteResult.getThrowable() != null) {
        throw methodExecuteResult.getThrowable();
    }
    return ret;
}
解析逻辑

解析核心类是LogRecordExpressionEvaluator,解析Spring EL表达式。

public class LogRecordExpressionEvaluator extends CachedExpressionEvaluator {

    private Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);

    private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);

    public String parseExpression(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
        return getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class);
    }
}

expressionCache这个Map是为了缓存方法、表达式和 SpEL 的 Expression 的对应关系,让方法注解上添加的 SpEL 表达式只解析一次。targetMethodCache Map是为了缓存传入到 Expression 表达式的 Object。

getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, String.class)这行代码就是解析参数和变量的。

日志上下文实现

方法参数中不存在的变量,我们可以通过LogRecordContext传入,而通过LogRecordContext传入的变量也是使用SpEL的getValue方法取值的。

1. 在LogRecordValueParser中创建EvaluationContext


EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(method, args, targetClass, ret, errorMsg, beanFactory);


public EvaluationContext createEvaluationContext(Method method, Object[] args, Class<?> targetClass,
                                                 Object result, String errorMsg, BeanFactory beanFactory) {
    Method targetMethod = getTargetMethod(targetClass, method);
    LogRecordEvaluationContext evaluationContext = new LogRecordEvaluationContext(
            null, targetMethod, args, getParameterNameDiscoverer(), result, errorMsg);
    if (beanFactory != null) {
        evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
    }
    return evaluationContext;
}

在解析的时候调用 getValue 方法传入的参数 evalContext,就是上面这个 EvaluationContext 对象。

2. LogRecordEvaluationContext

LogRecordEvaluationContext中将方法的参数、LogRecordContext中的变量、方法的返回值和ErrorMsg都放到SpEL解析的RootObject中。

public class LogRecordEvaluationContext extends MethodBasedEvaluationContext {

    public LogRecordEvaluationContext(Object rootObject, Method method, Object[] arguments,
                                      ParameterNameDiscoverer parameterNameDiscoverer, Object ret, String errorMsg) {
       //把方法的参数都放到 SpEL 解析的 RootObject 中
       super(rootObject, method, arguments, parameterNameDiscoverer);
       //把 LogRecordContext 中的变量都放到 RootObject 中
        Map<String, Object> variables = LogRecordContext.getVariables();
        if (variables != null && variables.size() > 0) {
            for (Map.Entry<String, Object> entry : variables.entrySet()) {
                setVariable(entry.getKey(), entry.getValue());
            }
        }
        //把方法的返回值和 ErrorMsg 都放到 RootObject 中
        setVariable("_ret", ret);
        setVariable("_errorMsg", errorMsg);
    }
}
默认操作人逻辑

在 LogRecordInterceptor 中 IOperatorGetService 接口,这个接口可以获取到当前的用户。组件在解析operator的时候,就判断注解上的operator是否是空,为空会查询默认用户。

private String getOperatorIdFromServiceAndPutTemplate(LogRecordOps operation, List<String> spElTemplates) {

    String realOperatorId = "";
    if (StringUtils.isEmpty(operation.getOperatorId())) {
        realOperatorId = operatorGetService.getUser().getOperatorId();
        if (StringUtils.isEmpty(realOperatorId)) {
            throw new IllegalArgumentException("[LogRecord] operator is null");
        }
    } else {
        spElTemplates.add(operation.getOperatorId());
    }
    return realOperatorId;
自定义函数逻辑

1. IParseFunction的接口定义

public interface IParseFunction {

    default boolean executeBefore() {
        return false;
    }

    String functionName();

    /**
     * @param value 函数入参
     * @return 文案
     * @since 1.1.0 参数从String 修改为Object类型,可以处理更多的场景,可以通过SpEL表达式传递对象了
     * 老版本需要改下自定义函数的声明,实现使用中把 用到 value的地方修改为 value.toString 就可以兼容了
     */
    String apply(Object value);
}

executeBefore 函数代表了自定义函数是否在业务代码执行之前解析。

2. ParseFunctionFactory:把所有的IParseFunction注入到函数工厂中

public class ParseFunctionFactory {
    private Map<String, IParseFunction> allFunctionMap;

    public ParseFunctionFactory(List<IParseFunction> parseFunctions) {
        if (CollectionUtils.isEmpty(parseFunctions)) {
            return;
        }
        allFunctionMap = new HashMap<>();
        for (IParseFunction parseFunction : parseFunctions) {
            if (StringUtils.isEmpty(parseFunction.functionName())) {
                continue;
            }
            allFunctionMap.put(parseFunction.functionName(), parseFunction);
        }
    }

    public IParseFunction getFunction(String functionName) {
        return allFunctionMap.get(functionName);
    }

    public boolean isBeforeFunction(String functionName) {
        return allFunctionMap.get(functionName) != null && allFunctionMap.get(functionName).executeBefore();
    }
}

3. DefaultFunctionServiceImpl:根据传入的函数名称 functionName 找到对应的 IParseFunction,然后把参数传入到 IParseFunction 的 apply 方法上最后返回函数的值。

public class DefaultFunctionServiceImpl implements IFunctionService {

    private final ParseFunctionFactory parseFunctionFactory;

    public DefaultFunctionServiceImpl(ParseFunctionFactory parseFunctionFactory) {
        this.parseFunctionFactory = parseFunctionFactory;
    }

    @Override
    public String apply(String functionName, Object value) {
        IParseFunction function = parseFunctionFactory.getFunction(functionName);
        if (function == null) {
            return value.toString();
        }
        return function.apply(value);
    }

    @Override
    public boolean beforeFunction(String functionName) {
        return parseFunctionFactory.isBeforeFunction(functionName);
    }
}
日志持久化逻辑

LogRecordInterceptor引用了ILogRecordService,业务可以实现这个接口保存日志。

@Slf4j
public class DefaultLogRecordServiceImpl implements ILogRecordService {

//    @Resource
//    private LogRecordMapper logRecordMapper;

    @Override
//    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record(LogRecord logRecord) {
        log.info("【logRecord】log={}", logRecord);
        //throw new RuntimeException("sss");
//        logRecordMapper.insertSelective(logRecord);
    }
}

业务可以把保存设置成异步或者同步,可以和业务放在一个事务中保证操作日志和业务的一致性,也可以新开辟一个事务,保证日志的错误不影响业务的事务。业务可以保存在 Elasticsearch、数据库或者文件中,用户可以根据日志结构和日志的存储实现相应的查询逻辑。

Starter逻辑封装

我们直接在Spring Boot启动类上添加@EnableLogRecord注解即可使用,就是对上面实现逻辑的组件做了Starter封装。

1. EnableLogRecord注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(LogRecordConfigureSelector.class)
public @interface EnableLogRecord {

    String tenant();

    /**
     * !不要删掉,为 null 就不代理了哦
     * true 都使用 CGLIB 代理
     * false 目标对象实现了接口 – 使用JDK动态代理机制(代理所有实现了的接口) 目标对象没有接口(只有实现类) – 使用CGLIB代理机制
     *
     * @return 不强制 cglib
     */
    boolean proxyTargetClass() default false;

    /**
     * Indicate how caching advice should be applied. The default is
     * {@link AdviceMode#PROXY}.
     *
     * @return 代理方式
     * @see AdviceMode
     */
    AdviceMode mode() default AdviceMode.PROXY;

    /**
     * 记录日志日志与业务日志是否同一个事务
     *
     * @return 默认独立
     */
    boolean joinTransaction() default false;

    /**
     * Indicate the ordering of the execution of the transaction advisor
     * when multiple advices are applied at a specific joinpoint.
     * <p>The default is {@link Ordered#LOWEST_PRECEDENCE}.
     *
     * @return 事务 advisor 的优先级
     */
    int order() default Ordered.LOWEST_PRECEDENCE;
}

代码中Import了LogRecordConfigureSelector.class,在 LogRecordConfigureSelector 类中暴露了 LogRecordProxyAutoConfiguration 类。

2. 核心类LogRecordProxyAutoConfiguration装配上面组件

@Configuration
@EnableConfigurationProperties({LogRecordProperties.class})
@Slf4j
public class LogRecordProxyAutoConfiguration implements ImportAware {

    private AnnotationAttributes enableLogRecord;


    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public LogRecordOperationSource logRecordOperationSource() {
        return new LogRecordOperationSource();
    }

    @Bean
    @ConditionalOnMissingBean(IFunctionService.class)
    public IFunctionService functionService(ParseFunctionFactory parseFunctionFactory) {
        return new DefaultFunctionServiceImpl(parseFunctionFactory);
    }

    @Bean
    public ParseFunctionFactory parseFunctionFactory(@Autowired List<IParseFunction> parseFunctions) {
        return new ParseFunctionFactory(parseFunctions);
    }

    @Bean
    @ConditionalOnMissingBean(IParseFunction.class)
    public DefaultParseFunction parseFunction() {
        return new DefaultParseFunction();
    }


    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public BeanFactoryLogRecordAdvisor logRecordAdvisor() {
        BeanFactoryLogRecordAdvisor advisor =
                new BeanFactoryLogRecordAdvisor();
        advisor.setLogRecordOperationSource(logRecordOperationSource());
        advisor.setAdvice(logRecordInterceptor());
        advisor.setOrder(enableLogRecord.getNumber("order"));
        return advisor;
    }

    @Bean
    @ConditionalOnMissingBean(ILogRecordPerformanceMonitor.class)
    public ILogRecordPerformanceMonitor logRecordPerformanceMonitor() {
        return new DefaultLogRecordPerformanceMonitor();
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public LogRecordInterceptor logRecordInterceptor() {
        LogRecordInterceptor interceptor = new LogRecordInterceptor();
        interceptor.setLogRecordOperationSource(logRecordOperationSource());
        interceptor.setTenant(enableLogRecord.getString("tenant"));
        interceptor.setJoinTransaction(enableLogRecord.getBoolean("joinTransaction"));
        //interceptor.setLogFunctionParser(logFunctionParser(functionService));
        //interceptor.setDiffParseFunction(diffParseFunction);
        interceptor.setLogRecordPerformanceMonitor(logRecordPerformanceMonitor());
        return interceptor;
    }

//    @Bean
//    public LogFunctionParser logFunctionParser(IFunctionService functionService) {
//        return new LogFunctionParser(functionService);
//    }

    @Bean
    public DiffParseFunction diffParseFunction(IDiffItemsToLogContentService diffItemsToLogContentService) {
        DiffParseFunction diffParseFunction = new DiffParseFunction();
        diffParseFunction.setDiffItemsToLogContentService(diffItemsToLogContentService);
        return diffParseFunction;
    }

    @Bean
    @ConditionalOnMissingBean(IDiffItemsToLogContentService.class)
    @Role(BeanDefinition.ROLE_APPLICATION)
    public IDiffItemsToLogContentService diffItemsToLogContentService(LogRecordProperties logRecordProperties) {
        return new DefaultDiffItemsToLogContentService(logRecordProperties);
    }

    @Bean
    @ConditionalOnMissingBean(IOperatorGetService.class)
    @Role(BeanDefinition.ROLE_APPLICATION)
    public IOperatorGetService operatorGetService() {
        return new DefaultOperatorGetServiceImpl();
    }

    @Bean
    @ConditionalOnMissingBean(ILogRecordService.class)
    @Role(BeanDefinition.ROLE_APPLICATION)
    public ILogRecordService recordService() {
        return new DefaultLogRecordServiceImpl();
    }

    @Override
    public void setImportMetadata(AnnotationMetadata importMetadata) {
        this.enableLogRecord = AnnotationAttributes.fromMap(
                importMetadata.getAnnotationAttributes(EnableLogRecord.class.getName(), false));
        if (this.enableLogRecord == null) {
            log.info("EnableLogRecord is not present on importing class");
        }
    }
}

这个类继承 ImportAware 是为了拿到 EnableLogRecord 上的租户属性,这个类使用变量 logRecordAdvisor 和 logRecordInterceptor 装配了 AOP,同时把自定义函数注入到了 logRecordAdvisor 中。

对外扩展类:分别是IOperatorGetService、ILogRecordService、IParseFunction。业务可以自己实现相应的接口,因为配置了 @ConditionalOnMissingBean,所以用户的实现类会覆盖组件内的默认实现。

总结

通过对bizlog组件的使用和代码分析,很好地解决了项目的需求,也了解了其内部是如何实现的,算是有所收获。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

企业小程序商城的推广方式有哪些_分享小程序商城的作用

其实搭建小程序商城比较容易&#xff0c;难的是后期的运营。要想办法进行引流&#xff0c;用户运营伙伴就给大家介绍一些引流推广的方法。 1、利用微信好友、微信群和朋友圈 可以让用户分享小程序给微信好友或微信群&#xff0c;这是吸引新用户的最快方法。除此之外&#xff0…

Kettle入门到实战

简介 Kettle是一个方便ETL(数据的抽取&#xff0c;装换&#xff0c;装载)开源框架。 官网 kettle下载、kettle源码下载 – Kettle中文网 百度网盘下载 链接&#xff1a;https://pan.baidu.com/s/1C-izMX_3KMkRb5hhdj66xg 提取码&#xff1a;yyds --来自百度网盘超级会员…

go radix tree

Radix Tree Search Insert Insert ‘water’ at the root Insert ‘slower’ while keeping ‘slow’ Insert ‘test’ which is a prefix of ‘tester’ Insert ‘team’ while splitting ‘test’ and creating a new edge label ‘st’ Insert ‘toast’ while splitti…

java 多线程()—— 线程同步=队列+锁

一、线程同步 队列 锁 同步就是多个线程同时访问一个资源。 那么如何实现&#xff1f; 队列锁。 想要访问同一资源的线程排成一个队列&#xff0c;按照排队的顺序访问。访问的时候加上一个锁&#xff08;参考卫生巾排队锁门&#xff09;&#xff0c;访问完释放锁。 二、 不…

gitblit 搭建本地 git 仓库

目录 一、简介 二、准备工作 1.安装Java 2.下载gitblit 3.创建资料目录 三、修改配置 1.git.repositoriesFolder 2.server.httpPort 3.server.httpBindInterface 4.installService.cmd 四、gitblit图标显示异常 结束 一、简介 Gitblit是一个用于管理&#xff0c;查…

数据结构与算法这么重要还不会?字节内部笔记来帮你轻松拿下!

对任何专业技术人员来说&#xff0c;理解数据结构都非常重要。作为软件开发者&#xff0c;我们要能够用编程语言和数据结构来解决问题。编程语言和数据结构是这些问题解决方案中不可或缺的一部分。如果选择了不恰当的数据结构&#xff0c;可能会影响所写程序的性能。因此&#…

VKL076-19*4点 超低功耗抗干扰LCD液晶段码显示屏驱动控制电路(IC/芯片),超低工作电流约7.5微安,多用于仪器仪表类,可提供FAE技术支持

产品品牌&#xff1a;永嘉微电/VINKA 产品型号&#xff1a;VKL076 封装形式&#xff1a;SSOP28 概述&#xff1a; VKL076 SSOP28是一个点阵式存储映射的LCD驱动器&#xff0c;可支持最大76点&#xff08;19SEGx4COM&#xff09;的 LCD屏。单片机可通过I2C接口配置显示参数和…

【Hack The Box】linux练习-- SwagShop

HTB 学习笔记 【Hack The Box】linux练习-- SwagShop &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月17日&#x1f334; &#x1…

Python 完美解决 Import “模块“ could not be resolved ...

vscode 中 python 提示警告错误&#xff0c;但是还是能跑起来代码&#xff1a; Import "playwright.sync_api" could not be resolved Pylance reportMissingImports 原因可能有两个&#xff1a; 1、未下载此包&#xff0c;打开命令行&#xff0c;输入 $ pip list&a…

problem B.Genshin Impact(2022合肥icpc)

题意&#xff1a;对目标持续施法&#xff0c;法术是每隔y秒让目标开始持续燃烧x秒&#xff0c;每次施法的概率是1/p 求燃烧时间比上总时间的期望值 &#xff08;题面是laji&#xff09; 思路&#xff1a;我们把总时间看成许多y段 当x<y的时候&#xff0c;只有一种情况就…

JVM虚拟机(整体架构、类文件结构)我来了~~~

虚拟机 1.1 发展历程 1.1.1 java往事 ​ Java诞生在一群懒惰、急躁而傲慢的程序天才之中。 ​ 1990年12月&#xff0c;Sun的工程师Patrick Naughton被当时糟糕的Sun C工具折磨的快疯了。他大声抱怨&#xff0c;并威胁要离开Sun转投当时在Steve Jobs领导之下的NeXT公司。领导…

CRGDFPASSC,CAS号:166184-23-2

CRGDFPASSC是一种含环rgd的十肽&#xff0c;与血小板表面的纤维蛋白原受体结合。在5号位置用Phe取代Ser的类似物作为血小板聚集抑制剂&#xff0c;其活性是CRGDSPASSC的3倍(IC₅₀ 2.5M)。 编号: 130659中文名称: CRGDFPASSC英文名: CRGDFPASSCCAS号: 166184-23-2单字母: CRGDF…

方法2—并行数据流转换为一种特殊串行数据流模块的设计,

并行数据流转换为一种特殊串行数据流模块的设计&#xff0c;设计两个可综合的电路模块1&#xff0c;第一个可综合模块&#xff0c;M1。2&#xff0c;描述M2模块3&#xff0c;描述M0模块的Verilog代码4&#xff0c;描述顶层模块5&#xff0c;电路生成的门级网表&#xff0c;netl…

【第五部分 | JS WebAPI】1:WebAPIs概述、网页元素的获取、事件

目录 | 概述 | 文档、元素、节点的概念 | 获取元素 根据ID获取 根据标签名获取 通过HTML5新增方法获取 特殊元素获取&#xff08;body html&#xff09; | 事件基础 事件三要素 点击事件 光标获得/失去焦点事件 [ 更多其它事件 ] 刷新网页自动执行某些事件 | 概述 …

Alibaba内部首发“面试百宝书+超全算法面试手册”PDF版下载

面试你打算要多高的薪资&#xff1f; 第一份工作的薪资水平就是你的薪资起点&#xff0c;如果你拿到的第一份薪水远高于其他人&#xff0c;那么你在未来涨薪路上就会省很多力。 想刚开始工作就拥有高薪&#xff0c;那就需要抬高自己的“身价”&#xff0c;提升自己的工作能力…

[附源码]java毕业设计-线上摄影平台系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Blob和ArrayBuffer和File

Blob Blob对象表示一个不可变、原始数据的类似文件的对象。Blob 表示的不一定是JavaScript原生格式的数据。 Represents a “Binary Large Object”, meaning a file-like object of immutable, raw data。 type BufferSource ArrayBufferView | ArrayBuffer; type BlobPart…

微前端——single-spa源码学习

前言 本来是想直接去学习下qiankun的源码&#xff0c;但是qiankun是基于single-spa做的二次封装&#xff0c;通过解决了single-spa的一些弊端和不足来帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。 所以我们应该先对single-spa有一个全面的认识和了解&#xff0c…

看了就能懂的NIO使用深入详解

NIO概述 NIO介绍 传统IO流(java.io):读写操作结束前,处于线性阻塞,代码简单,安全,性能低 NIO&#xff1a;支持非阻塞式编程,性能更有优势,但代码编写较为复杂。 概念理解 同步(synchronous):一条线程执行期间,其他线程就只能等待。 异步(asynchronous):一条线程在执行…

Java基础深化和提高-------多线程与并发编程

目录 多线程与并发编程 多线程介绍 什么是程序&#xff1f; 什么是进程? 什么是线程&#xff1f; 进程、线程的区别 什么是并发 线程和方法的执行特点 方法的执行特点 线程的执行特点 什么是主线程以及子线程 主线程 子线程 线程的创建 通过继承Thread类实现多线程 通过Ru…