spring boot使用jasypt加密原理解析


 

目錄

版本對應的坑

關鍵技術點

源碼解析

將jar包引入到spring boot中

@EnableAutoConfiguration原理

JasyptSpringBootAutoConfiguration

一是其@Import的StringEncryptorConfiguration.class

二是其對spring環境中包含的PropertySource對象的處理

一是AbstractApplicationContext的refresh方法

二是BeanFactoryPostProcessor接口的作用

EnableEncryptablePropertySourcesPostProcessor

具體的解密過程

補充1:查看JDK提供的Cipher算法

補充2:PBE的基礎算法demo,

參考:

 

首先介紹一下jasypt的使用方法

可以參考下面這篇文章:

Get史上最優雅的加密方式!沒有之一!

版本對應的坑
使用的時候還是遇到一個坑,就是jasypt的版本與spring boot版本存在對應情況。可以看到jasypt是區分java7和java8的,也存在依賴spring版本的情況。

自己嘗試了一下

在使用jasypt-spring-boot-starter的前提下

 

jasypt版本 springboot版本
2.1.0 2.1.0
1.5 1.4.2
1.5 1.5.3
1.8 1.4.2

 

所以如果引入maven之后啟動系統報錯,那么可以根據版本對應情況這個角度進行排查。

關鍵技術點
下面說一下jasypt的兩個關鍵的技術實現點

一是如何實現對spring環境中包含的PropertySource對象實現加密感知的

二是其默認的PBEWITHMD5ANDDES算法是如何工作的,並澄清一下在使用jasypt的時候最常遇到的一個疑問:既然你的password也配置在properties文件中,那么我拿到了加密的密文和password,不是可以直接解密嗎?

源碼解析
總結來說:其通過BeanFactoryPostProcessor#postProcessBeanFactory方法,獲取所有的propertySource對象,將所有propertySource都會重新包裝成新的EncryptablePropertySourceWrapper

解密的時候,也是使用EncryptablePropertySourceWrapper#getProperty方法,如果通過 prefixes/suffixes 包裹的屬性,那么返回解密后的值;如果沒有被包裹,那么返回原生的值。從源頭開始走起:

將jar包引入到spring boot中


spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ulisesbocchio.jasyptspringboot.JasyptSpringBootAutoConfiguration
這里補充一下spring boot @EnableAutoConfiguration的原理。

@EnableAutoConfiguration原理
@EnableAutoConfiguration注解@Import(AutoConfigurationImportSelector.class)

這個配置類實現了ImportSelector接口,重寫其selectImports方法

List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
getCandidateConfigurations方法,會從classpath中搜索所有META-INF/spring.factories配置文件,然后,將其中org.springframework.boot.autoconfigure.EnableAutoConfiguration key對應的配置項加載到spring容器中。這樣就實現了在spring boot中加載外部項目的bean或者第三方jar中的bean。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
其內部實現的關鍵點有:

1. ImportSelector 該接口的方法的返回值都會被納入到spring容器的管理中

2. SpringFactoriesLoader 該類可以從classpath中搜索所有META-INF/spring.factories配置文件,讀取配置

 

@EnableAutoConfiguration注解中有spring.boot.enableautoconfiguration=true就開啟,默認為true,可以在application.properties中設置此開關項

exclude()方法是根據類排除,excludeName是根據類名排除

 

在spring-boot-autoconfigure jar中,META-INF中有一個spring.factories文件,其中配置了spring-boot所有的自動配置參數,如GsonAutoConfiguration,配合@ConditionalOnClass(Gson.class),可以實現如果Gson bean存在,就啟動自動注入,否則就不啟用此注入的靈活配置

 

好了,有了上面的基礎知識,我們就關心JasyptSpringBootAutoConfiguration

JasyptSpringBootAutoConfiguration
其@Import EnableEncryptablePropertySourcesConfiguration

關注兩個地方

