草庐IT

多线程异步方法Spring Security框架的SecurityContext无法获取认证信息的原因及解决方案

xiaomifeng1010 2023-12-22 原文

      Spring Security是Spring生态提供的用户应用安全保护的一个安全框架,其提供了一种高度可定制的实现身份认证(Authentication),授权(Authorization)以及对常见的web攻击手段做防护的方法。

      之前我的博客Oauth2与Spring Security框架的认证授权管理讲到过,使用Spring Security结合Oauth2进行身份认证,以及授权集成到项目的步骤。

    在集成成功后,每次接口的请求,都会在请求头中携带Authrization的请求头,携带access-token信息,然后在项目中使用SecutityContext对象就可以获取到用户身份信息。

    一般在项目中会创建一个工具类,用来获取用户信息,其中就是使用的SecutityContext进行封装的工具类。

     比如我在项目中创建了一个UserUtil工具类:

package com.dcboot.module.visit.system.util;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.dcboot.base.config.security.support.MyUserDetails;
import com.dcboot.module.common.enums.system.UserLevelEnum;
import com.dcboot.module.common.model.system.UserLevelVo;
import com.dcboot.module.system.dept.entity.Dept;
import com.dcboot.module.system.dept.service.DeptService;
import com.dcboot.module.system.deptusermid.entity.DeptUserMid;
import com.dcboot.module.system.deptusermid.service.DeptUserMidService;
import com.dcboot.module.system.user.entity.User;
import com.dcboot.module.system.user.service.UserService;
import com.dcboot.module.visit.system.service.BusUserService;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 2022/3/5 15:07
 * @Description
 */
@Component
@RequiredArgsConstructor
public class UserUtil {
    private final UserService userService;
    private final DeptUserMidService deptUserMidService;
    private final DeptService deptService;


    List<Long> deptIdList=new ArrayList<>();

    /**
     * @description: 获取当前用户账号
     * @author: xiaomifeng1010
     * @date: 2022/3/5
     * @param
     * @return: String
     **/
    public String getUserAccount(){
        MyUserDetails userDetails = (MyUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String userAccount = userDetails.getUsername();
        return userAccount;
    }

    /**
     * @description: 获取userId
     * @author:xiaomifeng1010
     * @date: 2022/4/11
     * @param
     * @return: Long
     **/
    public Long getUserId(){
        String userAccount = getUserAccount();
        return userService.getObj(Wrappers.<User> lambdaQuery()
                .select(User ::getId).eq(User ::getUserAccount,userAccount ),a -> Long.valueOf(String.valueOf(a)));
    }

    public User getUserInfo(){
        String userAccount = getUserAccount();
        return userService.getOne(Wrappers.<User> lambdaQuery()
                .eq(User ::getUserAccount,userAccount ));
    }

    /**
     * @description: 获取当前用户的主部门id
     * @author:xiaomifeng1010
     * @date: 2022/4/11
     * @param
     * @return: Long
     **/
    public Long getMasterDeptId(){
        Long userId = getUserId();
        return deptUserMidService.getObj(Wrappers.<DeptUserMid> lambdaQuery()
        .select(DeptUserMid::getDeptid).eq(DeptUserMid::getUserid,userId).eq(DeptUserMid::getIsmaster,1),
                a ->Long.valueOf(String.valueOf(a)));

    }


    public String getDeptCode(){
        Long masterDeptId = getMasterDeptId();
        Dept dept = deptService.getById(masterDeptId);
        return Objects.nonNull(dept) ? dept.getDeptcode() : StringUtils.EMPTY;
    }


    /**
     * @description: 获取所在主部门以及子级部门
     * @author: xiaomifeng1010
     * @date: 2022/4/11
     * @param deptId 主部门id
     * @return: List<Long>
     **/
    public List<Long> getDeptIdList(Long deptId){
        deptIdList.add(deptId);
        Long childDeptId = deptService.getObj(Wrappers.<Dept>lambdaQuery()
                .select(Dept::getId)
                .eq(Dept::getParentid, deptId), a -> Long.valueOf(a.toString()));
        if (Objects.nonNull(childDeptId)){
            getDeptIdList(childDeptId);

        }

        return deptIdList;

    }
 
}

Spring Security的安全上下文是由SecurityContext接口描述的。

/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.core.context;

import org.springframework.security.core.Authentication;

import java.io.Serializable;

/**
 * Interface defining the minimum security information associated with the current thread
 * of execution.
 *
 * <p>
 * The security context is stored in a {@link SecurityContextHolder}.
 * </p>
 *
 * @author Ben Alex
 */
public interface SecurityContext extends Serializable {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Obtains the currently authenticated principal, or an authentication request token.
	 *
	 * @return the <code>Authentication</code> or <code>null</code> if no authentication
	 * information is available
	 */
	Authentication getAuthentication();

