Spring基本应用示例

消息转换器

在Spring中org.springframework.http.converter.HttpMessageConverter规范中定义了Http请求和响应的消息转换规范, 我们知道SpringMvc可以接收不同的消息形式,也可以将不同的消息形式响应回去(最常见的是json);这些消息所蕴含的”有效信息”是一致的,那么各种不同的消息转换器,都会生成同样的转换结果. 至于各种消息间解析细节的不同,就被屏蔽在不同的HttpMessageConverter实现类中.

SpringMVC中使用FastJson作为转换器

通过SpringMvc中message-converts配置FastJson作为转换器

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
47
48
49
50
<!-- 默认的注解映射的支持,org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping -->
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager">
<mvc:message-converters register-defaults="true">
<!-- 将Jackson2HttpMessageConverter的默认格式化输出为true -->
<!-- 配置Fastjson支持 -->
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json</value>
</list>
</property>
<property name="features">
<list>
<!-- 输出key时是否使用双引号 -->
<value>QuoteFieldNames</value>
<!-- 是否输出值为null的字段 -->
<!-- <value>WriteMapNullValue</value> -->
<!-- 数值字段如果为null,输出为0,而非null -->
<value>WriteNullNumberAsZero</value>
<!-- List字段如果为null,输出为[],而非null -->
<value>WriteNullListAsEmpty</value>
<!-- 字符类型字段如果为null,输出为"",而非null -->
<value>WriteNullStringAsEmpty</value>
<!-- Boolean字段如果为null,输出为false,而非null -->
<value>WriteNullBooleanAsFalse</value>
<!-- null String不输出 -->
<value>WriteNullStringAsEmpty</value>
<!-- null String也要输出 -->
<!-- <value>WriteMapNullValue</value> -->

<!-- Date的日期转换器 -->
<value>WriteDateUseDateFormat</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>

<!-- REST中根据URL后缀自动判定Content-Type及相应的View -->
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="mediaTypes" >
<map>
<entry key="json" value="application/json"/>
</map>
</property>
<!-- 这里是否忽略掉accept header,默认就是false -->
<property name="ignoreAcceptHeader" value="true"/>
<property name="favorPathExtension" value="true"/>
</bean>

Fastjson轻量级属性转换

定义示例对象模型

做fastjson轻量注解配置, 更多配置参考https://github.com/alibaba/fastjson/wiki/JSONField

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
47
48
49
50
51
52
53
54
public class ModelTest implements Serializable{

// 使用ordinal指定字段
@JSONField(ordinal = 1)
private Long id;

@JSONField(ordinal = 2)
private String name;

@JSONField(ordinal = 3)
private Integer age;

// 使用serialize/deserialize指定字段不序列化
@JSONField(deserialize = false, serialize = false)
private String remark;

// 配置date序列化和反序列使用yyyyMMdd日期格式
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date crateTime;

// 配置属性序列化使用的名称
@JSONField(name = "updateTime", format="yyyy-MM-dd HH:mm:ss")
private Date modifyTime;

@JSONField(ordinal = 0)
private DeleteEnum deleteEnum;

public enum DeleteEnum {

DISABLE(1, "禁用"),
ENABLE(2, "启用");

private int value;
private String depict;

DeleteEnum(int value, String depict) {
this.value = value;
this.depict = depict;
}

public static DeleteEnum findByValue(int value) {
switch (value) {
case 1:
return DISABLE;
case 2:
return ENABLE;
default:
return null;
}
}
}

// getter and setter
}

定义示例Controller

示例完成序列化数据到前端和提交JSON数据转换成对象模型

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
@Controller
@RequestMapping(value = "/test")
public class TestWebController {

private static final Logger LOG = LoggerFactory.getLogger(TestWebController.class);

// 序列化对象到视图
@ResponseBody
@RequestMapping(value = {"/bean/data"}, method = {RequestMethod.GET, RequestMethod.POST})
public Object toBody(){

ModelTest modelTest = new ModelTest();
modelTest.setAge(11);
modelTest.setCrateTime(new Date());
modelTest.setModifyTime(new Date());
modelTest.setId(1L);
modelTest.setName("测试Fastjson");
modelTest.setRemark("备注");
modelTest.setDeleteEnum(ModelTest.DeleteEnum.ENABLE);

return ImmutableMap.<String, String>builder()
.put("code", "0")
.put("data", JSON.toJSONString(modelTest)).build();
}

// 解析JSON对象到实体模型
@ResponseBody
@RequestMapping(value = {"/bean/put"}, method = {RequestMethod.GET, RequestMethod.POST})
public Object getBody(@RequestBody ModelTest modelTest){
System.out.println(JSON.toJSONString(modelTest));
return ImmutableMap.<String, String>builder()
.put("code", "0")
.put("data", "ok").build();
}
}

