springboot指定上下文_springboot用户上下文注入

版本概要

springboot版本2.1.7.RELEASE

kotlin版本1.2.71

gradle版本5.2.1

idea版本2019.1.2 ultimate edition

新建项目点击file -> new project ->选择spring initializrd点击下一步

选择语言,选择项目管理工具

此篇讨论我们只进行数据模拟,不涉及实际数据,只引入springWebStarter进行请求测试即可

选择gradle路径(或者使用默认的),这里我选择本地路径

增加国内镜像地址

追加根节点 1

2

3

4repositories {

maven (url = "http://maven.aliyun.com/nexus/content/groups/public/")

jcenter()

}

增加fastJson依赖用以序列化

在dependencies中追加 1implementation("com.alibaba:fastjson:1.2.59")

重新导入等待编译完成

控制器注入

使用方法解析器,我们能够在控制器中有选择的解析并注入参数

用户上下文对象和标记注解:1

2

3

4

5data class UserContext(val userId: String, val username: String)

@Target(AnnotationTarget.VALUE_PARAMETER)

@Retention(AnnotationRetention.RUNTIME)

annotation class CurrentUser

配置方法解析器,给加上标记注解的UserContext对象自动解析请求头中的json信息并注入1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17class CurrentUserMethodArgumentResolver : HandlerMethodArgumentResolver {

/**

* 符合条件才进入此参数解析器

*/

override fun supportsParameter(parameter: MethodParameter): Boolean {

return parameter.parameterType.isAssignableFrom(UserContext::class.java)

&& parameter.hasParameterAnnotation(CurrentUser::class.java)

}

/**

* 参数解析并注入对象

*/

override fun resolveArgument(parameter: MethodParameter, mavContainer: ModelAndViewContainer?, webRequest: NativeWebRequest, binderFactory: WebDataBinderFactory?): Any? {

val userJson = webRequest.getHeader("user-test")

return JSON.parseObject(userJson, UserContext::class.java)

}

}

启用方法解析器1

2

3

4

5

6

7

8

9

10@Configuration

class SpringConfig : WebMvcConfigurer {

/**

* 加入解析器列表

*/

override fun addArgumentResolvers(resolvers: MutableList) {

super.addArgumentResolvers(resolvers)

resolvers.add(CurrentUserMethodArgumentResolver())

}

}

测试控制器1

2

3

4

5

6

7

8@SpringBootApplication

@RestController

class DemoApplication{

@GetMapping("/getArgument")

fun getArgument(@CurrentUser userContext: UserContext):UserContext {

return userContext

}

}

编写WebMvc测试类测试结果1

2

3

4

5

6

7

8

9

10

11

12

13

14

15@RunWith(SpringRunner::class)

@WebMvcTest(DemoApplication::class)

class WebMvcTest{

private val log = LoggerFactory.getLogger(this.javaClass)

@Autowired

lateinit var mockMvc: MockMvc

@Test

fun testGetArgument() {

val json = "{\"username\":\"测试\", \"userId\":\"测试\"}"

mockMvc.perform(MockMvcRequestBuilders.get("/getArgument")

.header("user-test", json))

.andExpect(status().isOk).andDo { log.info("返回结果 ${it.response.contentAsString}") }.andReturn()

}

}

测试结果

使用此方式我们可以很方便在需要时将用户上下文注入控制器中,并且只有需要时,才会进行参数解析

静态方法获取

使用构造器注入的方式,不方便之处在于当我们在service层需要使用时,只能一层一层的向内传,对我们的方法参数造成的一定程度上的污染,我们可以利用线程安全的ThreadLocal对象在每次请求时存储用户上下文

RequestContext对象1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18object RequestContext {

/**

* 用户上下文

*/

private val userContextThreadLocal: ThreadLocal = ThreadLocal()

fun setUserContext(userContext: UserContext) {

userContextThreadLocal.set(userContext)

}

fun getUserContext(): UserContext {

return userContextThreadLocal.get()

}

fun removeUserContext() {

userContextThreadLocal.remove()

}

}

