返回顶部
首页 > 资讯 > 后端开发 > Python >利用ThreadLocal实现一个上下文管理组件
  • 894
分享到

利用ThreadLocal实现一个上下文管理组件

ThreadLocal上下文管理组件ThreadLocal上下文管理 2022-11-13 18:11:10 894人浏览 泡泡鱼

Python 官方文档:入门教程 => 点击学习

摘要

目录1 ThreadLocal原理set() 方法get() 方法withInitial()方法ThreadLocal中的内存泄漏问题2 自定义上下文Scope3 在线程池中传递Sc

本文基于ThreadLocal原理,实现了一个上下文状态管理组件Scope,通过开启一个自定义的Scope,在Scope范围内,可以通过Scope各个方法读写数据;

通过自定义线程池实现上下文状态数据的线程间传递;

提出了一种基于FilterScopeRequest粒度的上下文管理方案。

GitHubhttps://github.com/penGChengSU/demo-request-scope

1 ThreadLocal原理

ThreadLocal主要作用就是实现线程间变量隔离,对于一个变量,每个线程维护一个自己的实例,防止多线程环境下的资源竞争,那ThreadLocal是如何实现这一特性的呢?

图1

从上图可知:

  • 每个Thread对象中都包含一个ThreadLocal.ThreadLocalMap类型的threadlocals成员变量;
  • 该map对应的每个元素Entry对象中:key是ThreadLocal对象的弱引用,value是该threadlocal变量在当前线程中的对应的变量实体;
  • 当某一线程执行获取该ThreadLocal对象对应的变量时,首先从当前线程对象中获取对应的threadlocals哈希表,再以该ThreadLocal对象为key查询哈希表中对应的value;
  • 由于每个线程独占一个threadlocals哈希表,因此线程间ThreadLocal对象对应的变量实体也是独占的,不存在竞争问题,也就避免了多线程问题。

有人可能会问:ThreadLocalMapThread成员变量(非public,只有包访问权限,Thread和Threadlocal都在java.lang 包下,Thread可以访问ThreadLocal.ThreadLocalMap),定义却在ThreadLocal中,为什么要这么设计?

源码的注释给出了解释:ThreadLocalMap就是维护线程本地变量设计的,就是让使用者知道ThreadLocalMap就只做保存线程局部变量这一件事。

set() 方法

public void set(T value) {
    Thread t = Thread.currentThread();	//获取当前线程
    ThreadLocalMap map = getMap(t);	//从当前线程对象中获取threadlocals,该map保存了所用的变量实例
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);	//初始threadlocals,并设置当前变量
    }
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get() 方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); //从当前线程对象中获取threadlocals,该map保存了所用的变量实体
    if (map != null) {
        // 获取当前threadlocal对象对应的变量实体
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map没有初始化,那么在这里初始化一下
    return setInitialValue();
}

withInitial()方法

由于通过 ThreadLocalset() 设置的值,只会设置当前线程对应变量实体,无法实现统一初始化所有线程的ThreadLocal的值。ThreadLocal提供了一个 withInitial() 方法实现这一功能:

ThreadLocal<String> initValue = ThreadLocal.withInitial(() -> "initValue");
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    // 返回SuppliedThreadLocal类型对象
    return new SuppliedThreadLocal<>(supplier);
}
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        // 获取初始化值
        return supplier.get();
    }
}

ThreadLocal中的内存泄漏问题

由图1可知,ThreadLocal.ThreadLocalMap 对应的Entry中,key为ThreadLocal对象的弱引用,方法执行对应栈帧中的ThreadLocal引用为强引用。当方法执行过程中,由于栈帧销毁或者主动释放等原因,释放了ThreadLocal对象的强引用,即表示该ThreadLocal对象可以被回收了。又因为Entry中key为ThreadLocal对象的弱引用,所以当JVM执行GC操作时是能够回收该ThreadLocal对象的。

