基于 ThreadLocal 实现一个上下文管理组件(附源码)

点击关注公众号,Java干货及时送达👇

d0b87f3527eb1189c29517014f420268.png

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

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

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

1 ThreadLocal原理

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

03132aa85d73a0053179dbf3efb0212b.jpeg图1

从上图可知:

  1. 每个Thread对象中都包含一个ThreadLocal.ThreadLocalMap类型的threadlocals成员变量;

  2. 该map对应的每个元素Entry对象中:key是ThreadLocal对象的弱引用,value是该threadlocal变量在当前线程中的对应的变量实体;

  3. 当某一线程执行获取该ThreadLocal对象对应的变量时,首先从当前线程对象中获取对应的threadlocals哈希表,再以该ThreadLocal对象为key查询哈希表中对应的value;

  4. 由于每个线程独占一个threadlocals哈希表,因此线程间ThreadLocal对象对应的变量实体也是独占的,不存在竞争问题,也就避免了多线程问题。

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

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

65ec910b7205d324c5f2a2568de87986.jpeg
图片
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);}@Overrideprotected 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;elsereturn 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对象已经被回收,清理该EntryexpungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;
}

2 自定义上下文Scope

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

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

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

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

Scope.java

public class Scope {// 静态变量,维护不同线程的上下文Scopeprivate static final ThreadLocal<Scope> SCOPE_THREAD_LOCAL = new ThreadLocal<>();// 实例变量,维护每个上下文中所有的状态数据,为了区分不同的状态数据,使用ScopeKey类型的实例作为keyprivate 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<>();// 不同线程中执行时,开启独占的 ScopeRunnable r = () -> {// 开启 ScopeScope.beginScope();try {String currentThreadName = Thread.currentThread().getName();localThreadName.set(currentThreadName);log.info("currentThread: {}", localThreadName.get());} finally {// 关闭 ScopeScope.endScope();}};new Thread(r, "thread-1").start();new Thread(r, "thread-2").start();/** 执行结果* [thread-1] INFO com.example.demo.testscope.TestScope - currentThread: thread-1* [thread-2] INFO com.example.demo.testscope.TestScope - currentThread: thread-2*/
}@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();/** 执行结果* [thread-1] INFO com.example.demo.testscope.TestScope - initValue: initVal* [thread-2] INFO com.example.demo.testscope.TestScope - initValue: initVal*/
}

上面的测试用例中在代码中手动开启和关闭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> TsupplyWithNewScope(@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());};// 不同线程中执行时,开启独占的 Scopenew Thread(() -> Scope.runWithNewScope(r), "thread-1").start();new Thread(() -> Scope.runWithNewScope(r), "thread-2").start();/** 执行结果* [thread-2] INFO com.example.demo.TestScope.testscope - currentThread: thread-2* [thread-1] INFO com.example.demo.TestScope.testscope - currentThread: thread-1*/
}

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();// 替换成外部传入的 ScopeSCOPE_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这一个方法就可以* 所有submit, invokeAll等方法都会代理到这里来*/@Overridepublic 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();}/** 执行结果* [pool-1-thread-1] INFO com.example.demo.testscope.TestScope - localVariable in thread pool: value out of thread pool* [pool-1-thread-2] INFO com.example.demo.testscope.TestScope - localVariable in thread pool: value out of thread pool*/
}@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);});/** 执行结果* [pool-1-thread-2] INFO com.example.demo.testscope.TestScope - localVariable in thread pool: value out of thread pool* [pool-1-thread-1] INFO com.example.demo.testscope.TestScope - localVariable in thread pool: value out of thread pool*/
}

