You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

测试Keycloak用户创建时遭遇RESTEASY004645空指针异常

Keycloak用户创建测试空指针异常排查

异常信息

java.lang.NullPointerException: RESTEASY004645: templateValues entry was null
        at com.springboot.services.KeycloakAdminClientServiceTest.createKeycloakUserTest(KeycloakAdminClientServiceTest.java:80)

问题背景

我正在为AdminClientService编写测试用例,该服务用于向Keycloak数据库创建用户。主业务流程运行正常,但编写测试时触发上述空指针异常。曾尝试用Mockito模拟KeycloakProvider,但未解决问题。


相关代码

测试类代码

@SpringBootTest
public class KeycloakAdminClientServiceTest {
    @Value("${keycloak.auth-server-url}")
    public String serverUrl;
    @Value("${keycloak.realm}")
    public String realm;
    @Value("${keycloak.resource}")
    public String clientId;
    @Value("${keycloak.credentials.secret}")
    public String clientSecret;
    
    // @Mock
    // KeycloakProvider kcProvider = Mockito.mock(KeycloakProvider.class);
    
    // @InjectMocks
    // 
    
    // @BeforeEach
    // void setMockOutput() {
    //     //when(helloService.get()).thenReturn("Hello Mockito");
    //     Mockito.when(kcProvider.getInstance()).thenReturn(KeycloakBuilder.builder()
    //         .realm(realm)
    //         .serverUrl(serverUrl)
    //         .clientId(clientId)
    //         .clientSecret(clientSecret)
    //         .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
    //         .build());
    // }
    KeycloakProvider kcProvider;
    KeycloakAdminClientService kcAdminClientService;
    
    @BeforeEach
    void createKeycloakProvider() {
        kcProvider = new KeycloakProvider();
        kcProvider.setServerUrl(serverUrl);
        kcProvider.setRealm(realm);
        kcProvider.setClientId(clientId);
        kcProvider.setClientSecret(clientSecret);
        kcAdminClientService = new KeycloakAdminClientService(kcProvider);
    }
    
    @Test
    public void createKeycloakUserTest(){
        Integer generatedInteger = (int) ((Math.random() * (9999999 - 1000000)) + 1000000);
        String generatedString = generatedInteger.toString();

        CreateUserRequest createUserRequest = new CreateUserRequest();
        createUserRequest.setUsername("admin_client_username_" + generatedString);
        createUserRequest.setPassword("admin_client_password");
        createUserRequest.setEmail("admin_client_email_" + generatedString + "@gmail.com");
        createUserRequest.setFirstname("admin_client_firstname_" + generatedString);
        createUserRequest.setLastname("admin_client_lastname_" + generatedString);
        
        kcAdminClientService.createKeycloakUser(createUserRequest);
        
        UsersResource usersResource = kcProvider.getInstance()
            .realm(kcAdminClientService.realm).users();
        List<UserRepresentation> userRepresentation = usersResource
            .search("admin_client_email_" + generatedString + "@gmail.com");
        assertNotNull(userRepresentation);
        assertEquals("admin_client_firstname_" + generatedString,
            userRepresentation.get(0).getFirstName());

    }

}

服务类代码

@Service
public class KeycloakAdminClientService {
    @Value("${keycloak.realm}")
    public String realm;

    private final KeycloakProvider kcProvider;


    public KeycloakAdminClientService(KeycloakProvider keycloakProvider) {
        this.kcProvider = keycloakProvider;
    }

    public Response createKeycloakUser(CreateUserRequest user) {
        UsersResource usersResource = kcProvider.getInstance().realm(realm).users();
        CredentialRepresentation credentialRepresentation = createPasswordCredentials(
            user.getPassword());

        UserRepresentation kcUser = new UserRepresentation();
        kcUser.setUsername(user.getEmail());
        kcUser.setCredentials(Collections.singletonList(credentialRepresentation));
        kcUser.setFirstName(user.getFirstname());
        kcUser.setLastName(user.getLastname());
        kcUser.setEmail(user.getEmail());
        kcUser.setEnabled(true);
        kcUser.setEmailVerified(false);
        Response response = usersResource.create(kcUser);

        return response;

    }

    private static CredentialRepresentation createPasswordCredentials(String password) {
        CredentialRepresentation passwordCredentials = new CredentialRepresentation();
        passwordCredentials.setTemporary(false);
        passwordCredentials.setType(CredentialRepresentation.PASSWORD);
        passwordCredentials.setValue(password);
        return passwordCredentials;
    }
}

KeycloakProvider代码

@Configuration
@Getter
@Setter
public class KeycloakProvider {
    @Value("${keycloak.auth-server-url}")
    public String serverUrl;
    @Value("${keycloak.realm}")
    public String realm;
    @Value("${keycloak.resource}")
    public String clientId;
    @Value("${keycloak.credentials.secret}")
    public String clientSecret;