Entry中value对应的是变量实体对象的强引用,因此释放一个ThreadLocal对象,是无法释放ThreadLocal.ThreadLocalMap中对应的value对象的,也就造成了内存泄漏。除非释放当前线程对象,这样整个threadlocals都被回收了。但是日常开发中会经常使用线程池等线程池化技术,释放线程对象的条件往往无法达到。

jdk处理的方法是,在ThreadLocalMap进行set()get()remove()的时候,都会进行清理:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            //如果key为null,对应的threadlocal对象已经被回收,清理该Entry
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

2 自定义上下文Scope

在工作中,我们经常需要维护一些上下文,这样可以避免在方法调用过程中传入过多的参数,需要查询/修改一些数据的时候,直接在当前上下文中操作就行了。举个具体点的例子:当WEB服务器收到一个请求时,需要解析当前登录态的用户,在后续的业务执行流程中都需要这个用户名。

如果只需要维护一个上下文状态数据还比较好处理,可以通过方法传参的形式,执行每个业务方法的时候都通过添加一个表示用户名方法参数传递进去,但是如果需要维护上下文状态数据比较多的话,这个方式就不太优雅了。

一个可行的方案是通过Threadlocal实现请求线程的上下文,只要是同一线程的执行过程,不同方法间不传递上下文状态变量,直接操作ThreadLocal对象实现状态数据的读写。当存在多个上下文状态的话,则需要维护多个ThreadLocal,似乎也可以勉强接受。但是当遇到业务流程中使用线程池的情况下,从Tomcat传递这些ThreadLocal到线程池中的线程里就变的比较麻烦了。

基于以上考虑,下面介绍一种基于Threadlocal实现的上下文管理组件Scope

Scope.java

public class Scope {

    // 静态变量,维护不同线程的上下文Scope
    private static final ThreadLocal<Scope> SCOPE_THREAD_LOCAL = new ThreadLocal<>();

    // 实例变量,维护每个上下文中所有的状态数据,为了区分不同的状态数据,使用ScopeKey类型的实例作为key
    private final ConcurrentMap<ScopeKey<?>, Object> values = new ConcurrentHashMap<>();

    // 获取当前上下文
    public static Scope getCurrentScope() {
        return SCOPE_THREAD_LOCAL.get();
    }

    // 在当前上下文设置一个状态数据
    public <T> void set(ScopeKey<T> key, T value) {
        if (value != null) {
            values.put(key, value);
        } else {
            values.remove(key);
        }
    }

    // 在当前上下文读取一个状态数据
    public <T> T get(ScopeKey<T> key) {
        T value = (T) values.get(key);
        if (value == null && key.initializer() != null) {
            value = key.initializer().get();
        }
        return value;
    }

    // 开启一个上下文
    public static Scope beginScope() {
        Scope scope = SCOPE_THREAD_LOCAL.get();
        if (scope != null) {
            throw new IllegalStateException("start a scope in an exist scope.");
        }
        scope = new Scope();
        SCOPE_THREAD_LOCAL.set(scope);
        return scope;
    }

    // 关闭当前上下文
    public static void endScope() {
        SCOPE_THREAD_LOCAL.remove();
    }
}

ScopeKey.java

public final class ScopeKey<T> {

    // 初始化器,参考 ThreadLocal 的 withInitial()
    private final Supplier<T> initializer;

    public ScopeKey() {
        this(null);
    }

    public ScopeKey(Supplier<T> initializer) {
        this.initializer = initializer;
    }

    // 统一初始化所有线程的 ScopeKey 对应的值,参考 ThreadLocal 的 withInitial()
    public static <T> ScopeKey<T> withInitial(Supplier<T> initializer) {
        return new ScopeKey<>(initializer);
    }

    public Supplier<T> initializer() {
        return this.initializer;
    }

    // 获取当前上下文中 ScopeKey 对应的变量
    public T get() {
        Scope currentScope = getCurrentScope();
        return currentScope.get(this);
    }