一是其@Import的StringEncryptorConfiguration.class
如果沒有自定義的EncryptorBean,即jasyptStringEncryptor bean,那么就注冊默認的jasyptStringEncryptor bean

@Conditional(OnMissingEncryptorBean.class)
@Bean(name = ENCRYPTOR_BEAN_PLACEHOLDER)
public StringEncryptor stringEncryptor(Environment environment) {
String encryptorBeanName = environment.resolveRequiredPlaceholders(ENCRYPTOR_BEAN_PLACEHOLDER);
LOG.info("String Encryptor custom Bean not found with name '{}'. Initializing String Encryptor based on properties with name '{}'",
encryptorBeanName, encryptorBeanName);
return new LazyStringEncryptor(() -> {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(getRequiredProperty(environment, "jasypt.encryptor.password"));
config.setAlgorithm(getProperty(environment, "jasypt.encryptor.algorithm", "PBEWithMD5AndDES"));
config.setKeyObtentionIterations(getProperty(environment, "jasypt.encryptor.keyObtentionIterations", "1000"));
config.setPoolSize(getProperty(environment, "jasypt.encryptor.poolSize", "1"));
config.setProviderName(getProperty(environment, "jasypt.encryptor.providerName", "SunJCE"));
config.setSaltGeneratorClassName(getProperty(environment, "jasypt.encryptor.saltGeneratorClassname", "org.jasypt.salt.RandomSaltGenerator"));
config.setStringOutputType(getProperty(environment, "jasypt.encryptor.stringOutputType", "base64"));
encryptor.setConfig(config);
return encryptor;
});
}
StringEncryptor接口提供了加密和解密的方法

我們可以自定義StringEncryptor,如