	/**
	 * Changes the currently authenticated principal, or removes the authentication
	 * information.
	 *
	 * @param authentication the new <code>Authentication</code> token, or
	 * <code>null</code> if no further authentication information should be stored
	 */
	void setAuthentication(Authentication authentication);
}

从源码中可以看出这个接口的主要职责就是存储身份认证对象和获取身份认证对象的,那么SecurityContext本身是如何被管理的呢?Spring Security框架提供了3种策略来管理SecurityContext.管理该类的对象是SecurityContextHolder.

这3种策略也定义在了SecurityContextHolder类中:

/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.core.context;

import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Constructor;

/**
 * Associates a given {@link SecurityContext} with the current execution thread.
 * <p>
 * This class provides a series of static methods that delegate to an instance of
 * {@link org.springframework.security.core.context.SecurityContextHolderStrategy}. The
 * purpose of the class is to provide a convenient way to specify the strategy that should
 * be used for a given JVM. This is a JVM-wide setting, since everything in this class is
 * <code>static</code> to facilitate ease of use in calling code.
 * <p>
 * To specify which strategy should be used, you must provide a mode setting. A mode
 * setting is one of the three valid <code>MODE_</code> settings defined as
 * <code>static final</code> fields, or a fully qualified classname to a concrete
 * implementation of
 * {@link org.springframework.security.core.context.SecurityContextHolderStrategy} that
 * provides a public no-argument constructor.
 * <p>
 * There are two ways to specify the desired strategy mode <code>String</code>. The first
 * is to specify it via the system property keyed on {@link #SYSTEM_PROPERTY}. The second
 * is to call {@link #setStrategyName(String)} before using the class. If neither approach
 * is used, the class will default to using {@link #MODE_THREADLOCAL}, which is backwards
 * compatible, has fewer JVM incompatibilities and is appropriate on servers (whereas
 * {@link #MODE_GLOBAL} is definitely inappropriate for server use).
 *
 * @author Ben Alex
 *
 */
public class SecurityContextHolder {
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	public static final String MODE_GLOBAL = "MODE_GLOBAL";
	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
	private static SecurityContextHolderStrategy strategy;
	private static int initializeCount = 0;

	static {
		initialize();
	}

	// ~ Methods
	// ========================================================================================================

	/**
	 * Explicitly clears the context value from the current thread.
	 */
	public static void clearContext() {
		strategy.clearContext();
	}

	/**
	 * Obtain the current <code>SecurityContext</code>.
	 *
	 * @return the security context (never <code>null</code>)
	 */
	public static SecurityContext getContext() {
		return strategy.getContext();
	}

	/**
	 * Primarily for troubleshooting purposes, this method shows how many times the class
	 * has re-initialized its <code>SecurityContextHolderStrategy</code>.
	 *
	 * @return the count (should be one unless you've called
	 * {@link #setStrategyName(String)} to switch to an alternate strategy.
	 */
	public static int getInitializeCount() {
		return initializeCount;
	}

	private static void initialize() {
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}

		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
		}
		else if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
		}
		else {
			// Try to load a custom strategy
			try {
				Class<?> clazz = Class.forName(strategyName);
				Constructor<?> customStrategy = clazz.getConstructor();
				strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
			}
			catch (Exception ex) {
				ReflectionUtils.handleReflectionException(ex);
			}
		}

		initializeCount++;
	}

	/**
	 * Associates a new <code>SecurityContext</code> with the current thread of execution.
	 *
	 * @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
	 */
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

	/**
	 * Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
	 * a given JVM, as it will re-initialize the strategy and adversely affect any
	 * existing threads using the old strategy.
	 *
	 * @param strategyName the fully qualified class name of the strategy that should be
	 * used.
	 */
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}

	/**
	 * Allows retrieval of the context strategy. See SEC-1188.
	 *
	 * @return the configured strategy for storing the security context.
	 */
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
		return strategy;
	}

	/**
	 * Delegates the creation of a new, empty context to the configured strategy.
	 */
	public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}

	@Override
	public String toString() {
		return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount="
				+ initializeCount + "]";
	}
}

  然后介绍一下这3种策略:

MODE_THREALOCAL: 允许每个线程在安全上下文存储自己的详细信息,在每个请求一个线程的web应用程序中,这是一种常见的方法,因为每个请求都是一个单独的线程。这个策略也是Spring Security默认使用的策略。

MODE_INHERITABLETHREADLOCAL: 类似于MODE_THREALOCAL,但是从名称中可以知道,这是一个可以继承的模式,所以这种模式,可以在使用异步方法时,将安全上下文复制到下一个线程,这样再运行带有@Async注解的异步方法时,调用该方法的线程也能继承到该安全上下文。

MODE_GLOBAL:使引用程序的所欲线程看到相同的安全上下文实例。

由于Spring Security默认使用的是 MODE_THREALOCAL模式,该模式只允许在请求的主线程中获取安全上下文信息,用来获取用户身份信息,所以如果请求的接口中,又调用了异步方法,或者自定义了线程池去执行方法,则会获取不到用户信息,并且出现NullPointerException异常.

默认策略使用ThreadLocal管理上下文,ThreadLocal是JDK提供的实现,该实现作为数据集合来执行,但会确保应用程序的每个线程只能看到存储在集合中的数据,这样,每个请求都可以访问各自的安全上下文。线程之间不可以访问到其他线程的ThreadLocal。每个请求只能看到自己的安全上下文。确保线程获取的用户信息的准确性。

     注意:每个请求绑定一个线程这种架构只适用于传统的Servlet应用程序,其中每个请求都被分配了自己的线程。它不适用于响应式编程程序。

   如果我们在请求接口中调用了@Async注解的异步方法。那么就需要自定义配置,修改程序默认的策略。定义配置也很简单,只需要在配置类中注入一个bean即可,覆盖Spring Security默认的安全上下文策略即可:

   配置代码如下:

     * @description: 使异步任务可以继承安全上下文(SecurityContext);
     * 注意,该方法只适用于spring 框架本身创建线程时使用(例如,在使用@Async方法时),这种方法才有效;
     * 如果是在代码中手动创建线程,则需要使用{@link org.springframework.security.concurrent.DelegatingSecurityContextRunnable}
     * 或者{@link org.springframework.security.concurrent.DelegatingSecurityContextCallable},
     * 当然使用{@link org.springframework.security.concurrent.DelegatingSecurityContextExecutorService}
     * 来转发安全上下文更好
     * @author: xiaomifeng1010
     * @date: 2022/8/1
     * @param
     * @return: InitializingBean
     **/
    @Bean
    public InitializingBean initializingBean(){
        return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    }

但是这种方式仅适用于可以被Spring管理的线程。但是还有一种情况就是我们自己手动创建的线程池,Spring框架是没有进行管理的,或者说是不被Spirng框架知道的,这种线程称为自管理线程,针对这种情况,应该如何处理呢?好在Spring Security提供了一些实用的工具,将有助于将安全上下文传播到新创建的线程。

  SecurityContextHolder如果要处理自己定义的线程任务传递安全上下文是没有指定策略的。在这种情况下,如果我们需要安全上下文在线程之间传播。用于此目的的一种解决方案是使用DelegatingSecurityContextRunnable装饰想要在单独线程上执行的任务。DelegatingSecurityContextRunnable拓展了Runnable接口。当不需要预期的值时,则可以在任务执行时使用它。如果需要返回值,则可以使用DelegatingSecurityContextCallable<T>。这两个类用于处理异步执行的任务。

例如要将任务提交给ExecutorService执行,

package com.dcboot;

import com.dcboot.base.config.security.support.MyUserDetails;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.security.concurrent.DelegatingSecurityContextCallable;
import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.concurrent.*;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 2022/4/21 21:54
 * @Description
 */
public class Test {