    // 设置当前上下文中 ScopeKey 对应的变量
    public boolean set(T value) {
        Scope currentScope = getCurrentScope();
        if (currentScope != null) {
            currentScope.set(this, value);
            return true;
        } else {
            return false;
        }
    }
}

使用方式

@Test
public void testScopeKey() {
    ScopeKey<String> localThreadName = new ScopeKey<>();

    // 不同线程中执行时,开启独占的 Scope
    Runnable r = () -> {
        // 开启 Scope
        Scope.beginScope();
        try {
            String currentThreadName = Thread.currentThread().getName();
            localThreadName.set(currentThreadName);
            log.info("currentThread: {}", localThreadName.get());
        } finally {
            // 关闭 Scope
            Scope.endScope();
        }
    };

    new Thread(r, "thread-1").start();
    new Thread(r, "thread-2").start();

    
}

@Test
public void testWithInitial() {
    ScopeKey<String> initValue = ScopeKey.withInitial(() -> "initVal");

    Runnable r = () -> {
        Scope.beginScope();
        try {
            log.info("initValue: {}", initValue.get());
        } finally {
            Scope.endScope();
        }
    };

    new Thread(r, "thread-1").start();
    new Thread(r, "thread-2").start();

    
}

上面的测试用例中在代码中手动开启和关闭Scope不太优雅,可以在Scope中添加两个个静态方法包装下RunnableSupplier接口:

public static <X extends Throwable> void runWithNewScope(@Nonnull ThrowableRunnable<X> runnable)
        throws X {
    supplyWithNewScope(() -> {
        runnable.run();
        return null;
    });
}

public static <T, X extends Throwable> T
        supplyWithNewScope(@Nonnull ThrowableSupplier<T, X> supplier) throws X {
    beginScope();
    try {
        return supplier.get();
    } finally {
        endScope();
    }
}
@FunctionalInterface
public interface ThrowableRunnable<X extends Throwable> {
    void run() throws X;
}

public interface ThrowableSupplier<T, X extends Throwable> {
    T get() throws X;
}

以新的Scope执行,可以这样写:

@Test
public void testRunWithNewScope() {
    ScopeKey<String> localThreadName = new ScopeKey<>();

    ThrowableRunnable r = () -> {
        String currentThreadName = Thread.currentThread().getName();
        localThreadName.set(currentThreadName);
        log.info("currentThread: {}", localThreadName.get());
    };

    // 不同线程中执行时,开启独占的 Scope
    new Thread(() -> Scope.runWithNewScope(r), "thread-1").start();
    new Thread(() -> Scope.runWithNewScope(r), "thread-2").start();

    
}

3 在线程池中传递Scope

在上一节中实现的Scope,通过ThreadLocal实现了了一个自定义的上下文组件,在同一个线程中通过ScopeKey.set() / ScopeKey.get()读写同一个上下文中的状态数据。

现在需要实现这样一个功能,在一个线程执行过程中开启了一个Scope,随后使用线程池执行任务,要求在线程池中也能获取当前Scope中的状态数据。典型的使用场景是:服务收到一个用户请求,通过Scope将登陆态数据存到当前线程的上下文中,随后使用线程池执行一些耗时的操作,希望线程池中的线程也能拿到Scope中的登陆态数据。

由于线程池中的线程和请求线程不是一个线程,按照目前的实现,线程池中的线程是无法拿到请求线程上下文中的数据的。

解决方法是,在提交runnable时,将当前的Scope引用存到runnable对象中,当获得线程执行时,将Scope替换到执行线程中,执行完成后,再恢复现场。在Scope中新增如下静态方法:

// 以给定的上下文执行 Runnable
public static <X extends Throwable> void runWithExistScope(Scope scope, ThrowableRunnable<X> runnable) throws X {
    supplyWithExistScope(scope, () -> {
        runnable.run();
        return null;
    });
}