@Configuration
public class JasyptConfig {

@Bean(name = "jasypt.encryptor.bean:jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword("password");
config.setAlgorithm("PBEWithMD5AndDES");
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setProviderName("SunJCE");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
}
二是其對spring環境中包含的PropertySource對象的處理
@Configuration
@Import(StringEncryptorConfiguration.class)
public class EnableEncryptablePropertySourcesConfiguration implements EnvironmentAware {

private static final Logger LOG = LoggerFactory.getLogger(EnableEncryptablePropertySourcesConfiguration.class);
private ConfigurableEnvironment environment;

@Bean
public EnableEncryptablePropertySourcesPostProcessor enableEncryptablePropertySourcesPostProcessor() {
boolean proxyPropertySources = environment.getProperty("jasypt.encryptor.proxyPropertySources", Boolean.TYPE, false);
InterceptionMode interceptionMode = proxyPropertySources ? InterceptionMode.PROXY : InterceptionMode.WRAPPER;
return new EnableEncryptablePropertySourcesPostProcessor(environment, interceptionMode);
}

@Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
}
其提供了兩種模式來創建 分別為proxy和wrapper 默認情況下interceptionMode為wrapper

下面就是關鍵了,new了一個EnableEncryptablePropertySourcesPostProcessor

其implements BeanFactoryPostProcessor

這里又需要兩個背景知識

一是AbstractApplicationContext的refresh方法
是啟動spring容器的關鍵方法

// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
來注冊我們下面的postProcessors

二是BeanFactoryPostProcessor接口的作用
BeanFactoryPostProcessor接口提供了postProcessBeanFactory方法,在容器初始化之后執行一次

 

invokeBeanFactoryPostProcessors,獲取的手動注冊的BeanFactoryPostProcessor

/**
* Invoke the given BeanFactoryPostProcessor beans.
*/
private static void invokeBeanFactoryPostProcessors(
Collection<? extends BeanFactoryPostProcessor> postProcessors, ConfigurableListableBeanFactory beanFactory) {

for (BeanFactoryPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessBeanFactory(beanFactory);
}
}
可以看到postProcessors有4個

 

接下來看關鍵的EnableEncryptablePropertySourcesPostProcessor

EnableEncryptablePropertySourcesPostProcessor
public class EnableEncryptablePropertySourcesPostProcessor implements BeanFactoryPostProcessor, ApplicationListener<ApplicationEvent>, Ordered {
其實現了BeanFactoryPostProcessor以及Ordered接口

其中getOrder方法 讓這個jasypt定義的BeanFactoryPostProcessor的初始化順序最低,即最后初始化

我們知道spring中排序分為兩種PriorityOrdered 和Ordered接口,一般來說就是PriorityOrdered 優於Ordered 其次都是按照order大小來的排序

 

我們就知道了接下來就執行EnableEncryptablePropertySourcesPostProcessor的postProcessBeanFactory方法,

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
LOG.info("Post-processing PropertySource instances");
MutablePropertySources propSources = environment.getPropertySources();
StreamSupport.stream(propSources.spliterator(), false)
.filter(ps -> !(ps instanceof EncryptablePropertySource))
.map(s -> makeEncryptable(s, beanFactory))
.collect(toList())
.forEach(ps -> propSources.replace(ps.getName(), ps));
}
接下來,獲取所有的propertySource對象

 

然后用stream方式遍歷,如果是通過jasypt加密的,那么來執行方法makeEncryptable,使得propertySource對象具備加密解密的能力

private <T> PropertySource<T> makeEncryptable(PropertySource<T> propertySource, ConfigurableListableBeanFactory registry) {
StringEncryptor encryptor = registry.getBean(environment.resolveRequiredPlaceholders(ENCRYPTOR_BEAN_PLACEHOLDER), StringEncryptor.class);
PropertySource<T> encryptablePropertySource = interceptionMode == InterceptionMode.PROXY
? proxyPropertySource(propertySource, encryptor) : instantiatePropertySource(propertySource, encryptor);
LOG.info("Converting PropertySource {} [{}] to {}", propertySource.getName(), propertySource.getClass().getName(),
AopUtils.isAopProxy(encryptablePropertySource) ? "AOP Proxy" : encryptablePropertySource.getClass().getSimpleName());
return encryptablePropertySource;
}
首先獲取StringEncrypt Bean,然后執行instantiatePropertySource方法。

private <T> PropertySource<T> instantiatePropertySource(PropertySource<T> propertySource, StringEncryptor encryptor) {
PropertySource<T> encryptablePropertySource;
if (propertySource instanceof MapPropertySource) {
encryptablePropertySource = (PropertySource<T>) new EncryptableMapPropertySourceWrapper((MapPropertySource) propertySource, encryptor);
} else if (propertySource.getClass().getName().equals("org.springframework.boot.context.config.ConfigFileApplicationListener$ConfigurationPropertySources")) {
//Some Spring Boot code actually casts property sources to this specific type so must be proxied.
encryptablePropertySource = proxyPropertySource(propertySource, encryptor);
} else if (propertySource instanceof EnumerablePropertySource) {
encryptablePropertySource = new EncryptableEnumerablePropertySourceWrapper<>((EnumerablePropertySource) propertySource, encryptor);
} else {
encryptablePropertySource = new EncryptablePropertySourceWrapper<>(propertySource, encryptor);
}
return encryptablePropertySource;
}
可以看到將所有propertySource都會重新包裝成新的EncryptablePropertySourceWrapper

log日志:將上面的6個對象包裝一下

 

最后的application.properties中的配置項結果

 

完整的轉換完成后的EncryptablePropertySourceWrapper

 

到這里就注冊postProcessor完成了,而且每個PropertySource warpped,具備了加密解密的能力,然后繼續回到AbstractApplicationContext的流程

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
具體的解密過程
當spring boot項目啟動的時候,需要用到屬性值的時候,就是將原本spring中的propertySource的getProperty()方法委托給其自定義的實現EncryptablePropertySourceWrapper,調用其getProperty()方法,在這個方法的自定義實現中。判斷是否是已經加密的value,如果是,則進行解密。如果不是,那就返回原值。

 

調用EncryptablePropertySourceWrapper的getProperty方法,其extends PropertySource,override了getProperty方法

public class EncryptablePropertySourceWrapper<T> extends PropertySource<T> implements EncryptablePropertySource<T> {
private final PropertySource<T> delegate;
private final StringEncryptor encryptor;

public EncryptablePropertySourceWrapper(PropertySource<T> delegate, StringEncryptor encryptor) {
super(delegate.getName(), delegate.getSource());
Assert.notNull(delegate, "PropertySource delegate cannot be null");
Assert.notNull(encryptor, "StringEncryptor cannot be null");
this.delegate = delegate;
this.encryptor = encryptor;
}

@Override
public Object getProperty(String name) {
return getProperty(encryptor, delegate, name);
}
}
其getProperty就去調用其implements的EncryptablePropertySource的getProperty方法,於是執行下面

public interface EncryptablePropertySource<T> {
public default Object getProperty(StringEncryptor encryptor, PropertySource<T> source, String name) {
Object value = source.getProperty(name);
if(value instanceof String) {
String stringValue = String.valueOf(value);
if(PropertyValueEncryptionUtils.isEncryptedValue(stringValue)) {
value = PropertyValueEncryptionUtils.decrypt(stringValue, encryptor);
}
}
return value;
}
}
isEncryptedValue方法

private static final String ENCRYPTED_VALUE_PREFIX = "ENC(";
private static final String ENCRYPTED_VALUE_SUFFIX = ")";

public static boolean isEncryptedValue(final String value) {
if (value == null) {
return false;
}
final String trimmedValue = value.trim();
return (trimmedValue.startsWith(ENCRYPTED_VALUE_PREFIX) &&
trimmedValue.endsWith(ENCRYPTED_VALUE_SUFFIX));
}
如果通過 prefixes/suffixes 包裹的屬性,那么返回解密后的值;

如果沒有被包裹,那么返回原生的值;

 

如果是加密的值,那么就去解密

StandardPBEByteEncryptor

public byte[] decrypt(final byte[] encryptedMessage)
throws EncryptionOperationNotPossibleException {

if (encryptedMessage == null) {
return null;
}

// Check initialization
if (!isInitialized()) {
initialize();
}

if (this.saltGenerator.includePlainSaltInEncryptionResults()) {
// Check that the received message is bigger than the salt
if (encryptedMessage.length <= this.saltSizeBytes) {
throw new EncryptionOperationNotPossibleException();
}
}

try {

// If we are using a salt generator which specifies the salt
// to be included into the encrypted message itself, get it from
// there. If not, the salt is supposed to be fixed and thus the
// salt generator can be safely asked for it again.
byte[] salt = null;
byte[] encryptedMessageKernel = null;
if (this.saltGenerator.includePlainSaltInEncryptionResults()) {

final int saltStart = 0;
final int saltSize =
(this.saltSizeBytes < encryptedMessage.length? this.saltSizeBytes : encryptedMessage.length);
final int encMesKernelStart =
(this.saltSizeBytes < encryptedMessage.length? this.saltSizeBytes : encryptedMessage.length);
final int encMesKernelSize =
(this.saltSizeBytes < encryptedMessage.length? (encryptedMessage.length - this.saltSizeBytes) : 0);

salt = new byte[saltSize];
encryptedMessageKernel = new byte[encMesKernelSize];

System.arraycopy(encryptedMessage, saltStart, salt, 0, saltSize);
System.arraycopy(encryptedMessage, encMesKernelStart, encryptedMessageKernel, 0, encMesKernelSize);

} else if (!this.usingFixedSalt){

salt = this.saltGenerator.generateSalt(this.saltSizeBytes);
encryptedMessageKernel = encryptedMessage;

} else {
// this.usingFixedSalt == true

salt = this.fixedSaltInUse;
encryptedMessageKernel = encryptedMessage;

}


final byte[] decryptedMessage;
if (this.usingFixedSalt) {

/*
* Fixed salt is being used, therefore no initialization supposedly needed
*/
synchronized (this.decryptCipher) {
decryptedMessage =
this.decryptCipher.doFinal(encryptedMessageKernel);
}

} else {

/*
* Perform decryption using the Cipher
*/
final PBEParameterSpec parameterSpec =
new PBEParameterSpec(salt, this.keyObtentionIterations);

synchronized (this.decryptCipher) {
this.decryptCipher.init(
Cipher.DECRYPT_MODE, this.key, parameterSpec);
decryptedMessage =
this.decryptCipher.doFinal(encryptedMessageKernel);
}

}

// Return the results
return decryptedMessage;

} catch (final InvalidKeyException e) {
// The problem could be not having the unlimited strength policies
// installed, so better give a usefull error message.
handleInvalidKeyException(e);
throw new EncryptionOperationNotPossibleException();
} catch (final Exception e) {
// If decryption fails, it is more secure not to return any
// information about the cause in nested exceptions. Simply fail.
throw new EncryptionOperationNotPossibleException();
}

}
以spring.datasource.username為例:

明文是root

密文是ENC(X4OZ4csEAWqPCEvWf+aRPA==)

 

可以看到其salt是encryptedMessage的

System.arraycopy(encryptedMessage, saltStart, salt, 0, saltSize);
System.arraycopy(encryptedMessage, encMesKernelStart, encryptedMessageKernel, 0, encMesKernelSize);
0-7byte解析為salt,8-15byte解析為密文

 

然后就通過基本的PBE解析方式,來解析出來

 

ASCII碼對應的結果就是root

 

PBE解析原理圖:

加密過程:每一次隨機產生新的salt,所以每一次加密后生成的密文是不同的

 

解密過程:

 

所以我們就可以知道,如果我獲得了jasypt的password,那么由於其salt是放在encryptedMessage中的,那么我是沒什么壓力就可以解密的。

所以應該java -jar –Djasypt.encryptor.password=xxx abc.jar方式來啟動服務。這樣只要在運維端不泄露password,那么只拿到配置文件的密文,還是安全的。

補充1:查看JDK提供的Cipher算法
jasypt默認使用的是PBEWITHMD5ANDDES,其實JDK中由SunJCE所提供的。

可以通過下面的代碼來查看JDK中提供了哪些Cipher算法

@Test
public void listJdkAlgorithm() {
/* Provider[] providers = Security.getProviders();
for (Provider provider :
providers) {
LOGGER.info("security provider: {} , version: {}", provider.getName(), provider.getVersion());
LOGGER.info("security provider info: {}", provider.getInfo());
}*/
Set<String> messageDigest = Security.getAlgorithms("Cipher");
for (String s :
messageDigest) {
LOGGER.info("MessageDigest: {}",s);
}
}
更全面的安全方面的算法,如摘要算法、簽名算法等,參考:

Standard Algorithm Name Documentation

補充2:PBE的基礎算法demo,
而且可以看出來,jasypt中使用了幾乎相同的代碼來進行加解密的

public class PBECipher {

static final String CIPHER_NAME = "PBEwithMD5AndDES";

public static byte[] encrypt(String password, byte[] salt, byte[] input) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(CIPHER_NAME);
// 這個secretKey 就是我們將來要使用的加密的密鑰
SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
// 傳入1000,表示用戶輸入的口令,會與這個salt進行1000次的循環
PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec);
return cipher.doFinal(input);
}

