分類
發燒車訊

SpringSceurity(5)—短信驗證碼登陸功能

SpringSceurity(5)—短信驗證碼登陸功能

有關SpringSceurity系列之前有寫文章

1、SpringSecurity(1)—認證+授權代碼實現

2、SpringSecurity(2)—記住我功能實現

3、SpringSceurity(3)—圖形驗證碼功能實現

4、SpringSceurity(4)—短信驗證碼功能實現

一、短信登錄驗證機制原理分析

了解短信驗證碼的登陸機制之前,我們首先是要了解用戶賬號密碼登陸的機制是如何的,我們來簡要分析一下Spring Security是如何驗證基於用戶名和密碼登錄方式的,

分析完畢之後,再一起思考如何將短信登錄驗證方式集成到Spring Security中。

1、賬號密碼登陸的流程

一般賬號密碼登陸都有附帶 圖形驗證碼記住我功能 ,那麼它的大致流程是這樣的。

1、 用戶在輸入用戶名,賬號、圖片驗證碼後點擊登陸。那麼對於springSceurity首先會進入短信驗證碼Filter,因為在配置的時候會把它配置在
UsernamePasswordAuthenticationFilter之前,把當前的驗證碼的信息跟存在session的圖片驗證碼的驗證碼進行校驗。

2、短信驗證碼通過後,進入 UsernamePasswordAuthenticationFilter 中,根據輸入的用戶名和密碼信息,構造出一個暫時沒有鑒權的
 UsernamePasswordAuthenticationToken,並將 UsernamePasswordAuthenticationToken 交給 AuthenticationManager 處理。

3、AuthenticationManager 本身並不做驗證處理,他通過 for-each 遍歷找到符合當前登錄方式的一個 AuthenticationProvider,並交給它進行驗證處理
,對於用戶名密碼登錄方式,這個 Provider 就是 DaoAuthenticationProvider。

4、在這個 Provider 中進行一系列的驗證處理,如果驗證通過,就會重新構造一個添加了鑒權的 UsernamePasswordAuthenticationToken,並將這個
 token 傳回到 UsernamePasswordAuthenticationFilter 中。

5、在該 Filter 的父類 AbstractAuthenticationProcessingFilter 中,會根據上一步驗證的結果,跳轉到 successHandler 或者是 failureHandler。

流程圖

2、短信驗證碼登陸流程

因為短信登錄的方式並沒有集成到Spring Security中,所以往往還需要我們自己開發短信登錄邏輯,將其集成到Spring Security中,那麼這裏我們就模仿賬號

密碼登陸來實現短信驗證碼登陸。

1、用戶名密碼登錄有個 UsernamePasswordAuthenticationFilter,我們搞一個SmsAuthenticationFilter,代碼粘過來改一改。
2、用戶名密碼登錄需要UsernamePasswordAuthenticationToken,我們搞一個SmsAuthenticationToken,代碼粘過來改一改。
3、用戶名密碼登錄需要DaoAuthenticationProvider,我們模仿它也 implenments AuthenticationProvider,叫做 SmsAuthenticationProvider。

這個圖是網上找到,自己不想畫了

我們自己搞了上面三個類以後,想要實現的效果如上圖所示。當我們使用短信驗證碼登錄的時候:

1、先經過 SmsAuthenticationFilter,構造一個沒有鑒權的 SmsAuthenticationToken,然後交給 AuthenticationManager處理。

2、AuthenticationManager 通過 for-each 挑選出一個合適的 provider 進行處理,當然我們希望這個 provider 要是 SmsAuthenticationProvider。

3、驗證通過後,重新構造一個有鑒權的SmsAuthenticationToken,並返回給SmsAuthenticationFilter。
filter 根據上一步的驗證結果,跳轉到成功或者失敗的處理邏輯。

二、代碼實現

1、SmsAuthenticationToken

首先我們編寫 SmsAuthenticationToken,這裏直接參考 UsernamePasswordAuthenticationToken 源碼,直接粘過來,改一改。

說明

principal 原本代表用戶名,這裏保留,只是代表了手機號碼。
credentials 原本代碼密碼,短信登錄用不到,直接刪掉。
SmsCodeAuthenticationToken() 兩個構造方法一個是構造沒有鑒權的,一個是構造有鑒權的。
剩下的幾個方法去除無用屬性即可。