// 以给定的上下文执行 Supplier
public static <T, X extends Throwable> T supplyWithExistScope(Scope scope, ThrowableSupplier<T, X> supplier) throws X {
    // 保留现场
    Scope oldScope = SCOPE_THREAD_LOCAL.get();
    // 替换成外部传入的 Scope
    SCOPE_THREAD_LOCAL.set(scope);
    try {
        return supplier.get();
    } finally {
        if (oldScope != null) {
            // 恢复线程
            SCOPE_THREAD_LOCAL.set(oldScope);
        } else {
            SCOPE_THREAD_LOCAL.remove();
        }
    }
}

实现支持Scope切换的自定义线程池ScopeThreadPoolExecutor

public class ScopeThreadPoolExecutor extends ThreadPoolExecutor {

    ScopeThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                            TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public static ScopeThreadPoolExecutor newFixedThreadPool(int nThreads) {
        return new ScopeThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    }

    
    @Override
    public void execute(Runnable command) {
        Scope scope = getCurrentScope();
        // 提交任务时,把执行 execute 方法的线程中的 Scope 传进去
        super.execute(() -> runWithExistScope(scope, command::run));
    }
}

测试下ScopeThreadPoolExecutor是否生效:

@Test
public void testScopeThreadPoolExecutor() {
    ScopeKey<String> localVariable = new ScopeKey<>();
    Scope.beginScope();

    try {
        localVariable.set("value out of thread pool");
        Runnable r = () -> log.info("localVariable in thread pool: {}", localVariable.get());

        // 使用线程池执行,能获取到外部Scope中的数据
        ExecutorService executor = ScopeThreadPoolExecutor.newFixedThreadPool(10);
        executor.execute(r);
        executor.submit(r);

    } finally {
        Scope.endScope();
    }

    
}

@Test
public void testScopeThreadPoolExecutor2() {
    ScopeKey<String> localVariable = new ScopeKey<>();
    Scope.runWithNewScope(() -> {
        localVariable.set("value out of thread pool");
        Runnable r = () -> log.info("localVariable in thread pool: {}", localVariable.get());

        // 使用线程池执行,能获取到外部Scope中的数据
        ExecutorService executor = ScopeThreadPoolExecutor.newFixedThreadPool(10);
        executor.execute(r);
        executor.submit(r);
    });

    
}

以上两个测试用例,分别通过手动开启Scope、借助runWithNewScope工具方法自动开启Scope两种方式验证了自定义线程池ScopeThreadPoolExecutorScope可传递性。

4 通过Filter、Scope实现Request上下文

接下来介绍如何通过FilterScope实现Request粒度的Scope上下文。思路是:通过注入一个拦截器,在进入拦截器后开启Scope作为一个请求的上下文,解析Request对象获取获取相关状态信息(如登陆用户),并在Scope中设置,在离开拦截器时关闭Scope

AuthScope.java

// 获取登录态的工具类
public class AuthScope {
    private static final ScopeKey<String> LOGIN_USER = new ScopeKey<>();

    public static String getLoginUser() {
        return LOGIN_USER.get();
    }

    public static void setLoginUser(String loginUser) {
        if (loginUser == null) {
            loginUser = "unknownUser";
        }
        LOGIN_USER.set(loginUser);
    }
}

ScopeFilter.java

@Lazy
@Order(0)
@Service("scopeFilter")
public class ScopeFilter extends OncePerRequestFilter {

    @Override
    protected String getAlreadyFilteredAttributeName() {
        return this.getClass().getName();
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 开启Scope
        beginScope();
        try {
            Cookie[] cookies = request.getCookies();
            String loginUser = "unknownUser";
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals("login_user")) {
                        loginUser = cookie.getValue();
                        break;
                    }
                }
            }

            // 设置该 Request 上下文对用的登陆用户
            AuthScope.setLoginUser(loginUser);

            filterChain.doFilter(request, response);
        } finally {
            // 关闭Scope
            endScope();
        }
    }
}