测试对象序列化到前端展示

1
2
3
4
5
6
7
8
9
10
11
@Test
public void fastJSONTest01() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.post("/test/bean/data")
.cookie(new Cookie("token", "F3AF5F1D14F534F677XF3A00E352C")) // 登录授权
.accept(MediaType.parseMediaType("application/json;charset=UTF-8")))
.andExpect(handler().handlerType(TestWebController.class)) // 验证执行的控制器类型
.andExpect(status().isOk()) // 验证执行状态
.andDo(print()) // 打印交互信息
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}

执行结果:

1
2
3
4
{
"code": "0",
"data": "{"crateTime":"2016-12-2808: 02: 50","deleteEnum":"ENABLE","updateTime":"2016-12-2808: 02: 50","id":1,"name":"测试Fastjson","age":11}"
}

测试提交数据转换成模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void fastJSONTest02() throws Exception {

ModelTest modelTest = new ModelTest();
modelTest.setAge(11);
modelTest.setCrateTime(new Date());
modelTest.setModifyTime(new Date());
modelTest.setId(1L);
modelTest.setName("测试Fastjson");
modelTest.setRemark("备注");
modelTest.setDeleteEnum(ModelTest.DeleteEnum.ENABLE);

MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.post("/test/bean/put")
.content(JSON.toJSONString(modelTest))
.contentType(MediaType.APPLICATION_JSON)
.cookie(new Cookie("token", "F3AF5F1D14F534F677XF3A00E352C")) // 登录授权
.accept(MediaType.parseMediaType("application/json;charset=UTF-8")))
.andExpect(handler().handlerType(TestWebController.class)) // 验证执行的控制器类型
.andExpect(status().isOk()) // 验证执行状态
.andDo(print()) // 打印交互信息
.andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}

执行后台打印:

1
{"crateTime":"2016-12-28 08:04:33","deleteEnum":"ENABLE","updateTime":"2016-12-28 08:04:33","id":1,"name":"测试Fastjson","age":11}

枚举绑定处理

示例中我们看到序列化的枚举为枚举的name(). 如果我们想用枚举的寓意值进行传输过程中的映射时,可以这样做, 透传一个数值, 该数值和枚举在getter()和setter()方法上绑定即可.

修改示例的对象模型

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class ModelTest implements Serializable{

// 使用ordinal指定字段
@JSONField(ordinal = 1)
private Long id;

@JSONField(ordinal = 2)
private String name;

@JSONField(ordinal = 3)
private Integer age;

// 使用serialize/deserialize指定字段不序列化
@JSONField(deserialize = false, serialize = false)
private String remark;

// 配置date序列化和反序列使用yyyyMMdd日期格式
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date crateTime;

// 配置属性序列化使用的名称
@JSONField(name = "updateTime", format="yyyy-MM-dd HH:mm:ss")
private Date modifyTime;

@JSONField(ordinal = 0)
private DeleteEnum deleteEnum;

private int enable;

public enum DeleteEnum {

DISABLE(1, "禁用"),
ENABLE(2, "启用");

private int value;
private String depict;

DeleteEnum(int value, String depict) {
this.value = value;
this.depict = depict;
}

public static DeleteEnum findByValue(int value) {
switch (value) {
case 1:
return DISABLE;
case 2:
return ENABLE;
default:
return null;
}
}
}

// 数值绑定枚举
public int getEnable() {
this.deleteEnum = DeleteEnum.findByValue(enable);
return enable;
}

// 数值绑定枚举
public void setEnable(int enable) {
this.enable = enable;
this.deleteEnum = DeleteEnum.findByValue(enable);
}

// 枚举映射数值
public DeleteEnum getDeleteEnum() {
this.enable = this.deleteEnum.value;
return deleteEnum;
}

// 枚举映射数值
public void setDeleteEnum(DeleteEnum deleteEnum) {
this.deleteEnum = deleteEnum;
this.enable = this.deleteEnum.value;
}
}

我们保持示例中Controller类和测试用例的代码不变,执行用例.

  • 测试对象序列化到前端展示执行结果
1
2
3
4
{
"code": "0",
"data": "{"crateTime":"2016-12-2808: 12: 52","deleteEnum":"ENABLE","enable":2,"updateTime":"2016-12-2808: 12: 52","id":1,"name":"测试Fastjson","age":11}"
}
  • 测试提交数据转换成模型 执行后台打印
1
{"crateTime":"2016-12-28 08:20:17","deleteEnum":"ENABLE","enable":2,"updateTime":"2016-12-28 08:20:17","id":1,"name":"测试Fastjson","age":11}

修改用例模拟的实体对象, 测试数值到枚举的映射.