代碼

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中該字段代表登錄的用戶名,
     * 在這裏就代表登錄的手機號碼
     */
    private final Object principal;

    /**
     * 構建一個沒有鑒權的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 構建擁有鑒權的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

2、SmsAuthenticationFilter

然後編寫 SmsAuthenticationFilter,參考 UsernamePasswordAuthenticationFilter 的源碼,直接粘過來,改一改。

說明

原本的靜態字段有 usernamepassword,都幹掉,換成我們的手機號字段。
SmsCodeAuthenticationFilter() 中指定了這個 filter 的攔截 Url,我指定為 post 方式的 /sms/login
剩下來的方法把無效的刪刪改改就好了。

代碼

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    /**
     * form表單中手機號碼的字段name
     */
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = "mobile";
    /**
     * 是否僅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        //短信驗證碼的地址為/sms/login 請求也是post
        super(new AntPathRequestMatcher("/sms/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);
        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public String getMobileParameter() {
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}

3、SmsAuthenticationProvider

這個方法比較重要,這個方法首先能夠在使用短信驗證碼登陸時候被 AuthenticationManager 挑中,其次要在這個類中處理驗證邏輯。

說明

實現 AuthenticationProvider 接口,實現 authenticate() 和 supports() 方法。

代碼

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    /**
     * 處理session工具類
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_SMS";

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal();

        checkSmsCode(mobile);

        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        // 此時鑒權成功后,應當重新 new 一個擁有鑒權的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    private void checkSmsCode(String mobile) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 從session中獲取圖片驗證碼
        SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute(new ServletWebRequest(request), SESSION_KEY_PREFIX);
        String inputCode = request.getParameter("smsCode");
        if(smsCodeInSession == null) {
            throw new BadCredentialsException("未檢測到申請驗證碼");
        }

        String mobileSsion = smsCodeInSession.getMobile();
        if(!Objects.equals(mobile,mobileSsion)) {
            throw new BadCredentialsException("手機號碼不正確");
        }

        String codeSsion = smsCodeInSession.getCode();
        if(!Objects.equals(codeSsion,inputCode)) {
            throw new BadCredentialsException("驗證碼錯誤");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判斷 authentication 是不是 SmsCodeAuthenticationToken 的子類或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

4、SmsCodeAuthenticationSecurityConfig

既然自定義了攔截器,可以需要在配置里做改動。

代碼

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private SmsUserService smsUserService;
    @Autowired
    private AuthenctiationSuccessHandler authenctiationSuccessHandler;
    @Autowired
    private AuthenctiationFailHandler authenctiationFailHandler;

    @Override
    public void configure(HttpSecurity http) {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenctiationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenctiationFailHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        //需要將通過用戶名查詢用戶信息的接口換成通過手機號碼實現
        smsCodeAuthenticationProvider.setUserDetailsService(smsUserService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

5、SmsUserService

因為用戶名,密碼登陸最終是通過用戶名查詢用戶信息,而手機驗證碼登陸是通過手機登陸,所以這裏需要自己再實現一個SmsUserService

@Service
@Slf4j
public class SmsUserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RolesUserMapper rolesUserMapper;

    @Autowired
    private RolesMapper rolesMapper;

    /**
     * 手機號查詢用戶
     */
    @Override
    public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
        log.info("手機號查詢用戶,手機號碼 = {}",mobile);
        //TODO 這裏我沒有寫通過手機號去查用戶信息的sql,因為一開始我建user表的時候,沒有建mobile字段,現在我也不想臨時加上去
        //TODO 所以這裏暫且寫死用用戶名去查詢用戶信息(理解就好)
        User user = userMapper.findOneByUsername("小小");
        if (user == null) {
            throw new UsernameNotFoundException("未查詢到用戶信息");
        }
        //獲取用戶關聯角色信息 如果為空說明用戶並未關聯角色
        List<RolesUser> userList = rolesUserMapper.findAllByUid(user.getId());
        if (CollectionUtils.isEmpty(userList)) {
            return user;
        }
        //獲取角色ID集合
        List<Integer> ridList = userList.stream().map(RolesUser::getRid).collect(Collectors.toList());
        List<Roles> rolesList = rolesMapper.findByIdIn(ridList);
        //插入用戶角色信息
        user.setRoles(rolesList);
        return user;
    }
}

6、總結

到這裏思路就很清晰了,我這裡在總結下。

1、首先從獲取驗證的時候,就已經把當前驗證碼信息存到session,這個信息包含驗證碼和手機號碼。

2、用戶輸入驗證登陸,這裡是直接寫在SmsAuthenticationFilter中先校驗驗證碼、手機號是否正確,再去查詢用戶信息。我們也可以拆開成用戶名密碼登陸那樣一個
過濾器專門驗證驗證碼和手機號是否正確,正確在走驗證碼登陸過濾器。

3、在SmsAuthenticationFilter流程中也有關鍵的一步,就是用戶名密碼登陸是自定義UserService實現UserDetailsService后,通過用戶名查詢用戶名信息而這裡是
通過手機號查詢用戶信息,所以還需要自定義SmsUserService實現UserDetailsService后。

三、測試

1、獲取驗證碼

獲取驗證碼的手機號是 15612345678 。因為這裏沒有接第三方的短信SDK,只是在後台輸出。

向手機號為:15612345678的用戶發送驗證碼:254792

2、登陸

1)驗證碼輸入不正確

發現登陸失敗,同樣如果手機號碼輸入不對也是登陸失敗

2)登陸成功

當手機號碼 和 短信驗證碼都正確的情況下 ,登陸就成功了。

參考

1、Spring Security技術棧開發企業級認證與授權(JoJo)

2、SpringBoot 集成 Spring Security(8)——短信驗證碼登錄

別人罵我胖,我會生氣,因為我心裏承認了我胖。別人說我矮,我就會覺得好笑,因為我心裏知道我不可能矮。這就是我們為什麼會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(21)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

IDEA創建SpringBoot的多模塊項目教程

最近在寫一個多模塊的SpringBoot項目,基於過程總了一些總結,故把SpringBoot多個模塊的項目創建記錄下來。

首先,先建立一個父工程:

(1)在IDEA工具欄選擇File->New->Project

(2)選擇Spring Initializr,默認選擇Default,然後點擊Next:    

(3)在輸入框填寫以下截圖內容,點擊Next

(4)直接點Next,無需選擇

(5)直接點擊Finish完成創建

(6)按照以上步驟,可以生成以下的項目目錄結構:

(7)這時把沒用的.mvn目錄,src目錄,mvnw還有mvnw.cmd都刪除,最終只保留.gitignore和pom.xml,若是web項目,可在該pom.xml里添加以下依賴:

1 <!--web特徵-->
2 <dependency>
3     <groupId>org.springframework.boot</groupId>
4     <artifactId>spring-boot-starter-web</artifactId>
5     <version>2.3.1.RELEASE</version>
6 </dependency>

最終得到以下的父結構目錄:

 

以上是創建父模塊,下面創建子模塊:

(1)在父模塊的根目錄fte上點右鍵,在彈出的框里選擇New->Module

(2)選擇Maven,點擊Next

(3)填寫以下內容,點擊Next

(4)填寫Module,點擊Finish

(5)同理添加fte-controller,fte-dao,fte-service,fte-web,最終得到以下的目錄結構:

(6)增加模塊之間的依賴:

controller層添加以下依賴:

 1 <dependencies>
 2     <dependency>
 3         <groupId>com.example</groupId>
 4         <artifactId>fte-common</artifactId>
 5         <version>0.0.1-SNAPSHOT</version>
 6     </dependency>
 7 
 8     <dependency>
 9         <groupId>com.example</groupId>
10         <artifactId>fte-dao</artifactId>
11         <version>0.0.1-SNAPSHOT</version>
12     </dependency>
13 
14     <dependency>
15         <groupId>com.example</groupId>
16         <artifactId>fte-service</artifactId>
17         <version>0.0.1-SNAPSHOT</version>
18     </dependency>
19 </dependencies>

service層添加以下依賴:

1 <dependencies>
2     <dependency>
3         <groupId>com.example</groupId>
4         <artifactId>fte-dao</artifactId>
5         <version>0.0.1-SNAPSHOT</version>
6     </dependency>
7 </dependencies>