注入Filter

@Slf4j
@Configuration
public class FilterConfig {

    @Bean
    public FilterReGIStrationBean<ScopeFilter> scopeFilterRegistration() {
        FilterRegistrationBean<ScopeFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new ScopeFilter());
        registration.addUrlPatterns("/rest/*");
        registration.setOrder(0);
        log.info("scope filter registered");
        return registration;
    }
}

UserController.java

@Slf4j
@RestController
@RequestMapping("/rest")
public class UserController {

    // curl --location --request GET 'localhost:8080/rest/getLoginUser' --header 'Cookie: login_user=zhangsan'
    @GetMapping("/getLoginUser")
    public String getLoginUser() {
        return AuthScope.getLoginUser();
    }

    // curl --location --request GET 'localhost:8080/rest/getLoginUserInThreadPool' --header 'Cookie: login_user=zhangsan'
    @GetMapping("/getLoginUserInThreadPool")
    public String getLoginUserInThreadPool() {
        ScopeThreadPoolExecutor executor = ScopeThreadPoolExecutor.newFixedThreadPool(4);
        executor.execute(() -> {
            log.info("get login user in thread pool: {}", AuthScope.getLoginUser());
        });

        return AuthScope.getLoginUser();
    }
}

通过以下请求验证,请求线程和线程池线程是否能获取登录态,其中登录态通过Cookie模拟:

curl --location --request GET 'localhost:8080/rest/getLoginUser' --header 'Cookie: login_user=zhangsan'
curl --location --request GET 'localhost:8080/rest/getLoginUserInThreadPool' --header 'Cookie: login_user=zhangsan'

5 总结

源代码

github:https://github.com/pengchengSU/demo-request-scope

以上就是利用ThreadLocal实现一个上下文管理组件的详细内容,更多关于ThreadLocal上下文管理组件的资料请关注编程网其它相关文章!

--结束END--

本文标题: 利用ThreadLocal实现一个上下文管理组件