以上两个测试用例,分别通过手动开启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 {@Overrideprotected String getAlreadyFilteredAttributeName() {return this.getClass().getName();}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {// 开启ScopebeginScope();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 {// 关闭ScopeendScope();}}
}

注入Filter

@Slf4j
@Configuration
public class FilterConfig {@Beanpublic 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.git

感谢阅读,希望对你有所帮助 :)

来源:juejin.cn/post/7153287656624324638

 
 

热门内容:

因ChatGPT爆红的大数据培养计划|拿不到年薪25W全额退款

用上这几个开源管理系统做项目,领导看了直呼专业!

ChatGPT 连夜迭代:你老婆不‍好使了

动态可监控线程池,你还没用起来吗?

 

43dad08b98ce83c8b9e0877dcc48cf41.jpeg

 
最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

个人专属chatGpt免费使用,只需要2步

第一步&#xff1a;先下载Docker 下载网址&#xff1a;docker.com 选择自己合适的环境下载 安装&#xff1a; 等一丢丢会就可以了 我这里已经装过了&#xff0c;第一次装的话提示信息是&#xff1a;close and restart 需要重新下电脑 重启完电脑后就会自动跳出这个界面&#…

社群运营及新媒体运营常用工具清单列表,推荐收藏

做运营3年收集的运营人必备工具&#xff0c;推荐你收藏 每一个运营人都是积极向上且好学的&#xff0c;随着互联网的发展&#xff0c;作为一名运营要学习的东西也很多&#xff0c;除了要会文案外&#xff0c;美工设计的活有时也是你的&#xff0c;会脚本&#xff0c;会视频剪辑…

新媒体营销渠道大盘点

做新媒体营销一定要做的就是微信公众账号和微博&#xff0c;不管适合不适合&#xff0c;反正就是要做。其实有些时候未必都适合自己。所以选择适合自己的渠道才是最重要的。首先我们来看看都有哪些是新媒体营销的渠道。第一阵营微信公众账号和微博&#xff0c;相对于微信来说&a…

新媒体运营必备的实用工具

一、小葫芦 主要做和直播相关的数据分析&#xff0c;如 抖音、快手、 斗鱼、虎牙等主流直播平台 的收入榜、弹幕榜、 涨粉榜、 点赞榜、土豪榜&#xff0c;可以免费查询到的榜单数量比较多 对于一般用户够用了&#xff0c;另外小葫芦还提供一些和直播相关的小工具&#xff0c;…

品牌如何利用Instagram网红营销做好2023斋月推广?

在当今社交媒体的时代&#xff0c;Instagram已成为品牌进行营销和推广的重要平台之一&#xff0c;而且是最受穆斯林欢迎的社交媒体之一&#xff0c;超过60%的穆斯林用户会在Instagram上搜索和关注与斋月有关的话题和品牌。根据Instagram的数据&#xff0c;斋月期间&#xff0c;…

【独立站运营】用Facebook做社媒营销的关键点,你掌握了吗?

Facebook 是全球营销人员最常用的社交媒体平台。无论您的营销目标是什么&#xff0c;Facebook 都需要成为您战略的一部分。 不仅要学习如何创建 Facebook主页&#xff0c;还要学习如何有效地使用它来推广您的品牌&#xff0c;这一点至关重要。我们整理了设置和自定义专业 Face…

「品牌案例分享」-WhatsApp营销势成主流与更多用户交流互动

从电子邮件营销和付费广告到更传统的电视广告&#xff0c;企业有很多方法可以提高品牌知名度、获得新客户和让品牌壮大。然而&#xff0c;通过新颖和有创意的方式接触顾客才是挑战所在。 WhatsApp 是一种相当新的营销工具。由于WhatsApp 在全球拥有超过20 亿活跃用户&#xff0…

新闻个性化推荐系统

新闻个性化推荐系统 一、绪论及背景1.1、绪论1.2、背景1.3、发展历史 二、需求分析2.1、功能需求2.1.1、用户功能需求2.1.2、运营功能需求2.1.3、算法功能需求 2.2、非功能需求2.2.1、性能需求2.2.2、准确性需求2.2.3、稳定性需求2.2.4、可靠性需求 三、详细设计3.1、系统结构设…

【WhatsApp营销】如何将WhatsApp与品牌的社交媒体营销相结合

用多样的角色讲故事&#xff0c;设计吸引人的邮件&#xff0c;用令人惊叹的照片吸引眼球——营销人员已经全都做过。如今的社交媒体已经发展成为一个多面兽&#xff0c;各种形式和规模的营销策略和活动也随之出现&#xff0c;可以说营销人员已经尝试了各种可能的方式。 虽然说…

收藏级!运营人必备2021全年热点营销日历

一份热点全面的营销日历&#xff0c;可以帮我们更有效地制定年度营销计划、把握全年运营节奏。 以下就是小M制作的2021年度热点营销日历&#xff0c;包括通用节日节气、重大纪念日、互联网重要节日等上百个营销节点。 可以直接免费获取▼▼▼ 运营人必备&#xff01;2021热点…

App出海:如何制定社媒营销策略?

如今&#xff0c;国内互联网已进入存量时代&#xff0c;而海外市场一片蓝海&#xff0c;发展前景喜人&#xff0c;加上国内政策对出海的大力支持&#xff0c;出海成为了不少企业寻找增量的不二之选。根据Nox聚星&#xff08;NoxInfluencer&#xff09;调研数据&#xff0c;2022…

介绍几个主流社媒平台的特点,以及如何通过这些社媒渠道开展营销活动

介绍几个主流社媒平台的特点&#xff0c;以及如何通过这些社媒渠道开展营销活动 红人营销作为品牌营销的主要方式之一&#xff0c;它给品牌带来的流量转化是巨大的&#xff0c;之前的文章有讲述过红人的营销的主要操作是什么&#xff0c;也提到了每个社媒平台有自己的营销逻辑…

2023年适合营销公司使用的十大「社交媒体管理」工具

在遍地都是数字营销公司的时代&#xff0c;对品牌来说&#xff0c;拥有强大的社交媒体影响力以保持竞争力从未如此重要。 而对于管理一个或多个品牌的数字营销公司来说&#xff0c;从内容创作到执行报告&#xff0c;使用正确的工具可以帮助你做到这一点。从规划、管理和跟踪社…

ChatGPT 1.0.0安卓逆向分析,仅限中国分享

ChatGPT 1.0.0安卓逆向分析&#xff0c;仅限中国分享 本文将对ChatGpt Android版本1.0.0 APK进行静态解包分析和抓包分析&#xff0c;从ChatGpt Android APK功能的设计、校验过程和代码内包含的信息来预测OpenAI的大致走向。 Sensor Tower指出&#xff0c;在美国市场&#xff…

PC端微信网页打不开

首先&#xff0c;写这篇博客就是想吐槽一下。 我只是想看一片技术博文&#xff0c;手机上显示不太友好&#xff0c;有些代码没有超出边界被隐藏起来&#xff0c;想着在PC端应该更友好一些&#xff0c;结果在PC端打不开&#xff0c;不显示内容&#xff0c;然后浪费了一个下午的时…

无法打开网页但可以登录电脑微信 解决办法

今天遇到了电脑微信可以登录但网页打不开的情况&#xff0c;原因可能是DNS 配置出现问题。DNS&#xff08;Domain Name System&#xff0c;域名系统&#xff09;可以简单的理解成根据域名解析成对应IP的解码服务器。 解决办法 1. 打开控制面板 2. 选择网络和Internet - 网络和…

解决网络连接正常,网页打不开的问题

win10系统 1.打开‘控制面板-’->点击‘网络和Internet’ 2.点击‘Internet 选项’ 3.在弹出的‘Internet’属性窗口选择上方的‘连接’选项栏-->点击下方的‘局域网设置’ 4.在弹出的‘局域网&#xff08;LAN&#xff09;设置’窗口的‘代理服务器’部分&#xff0c;取…

网站浏览器可以打开,在微信中打不开,排查问题的过程

今天收到遇到一个诡异的问题&#xff1a;网站浏览器可以打开&#xff0c;在微信中打不开&#xff0c;耗费了很长时间才将问题排查出来&#xff0c;现在将排查的步骤记录下来&#xff0c;仅供参考&#xff1a; 在手机微信里输入访问地址&#xff1a;显示无法 打开网页。如图所示…

关于网络很快 但是打不开腾讯相关网页的解决办法 亲测有效!

刷新见效果 刷新见效果 刷新见效果 重要的事情要提三遍 近日 发现我的电脑打不开电脑版微信里面的网页内容&#xff0c;进而发现连腾讯的微信公众号的主页也打不开&#xff0c;经过网上的查询和大量的尝试&#xff0c;终于发现解决办法&#xff0c;立竿见影&#xff1a; 打…

【亲测有效】解决部分网页打不开的方法(特别是CSDN),电脑浏览器突然打不开某个网页,其他网页正常使用解决方法

问题描述 电脑可以正常上网使用&#xff0c;在确保浏览器清理了缓存之后&#xff0c;电脑始终打不开Gitee和CSDN。点击CSDN浏览器载入网页后一直处于加载状态&#xff0c;始终无法响应加载出页面&#xff0c;导致超时&#xff0c;加载失败。 使用Google浏览器打开CSDN&#x…