(7)測試

在fte-controller創建com.zhu.fte.web包,增加以下兩個類:

fteWebApplication類:

 1 package com.zhu.fte.web;
 2 
 3 import org.springframework.boot.SpringApplication;
 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
 5 
 6 @SpringBootApplication
 7 public class fteWebApplication {
 8     public static void main(String[] args) {
 9         SpringApplication.run(fteWebApplication.class,args);
10     }
11 }

DemoController類

 1 package java.com.zhu.fte.web;
 2 
 3 import org.springframework.web.bind.annotation.GetMapping;
 4 import org.springframework.web.bind.annotation.RequestMapping;
 5 import org.springframework.web.bind.annotation.RestController;
 6 
 7 @RestController
 8 @RequestMapping("demo")
 9 public class DemoController {
10 
11     @GetMapping("test")
12     public String test(){
13         return "hello world";
14     }
15 
16 }

運行發現出現錯誤:

出現錯誤:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.22.2:test (default-test) on project fte-common: Execution default-test of goal org.apache.maven.plugins:maven-surefire-plugin:2.22.2:test failed: Plugin org.apache.maven.plugins:maven-surefire-plugin:2.22.2 or one of its dependencies could not be resolved: Could not transfer artifact junit:junit:jar:4.12 from/to central (https://repo.maven.apache.org/maven2): Connect to repo.maven.apache.org:443 [repo.maven.apache.org/151.101.52.215] failed: Connection timed out: connect -> [Help 1]

把缺少的org.apache.maven.plugins手動放到父工程的pom.xml里

 1 <build>
 2    <plugins>
 3       <plugin>
 4          <groupId>org.apache.maven.plugins</groupId>
 5          <artifactId>maven-clean-plugin</artifactId>
 6          <version>2.5</version>
 7       </plugin>
 8       <plugin>
 9          <groupId>org.apache.maven.plugins</groupId>
10          <artifactId>maven-source-plugin</artifactId>
11          <version>2.2</version>
12       </plugin>
13       <plugin>
14          <groupId>org.apache.maven.plugins</groupId>
15          <artifactId>maven-compiler-plugin</artifactId>
16          <version>3.0</version>
17          <configuration>
18             <source>1.8</source>
19             <target>1.8</target>
20             <encoding>${file.encoding}</encoding>
21             <!--編譯的時候方法不改變方法參數名稱,用於支持使用反射獲取方法參數名稱-->
22             <compilerArgument>-parameters</compilerArgument>
23          </configuration>
24       </plugin>
25       <plugin>
26          <groupId>org.apache.maven.plugins</groupId>
27          <artifactId>maven-install-plugin</artifactId>
28          <version>2.4</version>
29       </plugin>
30       <plugin>
31          <groupId>org.apache.maven.plugins</groupId>
32          <artifactId>maven-jar-plugin</artifactId>
33          <version>2.4</version>
34          <configuration>
35             <archive>
36                <manifest>
37                   <addDefaultImplementationEntries>true
38                   </addDefaultImplementationEntries>
39                </manifest>
40             </archive>
41          </configuration>
42       </plugin>
43 
44       <plugin>
45          <groupId>org.apache.maven.plugins</groupId>
46          <artifactId>maven-surefire-plugin</artifactId>
47          <version>2.13</version>
48          <configuration>
49             <argLine>-Xmx512M -Dfile.encoding=${file.encoding}</argLine>
50          </configuration>
51       </plugin>
52    </plugins>
53 </build>

運行fteWebApplication類里的main方法,默認端口為8080,訪問http://localhost:8080/demo/test,正常出現以下情況:

 

按照以上步驟,就可以初步建立SpringBoot多模塊的項目,下一章將基於這個基礎搭建Mybatis以及其逆向工程。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

分類
發燒車訊

Spring IoC 循環依賴的處理

前言

本系列全部基於 Spring 5.2.2.BUILD-SNAPSHOT 版本。因為 Spring 整個體系太過於龐大,所以只會進行關鍵部分的源碼解析。

本篇文章主要介紹 Spring IoC 是怎麼解決循環依賴的問題的。

正文

什麼是循環依賴

循環依賴就是循環引用,就是兩個或多個 bean 相互之間的持有對方,比如A引用B,B引用A,像下面偽代碼所示:

public class A {
    private B b;
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    // 省略get和set方法...
}

Spring 如何解決循環依賴

Spring IoC 容器對循環依賴的處理有三種情況:

  1. 構造器循環依賴:此依賴 Spring 無法處理,直接拋出 BeanCurrentlylnCreationException 異常。
  2. 單例作用域下的 setter 循環依賴:此依賴 Spring 通過三級緩存來解決。
  3. 非單例的循環依賴:此依賴 Spring 無法處理,直接拋出 BeanCurrentlylnCreationException 異常。

構造器循環依賴

還是假設上面的A和B類是構造器循環依賴,如下所示:

public class A {
    private B b;
    
    public A(B b) {
        this.b = b;
    }
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    public B(A a) {
        this.a = a;
    }
    
    // 省略get和set方法...
}

然後我們在 XML 中配置了構造器自動注入,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.leisurexi.ioc.circular.reference.A" autowire="constructor" />

    <bean id="b" class="com.leisurexi.ioc.circular.reference.B" autowire="constructor" />

</beans>

那麼我們在獲取 A 時,首先會進入 doGetBean() 方法(該方法在Spring IoC bean 的加載中分析過),會進行到如下代碼塊:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

    // 省略其它代碼...
    
    // 如果 bean 的作用域是單例
    if (mbd.isSingleton()) {
        // 創建和註冊單例 bean
        sharedInstance = getSingleton(beanName, () -> {
            try {
                // 創建 bean 實例
                return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
                destroySingleton(beanName);
                throw ex;
            }
        });
        // 獲取bean實例
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }
    
    // 省略其它代碼...
 
}

上面方法中的 getSingleton() 方法會判斷是否是第一次創建該 bean,如果是第一次會先去創建 bean,也就是調用 ObjectFacotygetObject() 方法,即調用 createBean() 方法創建 bean 前,會先將當前正要創建的 bean 記錄在緩存 singletonsCurrentlyInCreation 中。