    public static void main(String[] args) {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("test-async-%d").build();
        ExecutorService executor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024),
                namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
        Callable<String> task=() ->{
            MyUserDetails userDetails = (MyUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            String userAccount = userDetails.getUsername();
            return userAccount;
        };

//        注意此时不能直接用executor.submit(task).get()方法去获取用户账号,这样也是会空指针异常的,而是需要用DelegatingSecuityContextCallable装饰一下原来的Callbale任务
        DelegatingSecurityContextCallable<String> stringDelegatingSecurityContextCallable = new DelegatingSecurityContextCallable<>(task);
        try {
            String userAccount = executor.submit(stringDelegatingSecurityContextCallable).get();
            System.out.println(userAccount);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();
        }

    }

}

这里为了方便,所以我写在了main方法里边,在项目中,这个Callable任务对应的就是你的Service层的方法,在接口中用线程池调用的时候,就可以这样使用了。

但是这种处理方式是从要被执行的任务方法本身进行修饰处理的,还有一种就是可以直接从线程池入手,使用DelegatingSecurityContextExecutorService来转发安全上下文。

代码示例如下:

    public String test(){
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("test-async-%d").build();
        ExecutorService executor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024),
                namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
        Callable<String> task=() ->{
            MyUserDetails userDetails = (MyUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            String userAccount = userDetails.getUsername();
            return userAccount;
        };

        try {
//            也可以通过修饰线程池的方式,来传播安全上下文
            executor=new DelegatingSecurityContextExecutorService(executor);
            String userAccount = executor.submit(task).get();
            System.out.println(userAccount);
            return userAccount;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();
        }

    return  null;
    }

而我们在日常项目中,使用CompletableFuture做异步处理也是很多的。所以,在使用CompletableFuture时,就需要指定一下自定义的线程池,而不能使用默认的线程池;

示例代码如下:

    /**
     * @param taskObjectId
     * @description: 获取上市走访录入信息详情
     * @author: xiaomifeng1010
     * @date: 2022/4/12
     * @return: ApiResult
     **/
    @ApiOperation(value = "获取上市走访录入信息详情")
    @GetMapping("/getIPOVisitObjectDetail")
    @ResponseBody
    public ApiResult getIPOVisitObjectDetail(@NotNull(message = "任务对象id为空")  Long taskObjectId) {
        try {
            ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("visit-query-async-%d").build();
            ExecutorService executor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024),
                    namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
            executor=new DelegatingSecurityContextExecutorService(executor);
            IpoVisitInfoDetailRespVO ipoVisitInfoDetailRespVO1 = CompletableFuture.supplyAsync(() -> ipoVisitInfoService.getIPOVisitObjectDetail(taskObjectId),executor)
                    .thenApplyAsync(ipoVisitInfoDetailRespVO -> {
                        List<IpoVisitInfoDetailRespVO.approveSendBackInfo> approveSendBackInfoList = ipoVisitInfoService.getApproveSendBackInfoList(taskObjectId);
                        if (CollectionUtils.isNotEmpty(approveSendBackInfoList)) {
                            ipoVisitInfoDetailRespVO.setApproveSendBackInfoList(approveSendBackInfoList);
                        } else {
                            ipoVisitInfoDetailRespVO.setApproveSendBackInfoList(Collections.emptyList());
                        }
                        return ipoVisitInfoDetailRespVO;

                    },executor).get(5, TimeUnit.SECONDS);
            return ApiResult.success(ipoVisitInfoDetailRespVO1);
        } catch (InterruptedException | ExecutionException | TimeoutException ex ) {
            log.error("获取上市培育详情出错",ex);
            return  ApiResult.error("获取详情出错");
        }

    }

所以如果你要在异步执行的线程方法中获取用户身份信息,需要传递安全上下文,就需要使用下边截图中CompletableFuture源码中的第二个重载方法,需要传入线程池,并且是被DelegatingSecurityContextExecutorService修饰的线程池。如果异步方法中不需要获取用户身份信息,则可以使用第一个重载方法,直接使用默认线程池就可以。 

 

 此外对于线程池的修饰类,还有一些其他的工具类

去掉DelegatingSecurityContext前缀,就是这些类实现的对应JDK中的接口。比如 DelegatingSecurityContextExecutor就是实现了Executor接口,依次类比。

有关多线程异步方法Spring Security框架的SecurityContext无法获取认证信息的原因及解决方案的更多相关文章

  1. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

    大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  5. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

  6. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  7. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

  8. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

  9. ruby - 如何在 ruby​​ 中运行后台线程? - 2

    我是ruby​​的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp

  10. ruby - Rails 开发服务器、PDFKit 和多线程 - 2

    我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它:

随机推荐