public static byte[] decrypt(String password, byte[] salt, byte[] input) throws NoSuchAlgorithmException,
InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(CIPHER_NAME);
SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance(CIPHER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
return cipher.doFinal(input);
}
}
測試

@Test
public void testPBE() throws NoSuchAlgorithmException, UnsupportedEncodingException, NoSuchPaddingException,
InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException {
String message = "constfafa";
String password = "ydbs";
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(8);
System.out.printf("salt: %032x\n", new BigInteger(1, salt));

//加密和解密的salt是一樣的
byte[] data = message.getBytes("UTF-8");
byte[] encrypt = PBECipher.encrypt(password, salt, data);
LOGGER.info("encrypted data: {}", Base64.getEncoder().encodeToString(encrypt));

byte[] decrypt = PBECipher.decrypt(password, salt, encrypt);
LOGGER.info("decrypted data: {}", new String(decrypt,"UTF-8"));
}
參考:
Jasypt之源碼解析

官方github

8.Java 加解密技術系列之 PBE - crazyYong - 博客園
————————————————
版權聲明:本文為CSDN博主「const伐伐」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u013905744/article/details/86508236

 

@Import注解的應用和擴展

將一個對象交給Spring來管理,有三種做法:
1、@Bean
2、@Componet(@Service等歸為一類)
3、@Import
這里主要講第三種做法,打開Spring源碼

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    Class<?>[] value();
}