在創建A時發現依賴 B,便先去創建 B;B在創建時發現依賴A,此時A因為是通過構造函數創建,所以沒創建完,便又去創建A,發現A存在於 singletonsCurrentlyInCreation,即正在創建中,便拋出 BeanCurrentlylnCreationException 異常。

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    // 加鎖
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        // 一級緩存中不存在當前 bean,也就是當前 bean 第一次創建
        if (singletonObject == null) {
            // 如果當前正在銷毀 singletons,拋出異常
            if (this.singletonsCurrentlyInDestruction) {
                throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)");
            }
            // 創建單例 bean 之前的回調
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                // 獲取 bean 實例
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
				}
            // 省略異常處理...
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                // 創建單例 bean 之後的回調
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 將 singletonObject 放入一級緩存,並從二級和三級緩存中移除
                addSingleton(beanName, singletonObject);
            }
        }
        // 返回 bean 實例
        return singletonObject;
    }
}

// 單例 bean 創建前的回調方法,默認實現是將 beanName 加入到當前正在創建 bean 的緩存中,
// 這樣便可以對循環依賴進行檢測
protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

// 單例 bean 創建后的回調方法,默認實現是將 beanName 從當前正在創建 bean 的緩存中移除
protected void afterSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
        throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 這邊bean已經初始化完成了,放入一級緩存
        this.singletonObjects.put(beanName, singletonObject);
        // 移除三級緩存
        this.singletonFactories.remove(beanName);
        // 移除二級緩存
        this.earlySingletonObjects.remove(beanName);
        // 將 beanName 添加到已註冊 bean 緩存中
        this.registeredSingletons.add(beanName);
    }
}

setter循環依賴

還是假設上面的A和B類是 field 屬性依賴注入循環依賴,如下所示:

public class A {
    private B b;
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    // 省略get和set方法...
}

然後我們在 XML 中配置了按照類型自動注入,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.leisurexi.ioc.circular.reference.A" autowire="byType" />

    <bean id="b" class="com.leisurexi.ioc.circular.reference.B" autowire="byType" />

</beans>

Spring 在解決單例循環依賴時引入了三級緩存,如下所示:

// 一級緩存,存儲已經初始化完成的bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二級緩存,存儲已經實例化完成的bean
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三級緩存,存儲創建bean實例的ObjectFactory
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// 按先後順序記錄已經註冊的單例bean
private final Set<String> registeredSingletons = new LinkedHashSet<>(256);

首先在創建A時,會進入到 doCreateBean() 方法(前面的流程可以查看Spring IoC bean 的創建一文),如下:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
    // 獲取bean的實例
    BeanWrapper instanceWrapper = null;
    if (instanceWrapper == null) {
        // 通過構造函數反射創建bean的實例,但是屬性並未賦值
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    // 獲取bean的實例
    final Object bean = instanceWrapper.getWrappedInstance();
    
    // 省略其它代碼...

    // bean的作用域是單例 && 允許循環引用 && 當前bean正在創建中
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
    // 如果允許bean提前曝光
    if (earlySingletonExposure) {
        // 將beanName和ObjectFactory形成的key-value對放入singletonFactories緩存中
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    
    // 省略其它代碼...
    
}

在調用 addSingletonFactory() 方法前A的實例已經創建出來了,只是還未進行屬性賦值和初始化階段,接下來將它放入了三級緩存中,如下:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    // 加鎖
    synchronized (this.singletonObjects) {
        // 如果一級緩存中不包含當前bean
        if (!this.singletonObjects.containsKey(beanName)) {
            // 將ObjectFactory放入三級緩存
            this.singletonFactories.put(beanName, singletonFactory);
            // 從二級緩存中移除
            this.earlySingletonObjects.remove(beanName);
            // 將beanName加入到已經註冊過的單例bean緩存中
            this.registeredSingletons.add(beanName);
        }
    }
}

接下來A進行屬性賦值階段(會在後續文章中單獨分析這個階段),發現依賴B,便去獲取B,發現B還沒有被創建,所以走創建流程;在B進入屬性賦值階段時發現依賴A,就去調用 getBean() 方法獲取A,此時會進入 getSingleton() 方法(該方法的調用流程在Spring IoC bean 的加載一文中分析過),如下:

