目的
Nacos作為SpringBoot服務的注冊中心和配置中心。
在NacosServer中修改配置文件,在SpringBoot不重啟的情況下,獲取到修改的內容。
本例將在配置文件中配置一個 cml.age=100 的配置項,程序中編寫一個方法讀取配置文件,並通過 Get--->/test/age 接口提供給瀏覽器訪問。
- 若配置文件中的 age 修改為 200 ,不用重新啟動程序,直接訪問 /test/age 接口,將獲取到最新的值 200
- 若配置文件中沒有age 的配置項,或干脆沒有 cml 的配置項,訪問 /test/age 接口將返回默認的值 18
環境
- SpringCloud:2020.0.3
- SpringCloudAlibaba:2021.1
- SpringBoot:2.5.2
pom
pom中引入 nacos 相關配置:discovery,config,bootstrap
網上有人說,需要引入 actuator ,其實不用。也沒有 SpringSecurity 攔截的問題,NacosClient 端定時從 NacosServer 端拉取最新配置,以http方式請求。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.dependencies}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.dependencies}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- nacos-discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
</dependencies>
配置文件
bootstrap.yml
server:
port: 9556
spring:
application:
name: app
profiles:
active: test
nacos:
discovery:
username: nacos
password: nacos
server-addr: 192.168.1.61:8848
config:
server-addr: 192.168.1.61:8848
file-extension: yaml
app-dev.yaml
此配置指 NacosServer 中的配置文件 app-dev.yaml ,僅截取 cml.age 部分
cml:
age: 100
代碼
- RefreshScope注解:必須加在 controller 上面,加在主啟動內上面不好使。哪些資源需要自動刷新配置就在該controller上面添加此注解,可封裝一個 BaseController 。
- @Value("${cml.age:18}"):讀取配置文件中的 cml.age 配置項值,賦給變量 age ,默認值為 18
- getAge:獲取年齡接口
- /test/age接口需要添加到 Security.permitAll
問題:RefreshScope注解為什么一定要添加在 controller 上面?為什么在主啟動類上面添加不生效
@RefreshScope
@Api(tags = "測試 - api")
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
/**
* 獲取配置文件中的 cml.age 內容,若未獲取到,默認值為18
*/
@Value("${cml.age:18}")
private String age;
@ApiOperation(value = "獲取年齡 - 測試配置自動刷新", notes = "獲取年齡 - 測試配置自動刷新")
@GetMapping("/age")
public String getAge() {
return age;
}
}
日志
開啟 nacos-refresh 日志,打印配置內容更新情況
logging:
level:
com.alibaba.cloud.nacos.refresh: debug
打印的日志:
2022-01-28 13:43:30.574 [com.alibaba.nacos.client.Worker.longPolling.fixed-192.168.1.61_8848-zjrkm-admin] DEBUG com.alibaba.cloud.nacos.refresh.NacosContextRefresher.innerReceive:136 - Refresh Nacos config group=DEFAULT_GROUP,dataId=identityQrCodeAdmin-service-cml-test.yaml,configInfo=spring:
application:
name:
.
.
.
.
.
測試
在不重啟SpringBoot服務的情況,多次在 NacosServer 中修改 cml.age 配置項的值,然后通過瀏覽器訪問 /test/age 接口,發現每次都可以獲取到最新的 age 值。
特殊的情況
當項目集成了 SpringSecurity 的時候,如果在 UserDetailsService 實現類上添加 RefreshScope 注解,並在類中直接使用 @Value 獲取配置文件的內容,會導致用戶在登錄的時候找不到該類,從而無法調用loadUserByUsername 方法做用戶名密碼校驗,進而登錄功能無法使用。
解決方案是:
- 新建一個配置類(假設類名為 Account ),使用 ConfigurationProperties 將需要的配置注入到類屬性;並在類上添加 RefreshScope 注解
- 將 Account 類注入容器,UserDetailsService 實現類中將 Account 類 Autowired 進來再使用
核心代碼:
Account 類:
import lombok.Data;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.io.Serializable;
@RefreshScope
@Component
@ConfigurationProperties(prefix = "cml.admin")
@Data
@ToString
public class Account implements Serializable {
private String defaultUserName;
private String defaultPassword;
private String defaultUserName1;
private String defaultPassword1;
}
UserDetailsService 實現類:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private Account account;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (account.getDefaultUserName().equals(username)) {
String password = passwordEncoder.encode(account.getDefaultPassword());
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
if (account.getDefaultUserName1().equals(username)) {
String password = passwordEncoder.encode(account.getDefaultPassword1());
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin1"));
}
return null;
}
}