@Import注解只有一個value方法,注釋中指明該注解必須作用於@Configuration定義的類上,value可以為想要交給Spring管理的類文件數組、ImportSelector或ImportBeanDefinitionRegistrar,接下來我們依次執行三種做法

  • 1、指定class數組
    首先定義兩個類
    public class Apple {
    }
    public class Banana {
    }

然后定義配置類,並用@Import注解裝飾,輸入兩個自定義類

import com.lwl.entity.Apple;
import com.lwl.entity.Banana;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({Apple.class, Banana.class})
public class AppConfig {
}

 

測試類中打印容器中類的名稱

public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        for (String s : applicationContext.getBeanDefinitionNames()) {
            System.out.println(s);
        }
    }
}

 

輸出結果中可以看到Apple和Banana都被成功注入:

 
結果1
  • 2、實現ImportSelector接口
    定義一個新的實體,需求是通過ImportSelector將其注入Spring容器
    public class Berry {
    }
 

自定義selector實現ImportSelector接口,在方法中返回自定義的類路徑,Spring會自動將該路徑下的類注入到容器中

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class BerryImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{"com.lwl.entity.Berry"};
    }
}

 

修改配置代碼,在@Import中加入BerryImportSelector :

@Configuration
@Import({Apple.class, Banana.class, BerryImportSelector.class})
public class AppConfig {
}

 