使用spring提供的HandlerInterceptor接口我们可以跟踪请求,解析参数,并及时释放本地线程中的对象1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21class RequestInterceptor : HandlerInterceptor {

/**

* 如果让请求继续执行则返回true

*/

override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {

val userJson = request.getHeader("user-test")

if (userJson.isNullOrBlank()) {

return true

}

RequestContext.setUserContext(JSON.parseObject(userJson, UserContext::class.java))

return super.preHandle(request, response, handler)

}

/**

* 请求结束时移除上下文,抛出异常也会执行

*/

override fun afterCompletion(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception?) {

RequestContext.removeUserContext()

}

}

在spring中配置此拦截器1

2

3

4

5

6

7/**

* 加入拦截器列表

*/

override fun addInterceptors(registry: InterceptorRegistry) {

super.addInterceptors(registry)

registry.addInterceptor(RequestInterceptor())

}

测试控制器1

2

3

4

5@GetMapping("/getStatic")

fun getStatic(): UserContext {

// throw RuntimeException("啊偶 出错了")

return RequestContext.getUserContext()

}

测试方法1

2

3

4

5

6

7@Test

fun testGetStatic() {

val json = "{\"username\":\"测试1\", \"userId\":\"测试1\"}"

mockMvc.perform(MockMvcRequestBuilders.get("/getStatic")

.header("user-test", json))

.andExpect(status().isOk).andDo { log.info("返回结果 ${it.response.contentAsString}") }.andReturn()

}

测试结果

由于提供的都是静态方法,使用此方式我们就可以在任何地方使用用户上下文对象(注意避免空指针),例如,我们就可以使用mybatis拦截器替我们完成userId等属性的注入。

bean获取

由于静态类不由spring管理,业务类使用时不免使代码的耦合性变强,当我们需要变更方案时将会比较麻烦,因此我们希望将类委托spring进行管理

方案1(单例bean)我们提供一个接口,向外暴露一个getter方法,在getter方法中调用静态方法获取1

2

3

4

5interface UserContextManage{

fun getUserContext(): UserContext {

return RequestContext.getUserContext()

}

}

配置bean1

2

3

4@Bean

fun userContextManage(): UserContextManage {

return object : UserContextManage {}

}

控制器1

2

3

4

5

6

7@Autowired

lateinit var userContextManage: UserContextManage

@GetMapping("/getSingletonBean")

fun getSingletonBean(): UserContext {

return userContextManage.getUserContext()

}

测试方法1

2

3

4

5

6fun testGetSingletonBean() {

val json = "{\"username\":\"测试3\", \"userId\":\"测试3\"}"

mockMvc.perform(MockMvcRequestBuilders.get("/getSingletonBean")

.header("user-test", json))

.andExpect(status().isOk).andDo { log.info("返回结果 ${it.response.contentAsString}") }.andReturn()

}

测试结果

需要额外提及的是,当我们在业务层依赖此对象时,单元测试由于不涉及请求导致用户上下文为空,这里推荐使用mockBean进行测试1

2

3

4

5

6

7

8

9

10@MockBean

lateinit var userContextManage: UserContextManage

@Before

fun before() {

Mockito.`when`(userContextManage.getUserContext()).thenReturn(UserContext("mock测试","mock测试"))

}

@Test

fun testMockSingletonBean() {

log.info(userContextManage.getUserContext().toString())

}

mock结果

方案2(请求bean)

spring为我们提供了scope为request的bean,例如httpServletRequest就是一个这种bean,这种类型的bean的生命周期和请求是息息相关的,伴随的请求开始和结束进行创建和销毁

声明bean

1

2

3

4

5@Bean

@Scope(WebApplicationContext.SCOPE_REQUEST)

fun userContext(): UserContext {

return RequestContext.getUserContext()

}

控制器

1

2

3

4

5

6@Autowired(required = false)

var userContext: UserContext? = null

@GetMapping("/getRequestBean")