本文链接: https://lsjlt.com/news/169309.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 利用ThreadLocal实现一个上下文管理组件
    目录1 ThreadLocal原理set() 方法get() 方法withInitial()方法ThreadLocal中的内存泄漏问题2 自定义上下文Scope3 在线程池中传递Sc...
    99+
    2022-11-13
    ThreadLocal上下文管理组件 ThreadLocal上下文管理
  • Python利用contextvars实现管理上下文变量
    目录web 框架中的 requestThreadLocalcontextvarsc.Tokencontextvars.Context小结Python 在 3.7 的时候引入了一个模块...
    99+
    2024-04-02
  • 利用servlet怎么实现一个文件上传下载功能
    利用servlet怎么实现一个文件上传下载功能?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。一、准备工作:  1.1 文件上传插件:uploadify;  1.2 文件上传所需...
    99+
    2023-05-31
    servlet 一个文件
  • 利用SpringMVC如何实现一个文件上传下载功能
    这篇文章给大家介绍利用SpringMVC如何实现一个文件上传下载功能,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。文件上传文件上传是项目开发中最常见的功能。为了能上传文件,必须将表单的method设置为POST,并将e...
    99+
    2023-05-31
    springmvc 文件上传下载
  • Python怎么利用contextvars实现管理上下文变量
    本篇文章给大家带来了关于Python的相关知识,Python 在 3.7 的时候引入了一个模块:contextvars,从名字上很容易看出它指的是上下文变量,下面就来和大家详细讲讲如何使用contextvars实现管理上下文变量,希望对大家...
    99+
    2022-08-08
    python
  • 利用hadoop怎么实现一个文件上传功能
    本篇文章给大家分享的是有关利用hadoop怎么实现一个文件上传功能,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。hdfs上的文件是手动执行命令从本地linux上传至hdfs的。...
    99+
    2023-05-31
    hadoop
  • 利用servlet怎么实现一个文件上传功能
    本篇文章为大家展示了利用servlet怎么实现一个文件上传功能,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。1.servlet上传文件  servlet上传文件就是将客户端的文件上传到服务器端。  ...
    99+
    2023-05-31
    servlet 一个文件
  • 利用Struts怎么实现一个文件上传功能
    这期内容当中小编将会给大家带来有关利用Struts怎么实现一个文件上传功能,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。1.在jsp文件中进行定义<form action="/Struts...
    99+
    2023-05-31
    struts st
  • 利用javaweb如何实现一个文件上传功能
    利用javaweb如何实现一个文件上传功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。具体内容如下文件上传示例注意:jsp页面编码为"UTF-8"文件...
    99+
    2023-05-31
    javaweb 文件上传 ava
  • 利用Servlet怎么实现一个文件下载功能
    本篇文章给大家分享的是有关利用Servlet怎么实现一个文件下载功能,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Web容器为Servlet生成并且传递的HttpServlet...
    99+
    2023-05-31
    servlet 一个文件
  • 怎么在JAVA中利用HttpURLConnection实现一个文件上传下载功能
    怎么在JAVA中利用HttpURLConnection实现一个文件上传下载功能?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。HttpURLConnection文...
    99+
    2023-05-31
    java httpurlconnection ava
  • 利用Spring MVC怎么实现一个文件上传功能
    本篇文章给大家分享的是有关利用Spring MVC怎么实现一个文件上传功能,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。使用Spring MVC 上传文件必须配置文件解析器,如...
    99+
    2023-05-31
    springmvc 一个文件
  • 根据python上下文管理,写一个在读文
    利用上下文管理实现读f文件前后自动打开关闭文件#在本目录创建f文件,内容写monkey代码如下 1 import contextlib #导入模块1 2 @contextlib.contextmanager#装饰器2 3 de...
    99+
    2023-01-30
    上下文 python
  • 如何在Java项目中利用FasfDFS实现一个文件上传下载功能
    本篇文章给大家分享的是有关如何在Java项目中利用FasfDFS实现一个文件上传下载功能,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。一 : 添加配置文件当完成以上操作之后可以...
    99+
    2023-05-31
    fasfdfs java ava
  • nodejs+express实现文件上传下载管理网站
    nodejs+express-实现文件上传下载管理的网站 项目Github地址:https://github.com/qcer/updo 后端:基于nodejs的express的web框架. 前端:boot...
    99+
    2022-06-04
    上传下载 文件 管理网站
  • nodejs中怎么利用express实现一个文件上传功能
    这期内容当中小编将会给大家带来有关nodejs中怎么利用express实现一个文件上传功能,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。步骤:(1)使用express创建...
    99+
    2024-04-02
  • Vue.js中怎么实现一个上下滚动加载组件
    今天就跟大家聊聊有关Vue.js中怎么实现一个上下滚动加载组件,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。组件代码// scrollLo...
    99+
    2024-04-02
  • 使用Socket+JDBC实现一个Java文件上传下载功能
    这期内容当中小编将会给大家带来有关使用Socket+JDBC实现一个Java文件上传下载功能,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。该demo实现的功能有:用户注册:注册时输入两次密码,若两次输入不...
    99+
    2023-05-31
    socket jdbc ava
  • 如何在Java中利用FileUpload实现一个文件上传功能
    如何在Java中利用FileUpload实现一个文件上传功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。具体方法如下://创建磁盘文件项工厂 DiskFile...
    99+
    2023-05-31
    java fileupload ava
  • 如何在Java中利用COS实现一个文件上传功能
    如何在Java中利用COS实现一个文件上传功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。cos是O'Rrilly公司开发的一款用于HTTP上传文件的OpenSo...
    99+
    2023-05-31
    cos java ava
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作