測試代碼不變,打印結果:


 
結果2

Berry確實被注入進來了

  • 3、實現ImportBeanDefinitionRegistrar接口
    再定義一個新的實體:
    public class Tomato {
    }
 

創建TomatoRegistrar實現ImportBeanDefinitionRegistrar接口,在方法當中將類注冊到容器里,並將beanName修改為MyTomato:

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class TomatoRegistrar implements ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition(Tomato.class);
        beanDefinitionRegistry.registerBeanDefinition("MyTomato", beanDefinition);
    }
}

 

 

修改AppConfig代碼,將TomatoRegistrar放入@Import中:

@Configuration
@Import({Apple.class, Banana.class, BerryImportSelector.class, TomatoRegistrar.class})
public class AppConfig {
}

測試結果:

 
結果3

總結

在平時的業務開發當中,將對象放入容器,使用@Bean和@Compont基本就能夠滿足需求,但是@Import注解能夠方便擴展功能,舉例:

  • 1、控制類注入時機
    我希望能夠通過一個簡單的開關來控制是否注入Berry類,我們可以定義一個注解
    import org.springframework.context.annotation.Import;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Import({BerryImportSelector.class})
    public @interface EnableBerry {
    }
 

修改@AppConfig,刪除@Import中的BerryImportSelector.class,

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({Apple.class, Banana.class, TomatoRegistrar.class})
public class AppConfig {
}

 

 

這時候運行測試,發現Berry沒有被注入到容器中:

 
image.png

如果在AppConfig類加上@EnableBerry注解

@Configuration
@Import({Apple.class, Banana.class, TomatoRegistrar.class})
@EnableBerry
public class AppConfig {
}
 

再次執行測試,Berry成功注入:


 
image.png