fun getRequestBean(): UserContext {

return userContext!!

}

测试方法

1

2

3

4

5

6

7@Test

fun testGetRequestBean() {

val json = "{\"username\":\"测试4\", \"userId\":\"测试4\"}"

mockMvc.perform(MockMvcRequestBuilders.get("/getRequestBean")

.header("user-test", json))

.andExpect(status().isOk).andDo { log.info("返回结果 ${it.response.contentAsString}") }.andReturn()

}

在测试时我们会发现,哪怕我们不要求spring为我们一定要注入这个bean,spring还是会尝试注入并报错

这里有几种方案处理这种异常

方式1 引入javax inject依赖1implementation("javax.inject:javax.inject:1")

控制器中注入对象使用Provider包装1

2

3

4

5

6@Autowired

lateinit var userContext: Provider

@GetMapping("/getRequestBean")

fun getRequestBean(): UserContext {

return userContext.get()

}

方式2 为bean使用代理 注意如果使用kotlin不要使用类代理,否则会丢失字段值1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46// 使用kotlin需要配置消息转换器(java是否需要还未测试)

@Bean

fun httpMessageConverter(): HttpMessageConverter {

//创建fastJson消息转换器

val fastConverter = FastJsonHttpMessageConverter()

//升级最新版本需加=============================================================

val supportedMediaTypes = ArrayList()

supportedMediaTypes.add(MediaType.APPLICATION_JSON)

supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8)

supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML)

supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED)

supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM)

supportedMediaTypes.add(MediaType.APPLICATION_PDF)

supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML)

supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML)

supportedMediaTypes.add(MediaType.APPLICATION_XML)

supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA)

supportedMediaTypes.add(MediaType.IMAGE_GIF)

supportedMediaTypes.add(MediaType.IMAGE_JPEG)

supportedMediaTypes.add(MediaType.IMAGE_PNG)

supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM)

supportedMediaTypes.add(MediaType.TEXT_HTML)

supportedMediaTypes.add(MediaType.TEXT_MARKDOWN)

supportedMediaTypes.add(MediaType.TEXT_PLAIN)

supportedMediaTypes.add(MediaType.TEXT_XML)

fastConverter.supportedMediaTypes = supportedMediaTypes

//创建配置类

val fastJsonConfig = FastJsonConfig()

//修改配置返回内容的过滤

//WriteNullListAsEmpty :List字段如果为null,输出为[],而非null

//WriteNullStringAsEmpty : 字符类型字段如果为null,输出为"",而非null

//DisableCircularReferenceDetect :消除对同一对象循环引用的问题,默认为false(如果不配置有可能会进入死循环)

//WriteNullBooleanAsFalse:Boolean字段如果为null,输出为false,而非null

//WriteMapNullValue:是否输出值为null的字段,默认为false

fastJsonConfig.setSerializerFeatures(

SerializerFeature.DisableCircularReferenceDetect,

SerializerFeature.WriteMapNullValue,

SerializerFeature.WriteNullStringAsEmpty,

SerializerFeature.WriteMapNullValue

)

fastConverter.fastJsonConfig = fastJsonConfig

return fastConverter

}

修改用户上下文对象1

2

3

4

5

6open class UserContext(override val userId: String, override val username: String) : IUserContext

interface IUserContext{

val userId: String

val username: String

}

接口代理1

2

3

4

5@Bean

@Scope(WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.INTERFACES)

fun iUserContext(): IUserContext {

return RequestContext.getUserContext()

}

控制器注入1

2

3

4

5

6@Autowired

lateinit var userContext: IUserContext

@GetMapping("/getRequestBean")

fun getRequestBean(): IUserContext {

return userContext

}

处理之后测试运行结果

此方式使用将bean委托spring管理,耦合性较低,并且如果使用代理的方式用起来会更加方便,但需要注意的是,由于使用了代理,在请求不涉及用户上下文(即获取用户上下文为空)的情况下调用代理对象将直接抛异常(无法使用==null做空判断)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值