SpringBoot几个机制总结

1. 前言

本周写了一个自定义的认证、鉴权功能,发现以前SpringBoot的使用记录对于查找问题并不友好,这里对这几篇文章重新进行整理。分成以下几大部分:

  1. 存储

  2. 传输

  3. Spring机制

  4. 缓存组件

本文S是应用过程中几个机制的小总结,包括Test、拦截器、Runner

2. Test

spring boot test( spring-boot-starter-test)包括很详细的测试,测试的两个主要概念:Unit Test与Integration Test。

Unit Test(单元)指的是对某一层进行测试,如controller层,而mock service来测试。

Integration Test(集成)就是正常的测试了。

mvn测试全部: mvn test

测试某个类: mvn test -Dtest=ProjectServiceTest

2.1 controller的集成测试

直接写代码吧

@SpringBootTest
class ProjectControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @BeforeEach
    public void setUp(){
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    @Test
    void createProject() throws Exception{
        ProjectDto dto = new ProjectDto(null,"forTest1", "描述", null);

        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/api/project/")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .accept(MediaType.APPLICATION_JSON)
                .content(JsonUtil.toJsonString(dto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value(dto.getName()))
                .andDo(print())
                .andReturn();

        String r = result.getResponse().getContentAsString();
        Project p = JsonUtil.toObject(r, Project.class);
        System.out.print(p);

    }
}

这里mockMvc.perform去执行,对返回用andExpect进行验证,andDo()进行打印,andReturn,转换成结果。
一般可以用jsonPath对返回数据进行验证即可。少数时候想看一下,用转换成对象来看。

2.2 controller的单元测试

@WebMvcTest(ProjectController.class)
public class ProjectControllerUnitTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ProjectService projectService;

    @Test
    public void createProjectTest() throws Exception{

        ProjectDto dto = new ProjectDto(null, "测试", "描述", null);
        Project project = new Project(3L, "测试", "描述", ProjectState.ON);
        given(this.projectService.save(Mockito.any(ProjectDto.class)))
                .willReturn(project);

        mockMvc.perform(MockMvcRequestBuilders.post("/api/project/")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .accept(MediaType.APPLICATION_JSON)
                .content(JsonUtil.toJsonString(dto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(3))
                .andDo(print());
    }   
}

这里跟controller很像,但对service层进行mock,用give + willReturn来指定输入、输出。

2.3 service的集成测试

@SpringBootTest
public class ProjectServiceIntegrationTest {

    @Autowired
    private ProjectService projectService;

    @Test
    void saveTest(){
        ProjectDto dto = new ProjectDto(null, "saveTest", "描述", null);
        Project project = projectService.save(dto);
        assertEquals(project.getName(), dto.getName());
        System.out.print(project);
    }
}

这个比较简单,不解释了

2.4 问题

  • 无法测试问题

    提示:java.lang.IllegalStateException: Could not load TestContextBootstrapper [null]. Specify @BootstrapWith's 'value' attribute or make the default bootstrapper class available.

    这种情况是由于spring-webmvc与spring-test的版本不一致导致。

    测试版本问题

  • 注入失败问题

    测试中发现,Autowired的对象为null,导致测试失败。

    解决方法:在测试类上增加了以下几个注解(ActiveProfiles可以不加)

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest
    @WebAppConfiguration
    @ActiveProfiles("dev")
    public class ApplicationTests {}
    

2.5 参考

Junit5

spring boot test

sping framework test

3. Intercepter

拦截器的基本用法就是实现HandlerInterceptor接口,然后通过再在配置中,add进InterceptorRegistry,并设置匹配的path即可,这里不做说明了。

这次做的功能是认证与鉴权,结合它们来看看拦截器。

3.1 注解

通过@interface声明一个注解

@Retention(RUNTIME)
@Target(METHOD)
public @interface AuthPermission {
    String[] value() default {};
}

这个注解中有个属性:value,在使用的时候,就可以设置该值了:

public class TestController{
    @AuthPermission(value = {"ORG"})
    @GetMapping("/org/{orgId}/test1")
    public MyResponseBody readCookie(@PathVariable("orgId")Long orgId, HttpServletRequest request, @ApiIgnore HttpSession session){
    }
}	

3.2 拦截器中判断注解

拦截器中获取注解使用过HandlerMethod的getMethodAnnotation()函数来获取的

@Component
public class AuthorizationInterceptor implements HandlerInterceptor {
      @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof HandlerMethod){
            HandlerMethod h = (HandlerMethod)handler;
            // 获取到注解对象
            AuthPermission authPermission = h.getMethodAnnotation(AuthPermission.class);
            if(authPermission == null){
                return true;
            }
            // 获取注解中的value
            String[] needPermissionList = authPermission.value();
            // ....其他操作...
        }
    }
}

3.3 拦截器中获取PathVariable

在Controller中,可以通过@PathVariable来获取path中的变量,在拦截器中,则通过request的getAttribute

 @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            Map<String, String> pathVarsMap = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            if(pathVarsMap == null){
                return true;
            }
            String param = pathVarsMap.get("orgId");
            
        }
    }

这里的URI_TEMPLATE_VARIABLES_ATTRIBUTE是这样一个值:

String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";

得到Map之后,再通过要取的字符串来获取相应参数.

参考1

参考2

4. Runner

本次,在程序启动后,我们想将一些基础的数据初始化到数据库中,这里我们使用了ApplicationRunner的方式来做启动后执行。springboot给我们提供了两种Runner方式:ApplicationRunner和CommandLineRunner。前者可以用来接收字符串数组的命令行参数,后者是使用ApplicationArguments 用来接收参数的。

@Component
public class DataRunner implements ApplicationRunner {
    @Value("${auth.prepare-data}")
    private boolean prepareData;
    
    //...ignore...
}

另外我们用prepare-data配置来判断是否需要准备数据,当然,也可以增加读库,如果没有就准备,如果有就不准备了。

参考

# spring 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×