1
2
3
4
5
6
7
8
ModelTest modelTest = new ModelTest();
modelTest.setAge(11);
modelTest.setCrateTime(new Date());
modelTest.setModifyTime(new Date());
modelTest.setId(1L);
modelTest.setName("测试Fastjson");
modelTest.setRemark("备注");
modelTest.setEnable(1);

后台打印输出:

1
{"crateTime":"2016-12-28 08:21:48","deleteEnum":"DISABLE","enable":1,"updateTime":"2016-12-28 08:21:48","id":1,"name":"测试Fastjson","age":11}

可以看到enable的值自动和枚举类型映射上.

使用FastJson做对象和JSON之间的转换

对象转换成JSON

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void fastJSONTest03() {
ModelTest modelTest = new ModelTest();
modelTest.setAge(11);
modelTest.setCrateTime(new Date());
modelTest.setModifyTime(new Date());
modelTest.setId(1L);
modelTest.setName("测试Fastjson");
modelTest.setRemark("备注");
modelTest.setEnable(1);
System.out.println(JSON.toJSONString(modelTest));
}

执行结果:

1
{"crateTime":"2016-12-28 08:30:42","deleteEnum":"DISABLE","enable":1,"updateTime":"2016-12-28 08:30:42","id":1,"name":"测试Fastjson","age":11}

JSON转换成对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void fastJSONTest04() {
String json = "{\n" +
" \"id\": 1,\n" +
" \"deleteEnum\": \"DISABLE\",\n" +
" \"updateTime\": 1482884802549,\n" +
" \"crateTime\": 1482884802549,\n" +
" \"age\": 11,\n" +
" \"name\": \"测试Fastjson\",\n" +
" \"enable\": 1\n" +
"}";
ModelTest modelTest = JSONObject.parseObject(json, ModelTest.class);
System.out.println(modelTest);
System.out.println(modelTest.getName());
}

执行结果:

1
2
ModelTest{id=1, name='测试Fastjson', age=11, remark='null', crateTime=Wed Dec 28 08:26:42 CST 2016, modifyTime=Wed Dec 28 08:26:42 CST 2016, deleteEnum=DISABLE, enable=1}
测试Fastjson

示例中直接打印输出对象,是因为复写了toString()方法.

SpringBean注解扫描组件

Spring中bean注解扫描类ClassPathScanningCandidateComponentProvider, 该类构造参数如下:

1
2
3
4
5
6
7
8
9
public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) {
this(useDefaultFilters, new StandardEnvironment());
}
public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) {
if (useDefaultFilters) {
registerDefaultFilters();
}
this.environment = environment;
}

类实例化方式构造依赖, 参数意义:
useDefaultFilters: 是否走默认的springBean扫描策略, spring启动时该值默认是为true, 扫描的组件是@Component
environment: 环境变量相关,基于spring.profiles相关配置

扩展自定义类

自己扩展的注解需要被SpringBean注解扫描器扫到的话需要注解上增加@Component,

自定义注解示例

自定义注解,绑定Spring注解@Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Component
public @interface VClass {

// 类名称
public String name();

// 表名称
public String tableName();

// 表注释
public String comment() default "";
}

应用自定义注解

在自定义实体类上增加自定义注解

1
2
3
4
5
6
7
import com.wplus.plugin.htmlplus.anno.bean.VClass;

@VClass(name = "mock", tableName = "tb_mock", comment = "测试实体")
public class Mock {

// this class used for test
}

使用Spring注解扫描器扫描自定义类

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
/**
* 根据路径扫描带有VClass注解的类
* @param scanPath 要扫描的包路径
* @return
*/
public List<String> scanAnnoBeanName(String scanPath){
if(StringUtils.isBlank(scanPath)){
scanPath = DEFAULT_BEAN_PATH;
}
return scanAnnoBeanName(scanPath, VClass.class);
}


/**
* 根据路径扫描要用来映射的类
* @param scanPath 要扫描的包
* @return
*/
public List<String> scanAnnoBeanName(String scanPath, Class<? extends Annotation> annotationType){
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(annotationType));
Set<BeanDefinition> sets = provider.findCandidateComponents(scanPath);
if(null !=sets && sets.size() > 0){
List<String> classNames = new ArrayList<String>();
for(BeanDefinition bean: sets){
classNames.add(bean.getBeanClassName());
}
return classNames;
}
return null;
}

public static void main(String[] args) {
List<String> beans = new VanoBeanLoadApiImpl().scanAnnoBeanName("com.wplus.plugin");
System.out.println(beans);
}

程序输出

1
[com.wplus.plugin.htmlplus.demo.Mock]

SpringResources资源扫描组件

Spring中Resources扫描类GenericApplicationContext, 对应的资源扫描方法如下:

1
2
3
public Resource getResource(String location) {
return this.resourceLoader != null?this.resourceLoader.getResource(location):super.getResource(location);
}

Resources默认资源加载路径String CLASSPATH_URL_PREFIX = "classpath:";

SpringResources资源组件扩展

定义资源加载方法,增加资源后缀匹配过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public List<Resource> scanResources(String locationPattern, final List<String> accepts) {
if(StringUtils.isBlank(locationPattern)){
locationPattern = DEFAULT_TEMPLATE_PATH;
}
try {
GenericApplicationContext context = new GenericApplicationContext();
Resource rs[] = context.getResources(locationPattern);
List<Resource> list = (null != rs && rs.length > 0)? Arrays.asList(rs): new ArrayList<Resource>();
// do return resource list if accepts list is empty.
if(CollectionUtils.isEmpty(accepts)){
return list;
}
// filter resource which file extension in accepts
List<Resource> result = list.stream() // convert list to stream
.filter(line -> accepts.contains(".".concat(FilenameUtils.getExtension(line.getFilename()))))
.collect(Collectors.<Resource>toList());
return result;
}catch (Exception e){
LOGGER.error(e.getMessage(), e);
}
return new ArrayList<Resource>();
}

示例使用Spring资源加载组件,扫描指定路径下的资源模板, 第二个参数用来标识扫描资源结果匹配的过滤,
后缀在接收列表中的资源,会加载到结果集中.

调用示例模拟

1
2
3
4
public static void main(String[] args) {
List<Resource> list = new TemplateLoadApiImpl().scanResources("classpath:/HTemplate/**", Arrays.asList(".html"));
list.forEach(li -> System.out.println(li.toString()));
}

方法通过扫描当前工程下资源模块HTemplate中的内容,递归遍历,收集后缀为.html结尾的资源文件.
如果要扫描jar包中的资源的话,classpath统配符应为"classpath*:/HTemplate/**"

@Aspect说明

Spring除了支持Schema方式配置AOP,还支持使用@Aspect注解方式切面声明。Spring默认注解支持关闭,开启配置:

1
<aop:aspectj-autoproxy/>

这样Spring就能发现@AspectJ风格的切面并且将切面应用到目标对象。

@Aspect依赖包

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>

@Aspect示例

定义切面,切入方法调用耗时

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Arrays;
@Component
@Aspect
@Order(3)
public class TraceInvokeTimelineAroundAspect {

private static final Logger LOG = LoggerFactory.getLogger(TraceInvokeTimelineAroundAspect.class);

public final static long MONITOR_MAX_TIME = 5000;

@Around(
"pointRestful() || pointService() || pointMapper()"
)
public Object invoke(ProceedingJoinPoint point) throws Throwable {
long startTime = System.currentTimeMillis();

String classMethodName = point.getSignature().getDeclaringType().getSimpleName() + "." + point.getSignature().getName();
Object result = point.proceed();

long usedTime = System.currentTimeMillis() - startTime;
StringBuffer buffer = new StringBuffer(600);
buffer.append("耗时:").append(usedTime + "ms").append(" ");
buffer.append("优化(>" + MONITOR_MAX_TIME + "ms):").append(usedTime >= MONITOR_MAX_TIME).append(" ");
buffer.append("接口:").append(classMethodName).append(" ");
buffer.append("入参:").append(Arrays.toString(point.getArgs())).append(" ");
buffer.append("返回:").append(JSON.toJSONString(result));
LOG.info(buffer.toString());
return result;
}

/**
* 声明切入点 - 控制层切入
*/
@Pointcut("execution(* com.tutorial.aspectj.web.controller..*.*(..))")
public void pointRestful() {

}

/**
* 声明切入点 - 业务层切入
*/
@Pointcut("execution(* com.tutorial.aspectj.facade.service..*.*(..))")
public void pointService(){

}

/**
* 声明切入点 - 数据层切入
*/
@Pointcut("execution(* com.tutorial.aspectj.facade.mapper..*.*(..))")
public void pointMapper() {

}

开启@Aspect支持

示例中我们用注解@Component对自定的Aspect进行了实例化,需要保证注解所在包能被Spring实例注解扫描到。
要使切面在两个容器(spring&springmvc)中都生效,必须两个都必须配置<aop:aspectj-autoproxy/>开启注解支持.

  • Spring容器
1
2
3
4
5
<context:annotation-config/>
<context:component-scan base-package="com.tutorial.aspectj..**" use-default-filters="true">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<aop:aspectj-autoproxy/>
  • SpringMvc容器
1
2
3
4
5
6
<context:annotation-config/>
<context:component-scan base-package="com.tutorial.aspectj.web.controller..**" use-default-filters="true">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>
<aop:aspectj-autoproxy/>