    private static Keycloak keycloak = null;

    public KeycloakProvider() {
    }

    public Keycloak getInstance() {
        if (keycloak == null) {

            return KeycloakBuilder.builder()
                    .realm(realm)
                    .serverUrl(serverUrl)
                    .clientId(clientId)
                    .clientSecret(clientSecret)
                    .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
                    .build();
        }
        return keycloak;
    }

    public KeycloakBuilder newKeycloakBuilderWithPasswordCredentials(
        String username,
        String password) {
        return KeycloakBuilder.builder()
                .realm(realm)
                .serverUrl(serverUrl)
                .clientId(clientId)
                .clientSecret(clientSecret)
                .username(username)
                .password(password);
    }

    public JsonNode refreshToken(String refreshToken) throws UnirestException {
        String url = serverUrl + "/realms/" + realm + "/protocol/openid-connect/token";
        return Unirest.post(url)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .field("client_id", clientId)
                .field("client_secret", clientSecret)
                .field("refresh_token", refreshToken)
                .field("grant_type", "refresh_token")
                .asJson().getBody();
    }
}

异常排查与解决方案

异常原因分析

RESTEASY004645: templateValues entry was null本质是Keycloak客户端构建时传入了null参数,导致RESTEasy在拼接请求URL模板时出错。结合代码来看,大概率是以下两种情况:

  1. 测试环境配置缺失:@Value注入的serverUrl/realm/clientId/clientSecret为空(测试配置文件未配置对应属性)
  2. 手动实例化KeycloakProvider的逻辑问题:静态变量keycloak在多测试场景下状态污染,或参数传递未生效

解决方案

方案1:补全测试环境配置

确保测试环境的配置文件(如application-test.yml)中完整配置Keycloak参数:

keycloak:
  auth-server-url: http://your-keycloak-url:8080
  realm: your-realm-name
  resource: your-client-id
  credentials:
    secret: your-client-secret

方案2:改用Mockito隔离外部依赖(推荐)

单元测试应避免调用真实Keycloak服务,通过Mock完全隔离依赖:

@SpringBootTest
public class KeycloakAdminClientServiceTest {
    @Mock
    private KeycloakProvider kcProvider;
    @Mock
    private Keycloak keycloak;
    @Mock
    private RealmResource realmResource;
    @Mock
    private UsersResource usersResource;
    
    @InjectMocks
    private KeycloakAdminClientService kcAdminClientService;
    
    @Value("${keycloak.realm}")
    private String realm;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        // 模拟Keycloak调用链
        when(kcProvider.getInstance()).thenReturn(keycloak);
        when(keycloak.realm(realm)).thenReturn(realmResource);
        when(realmResource.users()).thenReturn(usersResource);
        // 模拟用户创建成功响应
        when(usersResource.create(any(UserRepresentation.class)))
                .thenReturn(Response.created(URI.create("/users/123")).build());
    }
    
    @Test
    public void createKeycloakUserTest(){
        Integer generatedInteger = (int) ((Math.random() * (9999999 - 1000000)) + 1000000);
        String generatedString = generatedInteger.toString();

        CreateUserRequest createUserRequest = new CreateUserRequest();
        createUserRequest.setUsername("admin_client_username_" + generatedString);
        createUserRequest.setPassword("admin_client_password");
        createUserRequest.setEmail("admin_client_email_" + generatedString + "@gmail.com");
        createUserRequest.setFirstname("admin_client_firstname_" + generatedString);
        createUserRequest.setLastname("admin_client_lastname_" + generatedString);
        
        Response response = kcAdminClientService.createKeycloakUser(createUserRequest);
        
        // 验证响应状态
        assertEquals(HttpStatus.CREATED.value(), response.getStatus());
        // 验证用户参数是否正确传递
        ArgumentCaptor<UserRepresentation> userCaptor = ArgumentCaptor.forClass(UserRepresentation.class);
        verify(usersResource).create(userCaptor.capture());
        UserRepresentation capturedUser = userCaptor.getValue();
        assertEquals(createUserRequest.getEmail(), capturedUser.getUsername());
        assertEquals(createUserRequest.getFirstname(), capturedUser.getFirstName());
        assertEquals(createUserRequest.getLastname(), capturedUser.getLastName());
        assertTrue(capturedUser.isEnabled());
    }
}

优化建议:调整服务类注入方式

将服务类中realm的@Value注入改为构造函数注入,提升测试灵活性:

@Service
public class KeycloakAdminClientService {
    private final String realm;
    private final KeycloakProvider kcProvider;

    public KeycloakAdminClientService(KeycloakProvider keycloakProvider, @Value("${keycloak.realm}") String realm) {
        this.kcProvider = keycloakProvider;
        this.realm = realm;
    }

    // 其余方法不变...
}

内容的提问来源于stack exchange,提问作者MrMRas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 03:50:47