public Object getSingleton(String beanName) {
    // allowEarlyReference設置為true表示允許早期依賴
    return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 先從一級緩存中,檢查單例緩存是否存在
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果為空,並且當前bean正在創建中,鎖定全局變量進行處理
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 從二級緩存中獲取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 二級緩存為空 && bean允許提前曝光
            if (singletonObject == null && allowEarlyReference) {
                // 從三級緩存中獲取bean對應的ObjectFactory
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 調用預先設定的getObject(),獲取bean實例
                    singletonObject = singletonFactory.getObject();
                    // 放入到二級緩存中,並從三級緩存中刪除
                    // 這時bean已經實例化完但還未初始化完
                    // 在該bean未初始化完時如果有別的bean引用該bean,可以直接從二級緩存中取出返回
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

嘗試一級緩存 singletonObjects (肯定沒有,因為A還沒初始化完全),嘗試二級緩存 earlySingletonObjects(也沒有),嘗試三級緩存 singletonFactories,由於A通過 ObjectFactory 將自己提前曝光了,所以B能夠通過 ObjectFactory.getObject() 拿到A對象(雖然A還沒有初始化完全,但是總比沒有好呀)。B拿到A后順利創建並初始化完成,調用上面分析過的 addSingleton() 方法將自己放入一級緩存中。此時返回A中,A也能順利拿到完全初始化的B進行後續的階段,最後也將自己放入一級緩存中,並從二級和三級緩存中移除。

過程圖如下所示:

非單例循環依賴

對於非單例的 bean,Spring 容器無法完成依賴注入,因為 Spring 容器不進行緩存,因此無法提前暴露一個創建中的 bean

總結

本文主要介紹了 Spring 對三種循環依賴的處理,其實還有一種字段循環依賴,比如 @Autowired 註解標註的字段,但它和 setter 循環依賴的解決方法一樣,這裏就沒有多說。

最後,我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。

參考

  • 《Spring 源碼深度解析》—— 郝佳
  • https://juejin.im/post/5c98a7b4f265da60ee12e9b2

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

vue中使用element2

阻止谷歌下記住密碼

當我們將input框的類型設置為密碼框的時候,就會出現下面這種效果,不僅樣式不統一,有的時候,密碼框的上面並不是用戶名,而是其他的內容,也會被強制显示為用戶名:

 首先需要解決樣式問題:

 #app input:-webkit-autofill {
    -webkit-text-fill-color: #fff !important;
    -webkit-box-shadow: none !important;
    background-color: transparent;
    background-image: none;
    transition: background-color 999999s ease-in-out, color 999999s ease-in-out;
  }

 其次,阻止谷歌自帶的記住密碼:

 回車重定向

 單個el-input獲得焦點時,點擊鍵盤迴車,會觸發路由重定向。

解決方法:@submit.native.preven t阻止表單默認事件

 日期時間框的默認值在IE無法清除

element的日期框添加默認值后,在ie下,默認的清空按鈕無法清空默認日期值:

 

 

 數據應該是已經清空了,但是DOM沒有刷新,所以需要強制刷新DOM:

 

 自定義表頭

<template>
    <div>
        <el-table-column
            v-for="(item, idx) in list"
            :key="idx"
            v-bind="item" :show-overflow-tooltip="true">
            <tHeader
                v-if="item.children"
                :list="item.children">
            </tHeader>
        </el-table-column>
    </div>
</template>
<script>
export default {
    name: 'tHeader',
    props: [
        'list'
    ],
    methods: {
        repairEleSortBug() {
            this.list.unshift(this.list.pop());
        }
    },
    
    created() {
            //修復elementUI排序倒置的bug(將數組最後一個放到第一個)
        this.repairEleSortBug();
    }
};
</script>

 

 

 

 對象監聽

在vue中可以通過監聽一個變量的值變化觸發相應事件,但是當需要監聽的變量是個複雜對象時,通常在外出是監聽不到對象裏面值的變化,這時就需要深度監聽:

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

分類
發燒車訊

航空業減碳動起來 澳航允2050年前碳排減到零

摘錄自2019年11月11日中央社報導

澳洲航空今(11日)允諾加入英國航空(British Airways)母公司「國際航空集團」(IAG),要在2050年前,把淨碳排降至零。澳航(Qantas)執行長喬伊斯(Alan Joyce)發布聲明說,氣候變遷的憂慮「真實存在」,「我們之所以做這件事,是因為這件事很重要」。

來自環保團體「反抗滅絕」(Extinction Rebellion, XR)與環保小鬥士桑柏格(Greta Thunberg)壓力越來越大之際,國際航空集團上月允諾2050年前把淨碳排減到零,是首家做此承諾的大型航空公司。航空業者整體則已答應要在2050年前把碳排量減至2005年的一半。

澳航表示,希望能把淨碳排控制在2020年水準,並在10年間投資5000萬澳元開發永續能源來減碳,與傳統航空燃料相比,可減少8成的碳排。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

非洲黑猩猩擄走人類小孩 源自於棲地銳減

摘錄自2019年11月12日ETtoday綜合報導

非洲烏干達黑猩猩原始棲地逐年喪失,引爆人類與猩猩之間的激烈衝突。《國家地理》雜誌網站近日報導,當地黑猩猩為了覓食,不僅破壞農作物,甚至學會對人類發動攻擊;當地自2014年以來,已經發生多起兒童遭猩猩擄走事件,其中一人甚至被猩猩開膛剖肚,內臟還被吃掉。

在野外,雄性黑猩猩有時會獵捕其他猴子或山羊,並食用獵物的肉果腹;他們雖然對人類保持警覺心,但這也使他們變得更具侵略性。自從2014年以來,陸續發生多起孩童遭黑猩猩攻擊事件,造成至少3人死亡,另有至少6人受傷。目前還無法確定黑猩猩為何突然開始襲擊人類兒童,但據信與烏干達西部自然棲地逐漸被農田侵蝕有關。

烏干達野生動物管理局(UWA)表示,當局已經意識到猩猩攻擊兒童的問題,但面對黑猩猩的棲地森林和自然家園遭破壞也感到無能為力,理由是人們普遍認為開闢農地來維持生計,比起保育森林更加重要。

據悉,非洲大陸大約只剩下30萬頭野生黑猩猩,烏干達野生動物管理局考慮透過一系列教育性活動,向人們宣導保育森林的重要性。

 

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

分類
發燒車訊

BMW借普天之力 將在北京建200個充電樁

據知情人士透露,BMW汽車目前與中國普天集團旗下的普天新能源合作,2015年將在北京建設200個充電樁,具體合作模式是BMW提供自己的充電樁,由普天新能源為其建設和運營。   BMW生產的200個交流慢充樁已交付給普天新能源北京分公司,這批充電樁主要是為購買BMWi3、i8和華晨BMW之諾的BMW電動汽車客戶在公共領域充電使用,不過由於中德充電介面是統一的,所以其他品牌的電動汽車也可用其充電。   目前,BMW已在上海市區安裝了40多個公共充電裝置,主要分佈在上海各個BMW授權的經銷商處。BMW還與國家電網上海市電力公司及上海世博發展集團合作,在上海世博園區安裝50個公共充電樁。此外,其還與萬科集團達成戰略合作,在全國範圍內的400多個已建及新建社區內,支持業主安裝個人充電設施,並逐步在社區內配套採用國家通用標準的社區公共充電設施。    

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

歐洲純電動車市場熱絡 銷量同比勁增 73%

據《歐洲汽車新聞》報導,去年歐洲市場純電動車的銷量超越插電式混合電動車,不過今年由於車企將推出更多插電式混合車型,因此其銷量有可能超越純電動車。    歐洲汽車製造商協會 ACEA 數據顯示,2014 年在歐盟及自由貿易聯盟國家(EFTA)市場中,純電動車銷量同比勁增 73% 至 58,244 輛,插電式混合電動車及增程式電動車的銷量同比攀升 26% 至 39,547 輛,銷量最高的電動車為日產聆風,而該地區最暢銷的插電式混動車則為三菱歐藍德 PHEV。    汽車諮詢公司 IHS 預計到 2020 年,插混車的全球累計產量將達到 135 萬輛,且在 2020 年至 2025 年間將進一步翻倍至 270 萬輛。而純電動車 2020 年的銷量將在 100 萬輛以下。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

分類
發燒車訊

《HelloGitHub》第 51 期

興趣是最好的老師,HelloGitHub 就是幫你找到興趣!

簡介

分享 GitHub 上有趣、入門級的開源項目。

這是一個面向編程新手熱愛編程對開源社區感興趣 人群的月刊,月刊的內容包括:各種編程語言的項目讓生活變得更美好的工具書籍、學習筆記、教程等,這些開源項目大多都是非常容易上手,而且非常 Cool。主要是希望大家能動手用起來,加入到開源社區中。

  • 會編程的可以貢獻代碼
  • 不會編程的可以反饋使用這些工具中的 Bug
  • 幫着宣傳你覺得優秀的項目
  • Star 項目⭐️

在瀏覽、參与這些項目的過程中,你將學習到更多編程知識提高編程技巧找到編程的樂趣

最後 HelloGitHub 這個項目就誕生了

以下為本期內容|每個月 28 號發布最新一期|點擊查看往期內容

C 項目

1、goaccess:實時 Web 日誌分析工具

2、u6a:函數式編程語言 Unlambda 的一個樸素實現,包含字節碼編譯器和解釋器。此項目可以幫助初學者理解函數式編程的思想,並提供了實現函數式編程語言解釋器的一些樸素思路。

  • 性能優異:運行性能遠高於官方實現,且優於多數現有的開源實現
  • 穩定可靠:有豐富的測試樣例支撐,可靠性高
  • 簡單樸素:代碼簡單易讀,且提供了實現思路文檔,對初學或者完全沒有學過編譯原理的新手非常友好

C# 項目

3、Netch:一款 Windows 平台的開源遊戲加速工具

4、ScheduleMasterCore:一款基於 .NET Core 開發的分佈式任務調度系統。支持豐富的調度類型、靈活可控的系統參數、簡易的 UI 操作、支持多節點高可用、業務 API 集成等等特性。同時支持多樣化的部署方式,容易上手

5、HandyControl:一套 WPF 控件庫。它幾乎重寫了所有原生樣式,同時包含 70 餘款自定義控件。支持跨平台、國際化,適用於 MVVM 架構開發,扁平化設計、支持動態更換主題和背景色。豐富的自定義控件解決了 View 設計的痛點,讓程序員更加專註於業務邏輯的開發

C++ 項目

6、CnC_Remastered_Collection:EA 發布的《紅警》和《泰伯利亞黎明》遊戲源代碼

7、chinessChess:基於 Qt5 開發的中國象棋網絡對戰平台,支持單機和網絡對戰

Go 項目

8、grmon:Goroutine 的命令行監控工具

9、HackChrome:Go 語言實現的從 Chrome 中獲取自動保存的用戶名密碼工具。目前僅支持 Windows Chrome 中存儲的密碼,但是很有意思還可以學習怎麼用 Go 調用 DLL 動態鏈接庫的姿勢

10、seaweedfs:一款基於 Go 開發的部署方便、使用簡單且強大的分佈式文件系統

11、fate:起中文名工具,去吧!算名先生

Java 項目

12、JApiDocs:一個無需額外註解、開箱即用的 SpringBoot 接口文檔生成工具。特性:

  • 代碼即文檔
  • 支持導出 HTML
  • 同步導出客戶端 Model 代碼
  • 等等

13、PowerJob:基於 Akka 架構的新一代分佈式任務調度與計算框架。支持 CRON、API、固定頻率、固定延遲等調度策略,支持單機、廣播、MapReduce 等多種執行模式,支持在線任務治理與運維,提供 Shell、Python、Java 等功能豐富的任務處理器,提供工作流來編排任務解決依賴關係,使用簡單,功能強大,文檔齊全。同類產品對比:

JavaScript 項目

14、react-trello:任務狀態管理面板組件。實現了拖拽方式管理任務狀態,點擊即可編輯任務內容

15、perfume.js:用於測量第一個 dom 生成的時間、用戶最早可操作時間和組件的生命周期性能的庫。示例代碼:

perfume.start('fibonacci');
fibonacci(400);
perfume.end('fibonacci');
// Perfume.js: fibonacci 0.14 ms

16、Mongood:MongoDB 圖形化的管理工具。特性:

  • 基於微軟 Fluent UI,支持自動黑暗模式
  • 支持完整的 Mongo-shell 數據類型和查詢語法,利用索引實現的自動查詢和排序
  • 支持 Json 數據庫模式,既可用於 Server 也可用於 Client

17、TimeCat:一款 JS 的網頁錄屏工具。參考了遊戲錄像的原理而實現的渲染引擎,生成的錄像文件只有傳統視頻的百分之一!還可以在錄製語音的同時自動生成字幕,導出的視頻文件可以跨端播放。目前已經開發一段時間,後續還將實現更多有意思的功能,歡迎持續關注。在線預覽

18、react-visual-editor:基於 React 組件的可視化拖拽、搭建頁面的代碼生成工具。所見即所得,可以完美還原 UI 設計搞,並支持多款型號手機(可配置)和 PC 效果展示,模板功能可以使你分享你的頁面或者頁面中局部任何部分組件組合,減少相似頁面的重複操作。效果如下:

19、elevator.js:一個 back to top 返回頂部的插件。如他的名字一樣,網頁在返回頂部過程中像電梯向上運行,當頁面返回到頂部時,會有電梯“到達”的提示音。叮~頁面已到達頂部

PHP 項目

20、code6:一款 GitHub 代碼泄露監控系統,通過定期掃描 GitHub 發現代碼泄露行為。特性:

  • 全可視化界面,操作部署簡單
  • 支持 GitHub 令牌管理及智能調度
  • 掃描結果信息豐富,支持批量操作
  • 任務配置靈活,可單獨配置任務掃描參數
  • 支持白名單模式,主動忽略白名單倉庫

Python 項目

21、rich:一個讓你的終端輸出變得“花里胡哨”的三方庫。我的一位前輩告訴我,不要整那些花里胡哨的主題和樣式,這是在自尋煩惱。可是臣妾做不到啊,這麼好看的終端輸出,讓我的心情都愉悅起來了。瞧那性感的語法高亮、整齊的表格、舒服的顏色、進度條等,一切都是值得的

22、poetry:Python 虛擬環境、依賴管理工具。依賴管理工具有很多,我相上了它有三點:通過單文件 pyproject.toml 便可輕鬆的區別安裝、管理開發和正式環境、有版本鎖定可方便回滾、輸出界面簡單清爽。當然它還是個“新生兒”,嘗鮮的風險還是有的,選擇須謹慎

23、free-python-games:真入門級的 Python 遊戲集合庫。都是簡單的小遊戲:貪吃蛇、迷宮、Pong、猜字等,運行方便、代碼簡單易懂。用遊戲開啟的你 Python 學習之旅,玩完再學源碼,其樂無窮啊。安裝運行:

pip install freegames
python -m freegames.snake # freegames.遊戲名

24、py2sec:一款輕量級跨平台 Python “加密”、加速的腳本工具。原理是基於 Cython 將 .py 編譯成 run-time libraries 文件:.so(Linux && Mac)或 .pyd(Win),一定程度上實現了“加密”保護源代碼的功能。參數詳解如下:

-v,  --version    显示 py2sec 版本
-h,  --help       显示幫助菜單
-p,  --pyth       Python 的版本,默認為你的 Python 命令綁定的 Python 版本
-d,  --directory  Python 項目路徑(如果使用 -d 參數,將編譯整個 Python 項目)
-f,  --file       Python文件(如果使用 -f,將編譯單個 Python 文件)
-m,  --maintain   標記你不想編譯的文件或文件夾路徑
-x  --nthread     編譯啟用的線程數
-q  --quiet       靜默模式,默認 False
-r  --release     Release 模式,清除所有中間文件,只保留加密結果文件,默認 False
python py2sec.py -f test.py
python py2sec.py -f example/test1.py -r
python py2sec.py -d example/ -m test1.py,bbb/

25、oxfs:一個基於 sftp 協議的 fuse 網絡文件系統,功能上類似於 sshfs。特性:

  • 引入了異步併發讀遠端文件機制,提高了文件首次讀速度。
  • 緩存持久化到本地磁盤,下次掛載時訪問更加快速。
  • 異步任務負責同步文件,避免低速的網絡讀寫阻塞上層應用。

Swift 項目

26、Aerial:炫酷的蘋果系統屏保項目。該屏保視頻取材自蘋果零售店 Apple TV 的專用屏保,航拍質量超棒,快換上試試吧。直接下載 Aerial.saver.zip 文件,解壓后雙擊文件“即可食用”

其它

27、shan-shui-inf:自動生成一副山水畫

28、kuboard-press:一款基於 Kubernetes 的微服務管理界面。包含文檔、教程、管理界面和實戰分享

29、vscode-rainbow-fart:一款在你編程時花式誇你的 VSCode 擴展插件。可以根據代碼關鍵字,播放貼近代碼意義的真人語音,並且有一個醒目的項目名字“彩虹屁”

30、flink-training-course:Flink 視頻直播教程回放集合

31、raft-zh_cn:《分佈式 Raft 一致性算法論文》中文翻譯

32、GitHub-Chinese-Top-Charts:每周更新一次的 GitHub 中文項目排行榜

開源書籍

33、go-ast-book:《Go語法樹入門:開啟自製編程語言和編譯器之旅》

機器學習

34、Surprise:一款簡單易用基於 Python scikit 的推薦系統。如果你想用 Python 上手做一套推薦系統,那你可以試試它

35、djl:亞馬遜開源的一款基於 Java 語言的深度學習框架。對於 Java 開發者而言,可以在 Java 中開發及應用原生的機器學習和深度學習模型,同時簡化了深度學習開發的難度。通過 DJL 提供直觀的、高級的 API,Java 開發人員可以訓練自己的模型,或者利用數據科學家用 Python 預先訓練好的模型來進行推理。如果您恰好是對學習深度學習感興趣的 Java 開發者,那麼這個項目完全對口。運行效果如下:

36、data-science-ipython-notebooks:數據科學的 IPython 集合。包含:TensorFlow、Theano、Caffe、scikit-learn、Spark、Hadoop、MapReduce、matplotlib、pandas、SciPy 等方方面面

最後

如果你發現了 GitHub 上有趣的項目,歡迎在 HelloGitHub 項目提 issues 告訴我們。

關注 HelloGitHub 公眾號獲取第一手的更新

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

分類
發燒車訊

【asp.net core 系列】14 .net core 中的IOC

0.前言

通過前面幾篇,我們了解到了如何實現項目的基本架構:數據源、路由設置、加密以及身份驗證。那麼在實現的時候,我們還會遇到這樣的一個問題:當我們業務類和數據源越來越多的時候,我們無法通過普通的構造對象的方法為每個實例進行賦值。同時,傳統意義上的賦值遇到底層切換或者其他修改的時候,就需要修改大量的代碼,對改變不友好。為了改變這種現狀,我們基於面向接口編程,然後使用一些DI功能和IOC框架。

1. IOC和DI

先來給大家解釋幾個概念,IOC全稱Inversion of Control,翻譯過來就是控制反轉,是面向對象編程的一種設計原則,用來降低代碼之間的耦合度。所謂的控制反轉簡單來講就是將類中屬性或者其他參數的初始化交給其他方處理,而不是直接使用構造函數。

public class Demo1
{
}

public class Demo2
{
	public Demo1 demo;
}

對於以上簡單示例代碼中,在Demo2類中持有了一個Demo1的實例。如果按照之前的情況來講,我們會通過以下方法為demo賦值:

// 方法一
public Demo1 demo = new Demo1();
// 方法二
public Demo2()
{
    demo = new Demo1();
}

這時候,如果Demo1變成下面的樣子:

public class Demo1
{
    public Demo1(Demo3 demo3)
    {
        // 隱藏
    }
}
public class Demo3
{
}

那麼,如果Demo2 沒有持有一個Demo3的實例對象,這時候創建Demo1的時候就需要額外構造一個Demo3。如果Demo3需要持有另外一個類的對象,那麼Demo2中就需要多創建一個對象。最後就會發現這樣就陷入了一個構造“地獄”(我發明的詞,指這種為了一個對象卻得構造一大堆其他類型的對象)。

實際上,對於Demo2並不關心Demo1的實例對象是如何獲取的,甚至都不關心它是不是Demo1的子類或者接口實現類。我在示例中使用了類,但這裏可以同步替換成Interface,替換之後,Demo2在調用Demo1的時候,還需要知道Demo1有實現類,以及實現類的信息。

為了解決這個問題,一些高明的程序員們提出了將對象的創建這一過程交給第三方去操作,而不是調用類來創建。於是乎,上述代碼就變成了:

public class Demo2
{
    public Demo1 Demo {get;set;}
    public Demo2(Demo1 demo)
    {
        Demo = demo;
    }
}

似乎並沒有什麼變化?對於Demo2來說,Demo2從此不再負責Demo1的創建,這個步驟交由Demo2的調用方去創建,Demo2從此從負責維護Demo1這個對象的大麻煩中解脫了。

但實際上構造地獄的問題還是沒有解決,只不過是通過IOC的設計將這一步后移了。這時候,那些大神們想了想,不如開發一個框架這些實體對象吧。所以就出現了很多IOC框架:AutoFac、Sping.net、Unity等。

說到IOC就不得不提一下DI(Dependency Injection)依賴注入。所謂的依賴注入就是屬性對應實例通過構造函數或者使用屬性由第三方進行賦值。也就是最後Demo2的示例代碼中的寫法。

早期IOC和DI是指一種技術,後來開始確定這是不同的描述。IOC描述的是一種設計模式,而DI是一種行為。

2. 使用asp.net core的默認IOC

在之前的ASP.NET 框架中,微軟並沒有提供默認的IOC支持。在最新的asp.net core中微軟提供了一套IOC支持,該支持在命名空間:

Microsoft.Extensions.DependencyInjection

里,在代碼中引用即可。

主要通過以下幾組方法實現:

public static IServiceCollection AddScoped<TService>(this IServiceCollection services) where TService : class;
public static IServiceCollection AddSingleton<TService>(this IServiceCollection services) where TService : class;
public static IServiceCollection AddTransient<TService>(this IServiceCollection services) where TService : class;

這裏只列出了這三組方法的一種重載版本。

這三組方法分別代表三種生命周期:

  • AddScored 表示對象的生命周期為整個Request請求
  • AddTransient 表示每次從服務容器進行請求時創建的,適合輕量級、 無狀態的服務
  • AddSingleton 表示該對象在第一次從服務容器請求后獲取,之後就不會再次初始化了

這裏每組方法只介紹了一個版本,但實際上每個方法都有以下幾個版本:

public static IServiceCollection AddXXX<TService>(this IServiceCollection services) where TService : class;
public static IServiceCollection AddXXX(this IServiceCollection services, Type serviceType, Type implementationType);
public static IServiceCollection AddXXX(this IServiceCollection services, Type serviceType, Func<IServiceProvider, object> implementationFactory);
public static IServiceCollection AddXXX<TService, TImplementation>(this IServiceCollection services)
            where TService : class
            where TImplementation : class, TService;
public static IServiceCollection AddXXX(this IServiceCollection services, Type serviceType);
public static IServiceCollection AddXXX<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class;
public static IServiceCollection AddXXX<TService, TImplementation>(this IServiceCollection services, Func<IServiceProvider, TImplementation> implementationFactory)
            where TService : class
            where TImplementation : class, TService;

其中:implementationFactory 表示通過一個Provider實現TService/TImplementation 的工廠方法。當方法指定了泛型的時候,會自動依據泛型參數獲取要注入的類型信息,如果沒有使用泛型則必須手動傳入參數類型。

asp.net core如果使用依賴注入的話,需要在Startup方法中設置,具體內容可以參照以下:

public void ConfigureServices(IServiceCollection services)
{
    //省略其他代碼
    services.AddScoped<ISysUserAuthRepository,SysUserAuthRepository>();
}

asp.net core 為DbContext提供了不同的IOC支持,AddDbContext:

public static IServiceCollection AddDbContext<TContext>(
      this IServiceCollection serviceCollection,
      Action<DbContextOptionsBuilder> optionsAction = null,
      ServiceLifetime contextLifetime = ServiceLifetime.Scoped,
      ServiceLifetime optionsLifetime = ServiceLifetime.Scoped)
      where TContext : DbContext;

使用方法如下:

services.AddDbContext<DefaultContext>();

3. AutoFac 使用

理論上,asp.net core的IOC已經足夠好了,但是依舊原諒我的貪婪。如果有二三百個業務類需要我來設置的話,我寧願不使用IOC。因為那配置起來就是一場極其痛苦的過程。不過,可喜可賀的是AutoFac可以讓我免收這部分的困擾。

這裏簡單介紹一下如何使用AutoFac作為IOC管理:

cd Web  # 切換目錄到Web項目
dotnet package add Autofac.Extensions.DependencyInjection # 添加 AutoFac的引用

因為asp.net core 版本3更改了一些邏輯,AutoFac的引用方式發生了改變,現在不介紹之前版本的內容,以3為主。使用AutoFac需要先在 Program類里設置以下代碼:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    		Host.CreateDefaultBuilder(args)
    		.UseServiceProviderFactory(new AutofacServiceProviderFactory()) // 添加這行代碼
    		.ConfigureWebHostDefaults(webBuilder =>
			{
                webBuilder.UseStartup<Startup>();
            });

在Program類里啟用AutoFac的一個Service提供工廠類。然後在Startup類里添加如下方法:

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<DefaultContext>().As<DbContext>()
                .WithParameter("connectStr","Data Source=./demo.db")
                .InstancePerLifetimeScope();
            

    builder.RegisterAssemblyTypes(Assembly.Load("Web"))
        .Where(t => t.BaseType.FullName.Contains("Filter"))
        .AsSelf();

    builder.RegisterAssemblyTypes(Assembly.Load("Domain"),
                    Assembly.Load("Domain.Implements"), Assembly.Load("Service"), Assembly.Load("Service.Implements"))
                .AsSelf()
                .AsImplementedInterfaces()
                .InstancePerLifetimeScope()
                .PropertiesAutowired();
}

修改:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
            {
                options.Filters.Add<UnitOfWorkFilterAttribute>();
            }).AddControllersAsServices();// 這行新增
    // 省略其他
}

4. 總結

這一篇簡單介紹了如何在Asp.net Core中啟用IOC支持,並提供了兩種方式,可以說是各有優劣。小夥伴們根據自己需要選擇。後續會為大家詳細深入AutoFac之類IOC框架的核心秘密。

更多內容煩請關注我的博客《高先生小屋》

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準