SpringCloud中的@EnableEureka、@EnableDiscoveryClient就是利用這個原理

  • 2、通過代理來改變bean定義
    Spring-Mybatis的@MapperScan注解,是由@Import注解所修飾,並注入了MapperScannerRegistrar類:
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Documented
    @Import({MapperScannerRegistrar.class})
    public @interface MapperScan {
 

它在registerBeanDefinitions方法中掃描了基礎包,

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
 scanner.doScan(StringUtils.toStringArray(basePackages));
}

然后提取mapper產生代理類,最后注冊到容器當中


作者:擋不住的柳Willow
鏈接:https://www.jianshu.com/p/e6b44d8cec5a
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
 
 

@Repeatable 注解用於指示它注解聲明的注解類型是可重復的。@Repeatable 的值用於指示一個注解類型,這個注解類型用來存放可重復的注解類型。

初次看這段文字時,覺得比較難以理解,經過思考,我認為 用戶-角色場景可以通俗的解釋 @Repeatable 注解。

功能描述
一個系統中可以設定多個角色,每個角色我們稱之為 Role,系統定義的角色如下:

系統管理員:system_admin
業務管理員:biz_admin
客戶:custom
一個用戶(User)可以擁有其中的一個或者多個角色,用戶擁有的角色列表我們稱之為 Roles,假設有兩個用戶 User1、User2 ,他們的權限分別如下:

User1:system_admin
User2 :biz_admin、custom
通過 @Repeatable 注解來實現以上功能

定義角色注解 Role

package org.learn.annotation;


import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Roles.class)
public @interface Role {
    String value() default "";
}

這里需要說明 @Repeatable(Roles.class),它指示在同一個類中 @Role 注解是可以重復使用的,重復的注解被存放至 @Roles 注解中。

定義角色列表注解 Roles

package org.learn.annotation;


import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Roles {
    Role[] value();
}

@Roles 注解是如何存放 @Role 注解的呢?它定義了 Role[] value(); 用來存放可重復的注解。

這里提出一個問題,如果在同一個類中只有一個可重復的 @Role 注解,那這個值會被存入 @Roles 注解中嗎?

定義 User1

package org.learn.annotation;

/**
 * @author zhibo
 * @date 2019/5/31 15:03
 */

@Role("system_admin")
public class User1 {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

定義 User2

package org.learn.annotation;

/**
 * @author zhibo
 * @date 2019/5/31 15:03
 */

@Role("biz_admin")
@Role("custom")
public class User2 {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

進行測試

package org.learn.annotation;

/**
 * @author zhibo
 * @date 2019/5/31 15:00
 */
public class RepeatableDemo {

    public static void main(String[] args) {
        if(User1.class.isAnnotationPresent(Roles.class)){
            Roles roles = User1.class.getAnnotation(Roles.class);
            System.out.println("User1的角色如下:");
            for (Role role : roles.value()){
                System.out.println(role.value());
            }
        }

        if(User2.class.isAnnotationPresent(Roles.class)){
            Roles roles = User2.class.getAnnotation(Roles.class);
            System.out.println("User2的角色如下:");
            for (Role role : roles.value()){
                System.out.println(role.value());
            }
        }
    }
}

執行 main 方法,輸出如下:

 

 

從執行結果中可以看到 User2 的角色列表,通過注解的值我們可以進行用戶角色判定。

同時可以看到 User1 的角色是@Role("system_admin"),但是 User1 的角色沒有被輸出,在加上一個 Role 的話,就可以輸出角色了。由此可見,如果只聲明了一個注解 Role(被 @Repeatable 聲明的注解),那么注解值是不會被存放至 Roles 注解中的,測試類中不會存在 Roles 注解。

解惑
修改 User1 的代碼,為其增加 @Role("custom") 角色:

package org.learn.annotation;

/**
 * @author zhibo
 * @date 2019/5/31 15:03
 */

@Role("system_admin")
@Role("custom")
public class User1 {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

再次執行 main 方法,結果如下:

 

https://blog.csdn.net/claram/article/details/90717270

 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM