一、前言
在 Spring 框架和 Spring Boot 中,最常使用的 Web 技术就是 Spring MVC。
二、工程层次结构
在 cmd 中使用 tree /f 命令查看项目结构关系,如下:
├─spring-boot-hello │ │ │ ├─src │ │ ├─main │ │ │ ├─java │ │ │ │ └─com │ │ │ │ └─light │ │ │ │ └─springboot │ │ │ │ │ SpringbootApplication.java │ │ │ │ ├─conf │ │ │ │ ├─controller │ │ │ │ ├─dao │ │ │ │ │ └─impl │ │ │ │ ├─entity │ │ │ │ ├─service │ │ │ │ │ └─impl │ │ │ │ └─util │ │ │ │ SpringUtil.java │ │ │ │ │ │ │ └─resources │ │ │ │ application.properties │ │ │ │ │ │ │ ├─static │ │ │ │ ├─css │ │ │ │ ├─image │ │ │ │ └─js │ │ │ └─templates │ │ │ │ │ └─test │ │ ├─java │ │ └─resources
Java 包名结构一般为:
controller:通常包含项目中 MVC 的控制层,通常使用到 @Controller 注解;
service:通常包含业务层,impl 为其实现类,通常使用到 @Service 注解;
dao:通常包含持久层,impl 为其实现类,通常使用到 @Repository 注解;
entity:通常包含业务实体模型;
conf:通常包含一些配置类,通常使用到 @Configuration 和 @Bean 注解;
util:通常包含项目中的一些工具类。
Web 层结构一般为:
resource/templates:一般包含模板文件;
resource/static:一般包含模板文件使用到的静态资源文件,如 js、css、图片等,粉笔额存放到对应的目录中。
三、模板引擎
Spring Boot支持的模板类型有:Thymeleaf、FreeMarker、Velocity、Groovy 和 Mustache 等,Spring Boot 建议使用这些模板引擎,避免使用 JSP,若一定要使用 JSP 将无法实现 Spring Boot 的多种特性。此处分别介绍 Thymeleaf 和 freemarker 的整合,Spring Boot 支持同时使用多种模板引擎,通常情况下,Spring Boot 会依次检查每种模板引擎是否能处理指定的试图名。
3.1 整合 Thymeleaf
在 pom 文件中添加对 Thymeleaf 模板的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
在 application.properties 中添加如下内容:
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
其中 spring.thymeleaf.cache 在开发中设置为 false,表示关闭缓存,当开发人员修改模板时,可以及时生效,看到效果,在正式环境中将其值设置为 true。可以只设置第一项,其他均为默认值。
编写 ThymeleafController.java,如下:
package com.light.springboot.controller; import java.util.Map; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller @RequestMapping(value="/thymeleafController") public class ThymeleafController { @RequestMapping(value="/hello1/{id}") public String order(@PathVariable(value = "id",required = false) String id, @RequestParam(value = "action",required = false) String action, Map<String,Object> map) { System.out.println("id:" + id); System.out.println("action:" + action); map.put("msg", "Thymeleaf"); return "hello1"; } }
在 resources/templates 目录下创建名为 hello1.html 的文件,内容如下:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Spring Boot Thymeleaf</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <h2 th:text="${msg}"></h2> </body> </html>
页面展示的效果如下:
3.2 整合 freemarker
在 pom 文件中添加对 freemarker 模板的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
在 application.properties 中添加如下内容:
spring.freemarker.cache=false
spring.freemarker.allow-request-override=false spring.freemarker.check-template-location=true spring.freemarker.charset=UTF-8 spring.freemarker.content-type=text/html spring.freemarker.expose-request-attributes=false spring.freemarker.expose-session-attributes=false spring.freemarker.expose-spring-macro-helpers=false spring.freemarker.prefix=classpath:/templates/ spring.freemarker.suffix=.ftl
同理,其中 spring.freemarker.cache 在开发中设置为 false,表示关闭缓存,当开发人员修改模板时,可以及时生效,看到效果,在正式环境中将其值设置为 true。可以只设置第一项,其他均为默认值。
编写 FreemarkerController.java 如下:
package com.light.springboot.controller; import java.util.Map; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller @RequestMapping(value="/freemarkerController") public class FreemarkerController { @RequestMapping(value="/hello2/{id}") public String order1(@PathVariable(value = "id",required = false) String id, @RequestParam(value = "action",required = false) String action, Map<String,Object> map) { map.put("msg", "Freemarker"); return "hello2"; } }
在 resources/templates 目录下创建名为 hello2.ftl 的文件,ftl 是 freemarker 默认的模板文件后缀,内容如下:
<!DOCTYPE html> <html> <head> <title>Spring Boot Freemarker</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <h2>${msg}</h2> </body> </html>
页面展示的效果如下:
四、常用注解
以前使用 Spring 的时候,都是在 xml 中配置一个个的 Bean,自从出现了注解之后,注解取代传统的 xml 配置,成为主流的配置方式:
4.1 常用声明 Bean 的注解
Spring 提供了多个注解声明 Bean 为 Spring 管理的 Bean,注解不同代表的含义不一样,但是对于 Spring IOC 容器来说,都是 Sping 管理的 Bean。
@Controller:声明此类是一个 MVC 类,通常与 @RequestMapping 一起使用;
@Service:声明此类是一个业务处理类,通常与 @Transactional 一起配合使用;
@Repository:声明此类是一个数据库或者其他 NoSQL 访问类;
@RestController:用于 REST 服务,相当于 @Controller 和 @ResponseBody 同时使用;
@Component:声明此类是一个 Spring 管理的类,由于不属于具体的某一个层面,所有通常用于无法用上述注解描述的 Spring 管理类;
@Configuration:声明此类是一个配置类,也被称为 Java Config,以前通常是在 XML 中配置,现在使用 Java 编写配置,通常与注解 @Bean 配合使用;
@Bean:作用在方法上,声明该方法执行的返回结果是一个 Spring 容器管理的 Bean,包括 @PostConstruct 注解和 @preDestroy 注解;
@Value:为了简化读取 properties 文件中的配置值,spring支持 @Value("${key}") 注解的方式来获取,这种方式大大简化了项目配置,提高业务中的灵活性@Value("${key}")。
Spring 有两种方式来引用容器管理的 Bean,一种是根据名字 byName,另一种是根据类型 byType:
@Resource:该注解由 JavaEE 提供,默认按照名称进行匹配,其中可以指定引用的 Bean 名称。
@Autowired:该注解由 spring 提供,默认按照类型进行匹配,也可以配合 @Qualifier 属性来按名称匹配。
4.2 常用方法注解及参数
@RequestMapping:既可以作用在方法上,默认返回视图的名称,也可以作用在类上,按照 HTTP method 的不同,简化后又可分为:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping 和 @PatchMapping;
@ResponseBody:直接将返回的对象输出到客户端,如果是字符串,则直接返回;如果不是,Spring Boot 默认使用 Jackson 序列化成 JSON 字符串后输出;
@PathVariable:可以将 URL 中的值映射到方法参数中;
@RequestParam:对应于 HTTP 请求的参数,自动转化为参数对应的类型;
@RequestBody:Controller 方法带有@RequestBody 注解的参数,意味着请求的 HTTP 消息体的内容是一个 JSON,需要转化为注解指定的参数类型。Spring Boot 默认使用 Jackson 来处理反序列化工作;
@InitBinder:用在方法上,说明这个方法会注册多个转化器,用来个性化地将 HTTP 请求参数转化成对应的 Java 对象,如转化为日期类型、浮点类型和 JavaBean 等;
@ModelAttribute:通常作用在 Controller 的某个方法上,此方法会首先被调用,并将方法结果作为 Model 的属性,然后再调用对应的 Controller 处理方法;
Model:Spring 中通用的 MVC 模型,也可以使用 Map 和 ModelMap 作为渲染视图的模型;
ModelAndView:包含了模型和视图路径的对象,既可以通过方法声明,也可以在方法中构造。
五、JavaBean 接收 HTTP 参数
Spring 支持按照前缀自动映射到不同的对象上。比如用户提交了订单信息,订单信息包含了多个订单明细。
package com.light.springboot.entity; import java.util.List; public class OrderPostForm { private String name; private Order order; private List<OrderDetail> details; // 省略 getter 和 setter 方法 }
同理创建 Order 模型类和 OrderDetail 模型类。
以 name 为属性值的 HTTP 参数将直接映射到 OrderPostForm 类的 name 属性上;
以 order 为前缀的 HTTP 参数将映射到 OrderPostForm 类的 order 属性上;
以 details 属性为前缀的 HTTP 参数将映射到 OrderPostForm 的 details 属性上。
表单页面设计如下:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Insert title here</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <form action="/thymeleafController/saveorder" method="post"> 名称:<input name="name" /> <p></p> 订单名称:<input name="order.name" /> <p></p> 订单明细1:<input name="details[0].name" /> <br /> 订单明细2:<input name="details[1].name" /> <p></p> <input type="submit" value="Submit" /> </form> </body> </html>
则 Controller 的方法可以正常处理这个请求中的所有参数:
@RequestMapping(value="/saveorder") @ResponseBody public String saveOrder(OrderPostForm form) { System.out.println("form.name:" + form.getName()); System.out.println("form.order.name:" + form.getOrder().getName()); System.out.println("form.getDetails().size():" + form.getDetails().size()); System.out.println("form.details.name:" + form.getDetails().get(0).getName()); System.out.println("form.details.name:" + form.getDetails().get(1).getName()); return "success"; }
简单描述上面的 Spring 将 HTTP 参数映射到 JavaBean 的规则:
name:OrderPostForm 类对象 form 的 name 属性;
order.name:Order 类对象 order 的 name 属性,由于 order 对象也是 OrderPostForm 的一个属性,所以也可以说是 OrderPostForm 类对象 form 的 order 属性的 name 属性;
details[0].name/details[1].name:List<OrderDetail> 类对象 details 的 name 属性,由于 details 对象也是 OrderPostForm 的一个属性,所以也可以说是 OrderPostForm 类对象 form 的 details 属性的 name 属性,要求 details 是个数组或者 List(不能是 Set,因为 Set 为无序集合,不能用索引取值),details[0] 表示 details 属性的第一个元素。
六、文件上传和下载
通过 MultipartFile 来处理文件上传,编写用户上传文件的模型类 UserFile,用于封装数据:
package com.light.springboot.entity; public class UserFile { private String name; private String path; public UserFile(String name, String path) { this.name = name; this.path = path; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } }
编写文件上传和下载的 Controller:
package com.light.springboot.controller; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import com.light.springboot.entity.UserFile; /** * 文件上传和下载 * @author Administrator * */ @Controller @RequestMapping("/fileController") public class FileController { private String path = "G:\"; /** * 进入到文件上传页面 * @return */ @GetMapping public String file() {//不配置路径映射的话,表示此 fileController下的默认页面 return "upload_file"; } /** * 单文件上传 * @param file * @return * @throws Exception */ @PostMapping("/uploadFile") @ResponseBody public UserFile upload(MultipartFile file) throws Exception { if(!file.isEmpty()) {//判断上传内容为空,或者没有上传文件 System.out.println(file.getName());//表单参数name System.out.println(file.getOriginalFilename());//获取上传的文件名称 System.out.println(file.getSize());//文件上传的大小 byte[] fileByte = file.getBytes();//获取上传文件的内容,转换为字节数组 System.out.println(fileByte.length);//文件上传的字节数 File localFile = new File(path, file.getOriginalFilename()); // 保存上传文件到目标文件系统 file.transferTo(localFile); return new UserFile(file.getName(), localFile.getAbsolutePath()); } return null; } /** * 文件下载 * @param name * @param request * @param response */ @GetMapping("/{name}") @ResponseBody public void download(@PathVariable(value = "name",required = false) String name, HttpServletRequest request, HttpServletResponse response) { InputStream inputStream = null; OutputStream outputStream = null; File file = null; try { file = new File(path, name + ".txt"); inputStream = new FileInputStream(file); outputStream = response.getOutputStream(); // 设置response的Header response.addHeader("Content-Disposition", "attachment;filename=" + name + ".txt"); response.addHeader("Content-Length", "" + file.length()); response.setContentType("application/x-download"); // 通过IOUtil对接输入输出流,实现文件下载 IOUtils.copy(inputStream, outputStream); } catch (Exception e) { e.printStackTrace(); } } }
设计的页面上传模板如下:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Insert title here</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <h1>文件上传</h1> <form method="post" action="/fileController/uploadFile" enctype="multipart/form-data"> 选择一个文件: <input type="file" name="file" /> <br/><br/> <input type="submit" value="上传" /> </form> </body> </html>
注意:页面 input 中的 name 属性 file 对应于 upload(MultipartFile file)
进入上传页面:
上传结果页面:
下载结果页面:
如果同时上传多个文件,则使用 MultipartFile 数组类来接收多个文件上传:
/** * 多文件上传 * @param files * @return * @throws Exception */ @PostMapping("/uploadFiles") @ResponseBody public UserFile uploadFiles(MultipartFile[] files) throws Exception { // 处理多文件上传 return null; }
同理注意:页面 input 中的 name 属性 files 应该对应于 upload(MultipartFile[] files)
可以通过配置 application.properties 对 Spring Boot 上传的文件进行限定,默认如下配置:
# MULTIPART (MultipartProperties) spring.http.multipart.enabled=false spring.http.multipart.file-size-threshold=0 spring.http.multipart.location= spring.http.multipart.max-file-size=1MB spring.http.multipart.max-request-size=10MB spring.http.multipart.resolve-lazily=false
参数 enable 默认为 true,即允许上传附件,file-size-threshould 限定了当上传的文件超过一定长度时,就先写到临时文件里。这有助于上传文件不占用过多的内存,单位是 MB 或者 KB,默认是0,即不限定阈值。location 指的是临时文件的存放目录,如果不设定,则是 Web 服务器提供的一个临时目录。max-file-size 属性指定了单个文件的最大长度,默认是 1MB,max-request-size 属性说明单次 HTTP 请求上传的最大长度,默认是 10MB。resolve-lazily 表示当文件和参数被访问的时候再解析成文件。
七、验证框架
Spring Boot 支持 JSR-303、Bean 验证框架,默认实现用的是 Hibernate validator。JSR-303 是 Java 标准的验证框架,已有的实现有 Hibernate validator。JSR-303 定义了一系列注解用来验证 Bean 的属性,常用的有如下几种:
空检查:
@Null:验证对象是否为空;
@NotNull:验证对象不为空;
@NotBlank:验证字符串不为空或者不是空字符串,比如""和“ ”都会验证失败;
@NotEmpty:验证对象不为 null,或者集合不为空。
长度检查:
@Size(min=,max=):验证对象长度,可支持字符串,集合;
@Length:字符串长度。
数值检测:
@Min:验证数字是否大于等于指定的值;
@Max:验证数字是否大于等于指定的值;
@Digits:验证数字是否符合指定格式,如@Digits(integer=9,fraction=2);
@Range:验证数字是否在指定的范围内,如@Range(min=1,max=1000);
其他:
@Email:验证是否为邮件格式,为 null 则不做校验;
@Pattern:验证 String 对象是否符合正则表达式的规则。
在 Controller 中,只需要给方法参数加上 @Validated 即可触发一次校验。也可以通过自定义注解的方式来实现自定义校验。
八、 Redirect 和 Forward
此处的 Redirect 和 Forward 的功能与传统的 Java Web 中的功能一样,只是表现形式不一样。通常而言,Controller 都会返回一个视图名称,比如 ftl 结尾的视图交给 Freemarker 模板引擎渲染,有些情况下,Controller 会返回客户端一个 HTTP Redirect 重定向请求,希望客户端按照指定地址重新发起一次请求,比如客户登录成功后,重定向到后台系统首页。再比如客户端通过 POST 请求提交了一个订单,可以返回一个重定向请求到此订单明细的请求地址。这样做的好处是,如果用户再次刷新页面,则访问的是订单详情地址,而不会再次提交订单。
8.1 Redirect
@ResponseBody public String saveOrder(OrderPostForm form) { // save order return "redirect:/order/detail?orderId=1001"; }
也可以在 ModelAndView 中设置带有 “redirect:” 前缀的 URI:
ModelAndView view = new ModelAndView("redirect:/order/detail?orderId=1001");
或者直接使用 RedirectView 类:
RedirectView view = new RedirectView("/order/detail?orderId=1001");
8.2 Forward
Spring MVC 也支持 forward 前缀,用来在 Controller 执行完毕后,再执行另外一个 Controller 的方法:
@RequestMapping("/bbs") public String index(){ // forward 到 module 方法 return "forward:/bbs/module/1-1" } @RequestMapping("/bbs/module/{type}-{page}") public ModelAndView module(){ // some code }
备注:
传统 Java Web 中,
Redirect 的形式为:response.sendRedirect("资源的URL");//请求重定向到另外的资源。
Forward 的形式为:request.getRequestDispatcher("资源的URL").forward(request,response);//获取请求转发器对象,该转发器的指向通过 getRequestDisPatcher() 的参数设置,然后调用 forward() 方法,转发请求。
九、@Service 和 @Transactional
在 Spring Boot 中,Controller 调用业务逻辑处理交给了被 @Service 注解的类,这也是个普通的 JavaBean,Controller 中可以自动注入这个种 Bean,并调用其方法完成主要的业务逻辑,正如 Controller 注解经常和 @RequestMapping 搭配使用一样,@Service 和 @Transactional 配合使用。
一般在开发中,会定义相关的业务接口,然后实现此接口,并在实现类中添加 @Service 来让 Spring Boot 扫描到,同时搭配上 @Transactonal,Spring Boot 会对这样的 Bean 进行事务增强。如下:
package com.light.springboot.service.impl; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.light.springboot.service.DataService; @Service @Transactional public class DataServiceImpl implements DataService { @Override public void hello() { System.out.println("say Hello"); } }
@Transactional 可以作用在类上,这样,所有的方法都会参与事务管理。也可以放到方法上。
事务对于系统的重要性不言而喻,针对事务,基本的概念和 ACID 四个特性在这里不做赘述,其他需要了解内容有:
9.1 事务主要分为两种管理方式:
编程式事务:编程式事务指的是通过编码方式实现事务,即类似于 JDBC 编程实现事务管理。管理使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager,对于编程式事务管理,Spring推荐使用 TransactionTemplate。
声明式事务:声明式事务建立在 AOP 之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于 @Transactional 注解的方式),便可以将事务规则应用到业务逻辑中。显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。
对比分析:声明式事务管理使业务代码,一个普通的 POJO 对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等。
Spring 事务的七种传播属性和四种隔离级别
9.2 事务传播行为:所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量。
传播行为 | 描述 |
TransactionDefinition.PROPAGATION_REQUIRED | 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。该设置最常用。 |
TransactionDefinition.PROPAGATION_REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起 |
TransactionDefinition.PROPAGATION_SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行 |
TransactionDefinition.PROPAGATION_NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起 |
TransactionDefinition.PROPAGATION_NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常 |
TransactionDefinition.PROPAGATION_MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常 |
TransactionDefinition.PROPAGATION_NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED |
9.3 隔离级别:代表并发事务的隔离程度,也是通过 TransactionDefinition 接口定义的。
隔离级别 | 描述 |
TransactionDefinition.ISOLATION_DEFAULT | 这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED |
TransactionDefinition.ISOLATION_READ_UNCOMMITTED | 该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别 |
TransactionDefinition.ISOLATION_READ_COMMITTED | 该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值 |
TransactionDefinition.ISOLATION_REPEATABLE_READ | 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。 |
TransactionDefinition.ISOLATION_SERIALIZABLE | 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 |
常用数据库默认事务隔离级别:
MySQL:默认为 REPEATABLE_READ
SQLServer:默认为 READ_COMMITTED
Oracle:默认为 READ_COMMITTED
Xml 配置:
<tx:method name="get*" propagation="REQUIRED" isolation="DEFAULT" read-only="true" />
注解方式:
默认的事务传播行为和隔离级别:
@Transactional(propagation=Propagation.REQUIRED)
大多数主流数据库的默认事务等级:isolation=Isolation.READ_COMMITTED
@Transactional(isolation=Isolation.READ_COMMITTED)
十、Jackson 技术
在 MVC 框架中,Spring Boot 内置了Jackson 来完成 JSON de 序列化和反序列化,国内阿里巴巴提供的 Fastjson 也是个不错的工具。总的来说,Jackson 功能非常强大,既能满足简单的序列化和反序列化操作,也能实现复杂的、个性化的序列化和反序列化操作。到目前为止,Jackson 的序列化和反序列化性能都非常优秀,已经是国内外大部分 JSON 相关编程的首选工具。
Jackson 是一个流行的高性能 JavaBean 到 JSON 的绑定工具,Jackson 使用 ObjectMapper 类将 POJO 对象序列化成 JSON 字符串,也能将 JSON 字符串反序列化成 POJO 对象。实际上,Jackson 支持三种层次的序列化和反序列化方式,最常用的一种是采用 DataBind 方式,将 POJO 序列化成 JSON,或者将 JSON 反序列化到 POJO,这是最直接和最简单的一种方式,不过有时候需要借助 Jackson 的注解或者上述序列化实现类来个性化序列化和反序列化操作。
注意:JSON 规范要求 Key 是字符串,且用双引号,尽管很多工具都可以用单引号甚至不用也能识别,但还是建议遵照 JSON 的规范。Jackson从2.0开始改用新的包名 fasterxml,1.x版本的包名是codehaus。除了包名不同,他们的 Maven artifact id 也不同。1.x 版本现在只提供bug-fix,而2.x版本还在不断开发和发布中。如果是新项目,建议直接用 2x,即 fasterxml jackson。
10.1 ObjectMapper 类是 Jackson 库的主要类,它能够实现 Java 对象与 JSON 数据之间的序列化和反序列化
UserEntity 模型类:
package com.light.springboot.entity; import java.util.Date; import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonView; //@JsonIgnoreProperties({"id","name"}) public class UserEntity { private Map<String, Object> map = new HashMap<String, Object>(); private Long id; private String name; private int age; @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", locale = "zh" , timezone="GMT+8") private Date createDate; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } @JsonAnySetter public void other(String property, Object value){ map.put(property, value); System.out.println(property + ":" + map.get(property)); } @JsonAnyGetter public Map<String, Object> getOtherProperties(){ return map; } @Override public String toString() { return "UserEntity [id=" + id + ", name=" + name + ", age=" + age + ", createDate=" + createDate + "]"; } }
JacksonUtil 工具类实现序列化和反序列化:
package com.light.springboot.util; import java.io.IOException; import java.util.Date; import java.util.List; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * Jackson从2.0开始改用新的包名fasterxml,1.x版本的包名是codehaus。除了包名不同,他们的Maven artifact id也不同。 * 1.x版本现在只提供bug-fix,而2.x版本还在不断开发和发布中。如果是新项目,建议直接用2x,即fasterxml jackson * @author Administrator * jackson-core:核心包 * jackson-annotations:注解包 * jackson-databind:数据绑定包 */ public class JacksonUtil { private static ObjectMapper objectMapper = new ObjectMapper(); /** * JSON字符串反序列化成Java对象 * @param json * @return */ public static UserEntity databind(String json){ UserEntity user = null; try { user = objectMapper.readValue(json, UserEntity.class); } catch (JsonParseException e) { e.printStackTrace(); } catch (JsonMappingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return user; } /** * Java对象序列化成JSON字符串 * @param user * @return */ public static String custom(UserEntity user){ String json = null; try { json = objectMapper.writeValueAsString(user); } catch (JsonGenerationException e) { e.printStackTrace(); } catch (JsonMappingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return json; } /** * Jackson提供了JavaType,用来指明集合类型和泛型 * @param collectonClass * @param elementClasses * @return */ public static JavaType getCollectionType(Class<?> collectonClass, Class<?>... elementClasses){ return objectMapper.getTypeFactory().constructParametricType(collectonClass, elementClasses); } /** * 集合的反序列化 * @param jsonArray * @return */ public static List<UserEntity> databinds(String jsonArray){ JavaType type = getCollectionType(List.class, UserEntity.class); List<UserEntity> list = null; try { list = objectMapper.readValue(jsonArray, type); } catch (JsonParseException e) { e.printStackTrace(); } catch (JsonMappingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return list; } public static void main(String[] args) throws JsonProcessingException { //JSON字符串反序列化成Java对象 String json = "{"name":"Rain","id":4405,"age":28,"createDate":"2018-01-25 12:17:22","phone":"13891701101"}"; UserEntity user = databind(json); System.out.println(user.toString()); //Java对象序列化成JSON字符串 //UserEntity user = new UserEntity(); user.setId(10086L); user.setName("Bob"); user.setAge(18); user.setCreateDate(new Date()); System.out.println(custom(user)); //集合的反序列化和序列化 String jsonArray = "[{"name":"Rain","id":4405,"age":28,"createDate":"2018-01-25 12:17:22","phone":"13891701102"}," + "{"name":"Jim","id":4406,"age":26,"createDate":"2018-01-25 12:17:22","phone":"13891701103"}]"; List<UserEntity> userList = databinds(jsonArray); for (UserEntity userEntity : userList) { System.out.println(userEntity.toString()); } //集合的序列化 System.out.println(objectMapper.writeValueAsString(userList)); } }
10.2 Jackson 常用注解
Jackson 包含了很多注解,用来个性化序列化和反序列化操作,主要包含如下注解。
@JsonProperty,作用在属性上,用来为 JSON Key 指定一个别名;
@JsonIgnore,作用在属性上,用来忽略此属性;
@JsonIgnoreProperties,忽略一组属性,作用在类上,比如 @JsonIgnorePropertiess({"id","phone"});
@JsonAnySetter,标记在某个方法上,此方法接收 Key、Value,用于 Jackson 在反序列化过程中,未找到的对应属性都调用此方法。通常这个方法用一个 Map 来实现;
@JsonAnyGetter,此注解标注在一个返回 Map 的方法上,Jackson 会取出 Map中的每一个值进行序列化;
@JsonFormat,用于日期的格式化,如:
@JsonFormat(pattern="yyyy-MM-dd HH-mm-ss") private Date createDate;
@JsonNaming,用于指定一个命名策略,作用于类或者属性上,类似 @JsonProperty,但是会自动命名。Jackson 自带了多种命名策略,你也可以实现自己的命名策略,比如输入的 Key 由 Java 命名方式转换为下划线命名方式:userName 转换为 user-name;
@JsonSerialize,指定一个实现类来自定义序列化。类必须实现 JsonSerializer 接口;
@JsonDeserialize,用户自定义反序列化,同 @JsonSerialize,类需要实现 JsonDeserialize 接口;
@JsonView,作用在类或者属性上,用来定义一个序列化组。Spring MVC 的 Controller 方法可以使用同样的 @JsonView 来序列化属于这一组的配置。