类和接口

第 15 条:使类和成员的可访问性最小化

公有静态 final 域暴露常量,要么包含基本类型值,要么包含指向不可变对象的引用

第 16 条:在公有类中使用访问方法而非公有域

  • 公有类不应该暴露可变域,因为其客户端代码遍布各处,将来极难改变内部表示法
  • 公有类暴露不可变域危害较小,事实上也不应该
  • 包级私有或私有嵌套类,直接暴露数据域并没有本质错误

第 17 条:使可变性最小化

如果类不能被做成不可变,仍然应该尽可能限制它的可变性,降低对象可以存在的状态数!构造器应该创建完全初始化的对象,并建立所有的约束关系,不要在构造器或静态工厂之外再提供公有的初始化方法,除非有足够的理由!

第 18 条:复合优先于继承

在包内部使用继承是安全的,跨越包边界的继承是危险的

第 19 条:要么为继承而设计,并提供文档说明,要么就禁止继承

对于专门为继承而设计且有良好文档说明的类

  • 该类文档必须有文档说明它可覆盖方法的自用性
  • 该类必须通过某种形式提供适当的钩子,以便能进入它的内部工作流程中
  • 构造器,clone 或者 readObject 绝不能调用可被覆盖的方法,不管是直接还是间接

并非为了安全进行子类化而设计和编写文档的类,要禁止子类化

  • 将类声明为 final
  • 将构造器变成 private 或 package-private,并增加一些公有的静态工厂来替代构造器

第 20 条:接口优于抽象类

接口与抽象类的区别

  • 抽象类允许包含某些方法的实现,接口不允许
  • 为实现抽象类定义类型,类必须成为抽象类的子类,而接口则不用关心类层次
  • 现有类容易被更新以实现新接口,一般无法更新现有类来扩展新抽象类
  • 接口是定义 mixin 的理想选择,mixin 用来表明类提供了某些可供选择的行为,允许任选功能混合到类型主要功能中
  • 接口允许构造非层次结构的类型框架

第 21 条: 为子类设计接口

编写一个默认方法并不总是可能的,它保留了每个可能的实现的所有不变量。

在默认方法的情况下,接口的现有实现类可以在没有错误或警告的情况下编译,但在运行时会失败。

第 22 条:接口只用于定义类型

常量接口模式是对接口的不良使用,是反面典型,不值得效仿!

关键原因:如果类实现常量接口,将来版本中类修改了,不需要这些常量了,它还依然必须实现这个接口,以确保二进制兼容性!

第 23 条:类层次优于标签类

类层次!首先,定义一个抽象类作为根类,提取公用方法到抽象类中!然后,为每种原始标签类定义根类的具体子类,子类包含特定域和方法!

第 24 条:优先考虑静态成员类,而不是非静态类

第 25 条: 将源文件限制为单个顶级类

虽然 Java 编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。 风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能。 使用哪个定义会受到源文件传递给编译器的顺序的影响。


对所有对象都通用的方法

第 10 条:覆盖 equals 时请遵守通用约定

  • 自反性 - 对象必须等于其自身
  • 对称性 - 任何两个对象对于它们是否相等的问题都必须保持一致,Timestamp 违反了,不值得效仿
  • 传递性 - 如果对象 A 等于B,B 等于 C,那么 A 一定等于 C
  • 一致性 - 相等的对象永远相等,不相等的对象永远不相等
  • 非空性 - 所有的对象都必须不等于 null

第 11 条:覆盖 equals 时总要覆盖 hashCode

  • 程序一次执行期间,只要对象的 equals 比较用到的信息没有被修改,则对该对象调用多次 hashCode 都必须返回同一值
  • 程序多次执行过程,每次执行返回的 hashCode 可以不一致
  • 相等的对象必须有相等的散列码
  • 不相等的对象不一定要产生不同的散列码,但是通常倾向于为不相等对象产生不相等的散列码

注意,如果一个类是不可变的,同时计算散列码开销也大,则可把散列码缓存在对象内部!不要试图从散列码计算中排除掉一个对象的关键部分来提高性能!

第 12 条:始终要覆盖toString

java.lang.Object 提供了 toString 方法的一个实现:类名 + ‘@’ + 散列码无符号 16 进制表示!

无论是否指定格式,都为 toString 返回值中包含的所有信息提供一种编程式的访问途径!如果没有,则会迫使程序员编写额外代码解析字符串,既降低性能,又容易解析出错导致系统不稳定!

第 13 条:谨慎地覆盖 clone

Object 的 clone 方法是受保护的!Cloneable 接口没有包含任何方法!Cloneable 改变了超类中受保护的方法的行为!对于实现了 Cloneable 的类,我们总是希望它提供一个功能适当的公有的 clone 方法!此方法首先调用 super.clone,然后修正任何需要修正的域!

实现对象拷贝更好的办法

  • 拷贝构造器 - 唯一参数类型是包含该构造器的类,public Yum(Yum yum);
  • 拷贝工厂 - public static Yum newInstance(Yum yum);

第 14 条:考虑实现 Comparable 接口

包含 compareTo 方法的对象与指定对象比较,当该对象小于、等于或者大于指定对象时,分别返回一个负整数、零或者正整数!compareTo 要满足自反性、对称性和传递性!

注意,比较整数基本类型域可使用关系操作符<和>,浮点域可使用 Double.compare 或者 Float.compare!

通常在 compareTo 中为了简化代码,会以两数作差表示比较大小的结果,这种方法有风险!记住!一个有符号的 32 位的整数还没有大到足以表达任意两个32位整数的差!如一个很大的正整数减去一个很大负整数将会溢出,并返回一个负值,导致错误结果!


创建和销毁对象

第 1 条 : 考虑使用静态工厂方法替代构造方法

静态工厂方法实例

public static Boolean valueOf(boolean b) { 
return b ? Boolean.TRUE : Boolean.FALSE;
}

静态工厂方法的优点:

  • 有方法名,比构造方法更加容易理解
  • 不用重复创建新对象
  • 可以返回子类型
  • 返回对象的类可以根据输入参数的不同而不同
  • 静态方法更适于做服务型接口

静态工厂方法的缺点:

  • 可能无法被子类实例化(私有的构造器)
  • 不利于检索(比较难找到对应代码)

第2条:当构造方法参数过多时使用 builder 模式

可选参数比较多时,可用下面几种方法

  1. 可伸缩构造方法模式,但是当有很多参数时,很难编写,并读懂它。
  2. 使用javaBean模式,通过setter方法设置参数,但构造变得困难,而且变得不安全
  3. Builder模式,不直接生成对象,而是先得到builder对象,通过builder的类似setter方法设置参数,最后调用build方法生成不可变对象

缺点:Builder 模式比重叠构造器更冗长,只在参数很多的时候才使用,比如四个或者更多。

Builder方法实例

// Builder Pattern 
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;

public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;

// Optional parameters - initialized to default values
private int calories
private int fat
private int sodium= 0; = 0; = 0;
private int carbohydrate = 0;

public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}

public Builder calories(int val) {
calories = val;
return this;
}

public Builder fat(int val) {
fat = val;
return this;
}

public Builder sodium(int val) {
sodium = val;
return this;
}

public Builder carbohydrate(int val) {
carbohydrate = val; return this;
}

public NutritionFacts build() {
return new NutritionFacts(this);
}
}

private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate
}
}

第3条:使用私有构造方法或枚举类实现 Singleton 属 性

Java 1.5 之前,实现单例是通过“构造器私有化,静态公有 final 实例对象”,或直接获取,或通过方法获取。

Java 1.5 之后,实现单例的最佳方法,是编写“单元素的枚举”类型:

public enum Elvis {
INSTANCE;
public void whateverMethod() { ... }
}

无偿提供序列化,防止多次实例化。

第4条:使用私有构造器执行非实例化

一些工具类不希望被实例化,将构造函数私有,避免外部调用,并在构造函数中抛异常,避免内部调用。

第5条:依赖注入优于硬连接资源(hardwiring resources)

行为参数化是依赖注入的有用变体,将Lexicon dictionary作为参数,相对硬编码更加灵活。

对于行为参数化的类,可以通过策略模式和lambda表达式灵活实现

第6条:避免创建不必要的对象

最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。如果对象是不可变的,它就始终可以被重用。

优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱,比如:下面这个程序会有大量装箱成Long

Long sum = 0
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;

使用 Byte.valueOf 来创建包装类型,因为 -128~127 的数会缓存起来,要从缓冲池中取,Short、Integer、Long 也是这样。

第 7 条:消除过期的对象引用

不需要对所有对象显式置空,以下情形考虑资源手工处理:

  • 类是自己管理内存,如例子中的 Stack 类。
  • 使用对象缓存机制时,需要考虑被从缓存中换出的对象,或是长期不会被访问到的对象。
  • 事件监听器和相关回调。使用弱引用

第 8 条:避免使用终结方法

finalizer 不保证会被及时的执行,甚至不一定执行。

除非是作为安全网,或者为终止非关键的本地资源,否则请不要使用终结方法。

第 9 条: 用 try-with-resources 代替 try-finally

实现 AutoCloseable 接口的类应使用 try-with-resources,因其可同时处理多个资源,不用嵌套


Spring MVC

1 Spring MVC 概览

Spring 提供了一个功能齐全的 MVC 框架用于构建 Web 应用程序。Spring 框架可以很容易的和其他的 MVC 框架融合(如 Struts),该框架使用控制反转(IOC)将控制器逻辑和业务对象分离开来。它也允许以声明的方式绑定请求参数到业务对象上。

  • DispatcherServlet
    • Spring 的 MVC 框架是围绕 DispatcherServlet 来设计的,它用来处理所有的 HTTP 请求和响应。
  • WebApplicationContext
    • WebApplicationContext 继承了 ApplicationContext,并添加了一些 web 应用程序需要的功能。和普通的 ApplicationContext 不同,WebApplicationContext 可以用来处理主题样式,它也知道如何找到相应的 servlet。
  • Controller
    • 控制器提供对应用程序行为的访问,通常通过服务接口实现。控制器解析用户的输入,并将其转换为一个由视图呈现给用户的模型。Spring 通过一种极其抽象的方式实现控制器,它允许用户创建多种类型的控制器。
    • @Controller 注解表示该类扮演控制器的角色。Spring 不需要继承任何控制器基类或应用 Servlet API。
  • @RequestMapping 注解用于将 URL 映射到任何一个类或者一个特定的处理方法上。

1.1 Spring MVC 运行原理

img

  1. Spring MVC 通过一个单独的前端控制器(DispatcherServlet)过滤分发请求。
  2. DispatcherServlet 根据处理器映射(HandlerMapping)和请求携带的 URL 决定将请求发送给某个控制器(Controller)。
  3. 控制器从请求中取得信息,然后委托业务逻辑组件处理。将处理结果打包在模型(model)中,然后指定一个视图(view)的逻辑名称,然后将请求和模型、视图名称一起发送回 DispatcherServlet。
  4. DispatcherServlet 用视图名称查找对应的视图解析器(ViewResolver),负责将逻辑名称转换成对应的页面实现。
  5. 最后一步就是视图的实现。视图会使用模型数据填充到视图实现中,然后将结果放在 HTTP 响应对象中。

1.2 Spring MVC 配置

1)配置前端控制器

继承了 AbstractAnnotationConfigDispatcherServletInitializer,会在项目运行初始化被自动发现并加载。

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// 根容器
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
// Spring mvc 容器,指定配置类
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
// DispatcherServlet 映射,从"/"开始
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}

AppInitializer 类需要实现三个方法,RootConfig 和 WebConfig 是两个关键配置类,而 getServletMappings 只需要返回一个 String 的列表,{“/”}的意思是监听访问 url 下所有的请求。

2)配置视图解析器

@EnableWebMvc:启动 Spring MVC 特性
configer.enable():静态资源的请求将转交给 servlert 容器的 default servlet 处理。

  • setPrefix() 方法用于设置视图路径的前缀;
  • setSuffix() 用于设置视图路径的后缀;
  • setExposeContextBeansAsAttributes(true) 使得可以在 JSP 页面中通过 ${ } 访问容器中的 bean
@Configuration
@EnableWebMvc//启动Spring MVC
@ComponentScan("org.test.spittr.web")//启动组件扫描
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public ViewResolver viewResolver() {
// 配置JSP视图解析器
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
// 可以在JSP页面中通过${}访问beans
resolver.setExposeContextBeansAsAttributes(true);
return resolver;
}

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable(); //配置静态文件处理
}
}
3)配置 Bean

RootConfig 在设置扫描机制的时候,将之前 WebConfig 设置过的那个包排除了。

@Configuration
@ComponentScan(basePackages = { "org.test.spittr.controller" }, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class) })
public class RootConfig {
}

2 编写 Controller

2.1 控制器类

控制器类就是含有被 @RequestMapping 注解修饰的方法的类。@Controller 是 @Component 的别名,返回一个视图逻辑名称。

@RequestMapping 可加在类上和方法上,可同时映射多个路径

@Controller
@RequestMapping(value = "/")
public class HomeController {
@RequestMapping(value = { "/", "/homepage" }, method = RequestMethod.GET)
public String home() {
return "home";
}
}

2.2 请求参数

Spring MVC 提供了三种方式,可以让客户端给控制器的 handler 传入参数:

1)查询参数

@RequestParam,可设默认和非必填

@RequestMapping(method = RequestMethod.GET)
public List<Spittle> spittles(@RequestParam("max") long max,
@RequestParam(value = "count", defaultValue = "20") int count) {
return spittleRepository.findSpittles(max, count);
}
2)表单参数
  • “redirect:”前缀:解析为重定向的规则, 而不是视图的名称。
  • “forward:”前缀:请求将会前往(forward) 指定的 URL 路径, 而不再是重定向。
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(Spitter spitter) {
return "redirect:/spitter/" + spitter.getUsername();
}
3)路径参数

@PathVariable,如果函数参数和占位符名称相同,可省略注解的参数

@RequestMapping(value = "abc/{spittleId}", method = RequestMethod.GET)
public String showSpittle(@PathVariable("spittleId") long spittleId,Model model) {
}

2.3 校验参数

Java Validation API 定义了多个注解, 这些注解可以放到属性上,从而限制这些属性的值。

注 解 描 述
@AssertFalse 所注解的元素必须是 Boolean 类型, 并且值为 false
@AssertTrue 所注解的元素必须是 Boolean 类型, 并且值为 true
@DecimalMax 所注解的元素必须是数字, 并且它的值要小于或等于给定的 BigDecimalString 值
@DecimalMin 所注解的元素必须是数字, 并且它的值要大于或等于给定的 BigDecimalString 值
@Digits 所注解的元素必须是数字, 并且它的值必须有指定的位数
@Future 所注解的元素的值必须是一个将来的日期
@Max 所注解的元素必须是数字, 并且它的值要小于或等于给定的值
@Min 所注解的元素必须是数字, 并且它的值要大于或等于给定的值
@NotNull 所注解元素的值必须不能为 null
@Null 所注解元素的值必须为 null
@Past 所注解的元素的值必须是一个已过去的日期
@Pattern 所注解的元素的值必须匹配给定的正则表达式
@Size 所注解的元素的值必须是 String、 集合或数组, 并且它的长度要符合给定的范围
校验 bean 对象

@Valid 注解标注要检验的参数,Errors 参数要紧跟其后面

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@Valid Spitter spitter, Errors errors) {
if(errors.hasErrors()){
...
}
return "redirect:/spitter/" + spitter.getUsername();
}

2.4 其他注解

  • @RequestBody:将方法参数直接绑定到 HTTP 请求 Body 上
  • @ResponseBody:将返回值作为响应体
  • @RestController:避免重复写 @ResponseBody
  • @CookieValue
  • @RequestHeader

Spring 4.3 中引进了{@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping} 来帮助简化常用的 HTTP 方法的映射 并更好地表达被注解方法的语义

  • @GetMapping 是一个组合注解:是 @RequestMapping(method = RequestMethod.GET) 的缩写
  • @PostMapping 是一个组合注解:是 @RequestMapping(method = RequestMethod.POST) 的缩写

Spring MVC 进阶

1 替代配置

1.1 自定义 DispatcherServlet

除了三个必须重载的抽象方法,AbstractAnnotationConfigDispatcherServletInitializer 还有很多方法可以重载,以实现额外配置。

例如,通过对 customizeRegistration() 的重写,就可以对 DispatcherServlet 进行额外的配置。

@Override 
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}

ServletRegistration.Dynamic 作为入参,可以做很多事情,比如调用 setLoadOnStartup() 来设置加载时优先级,调用 setInitParameter() 来设置初始化参数,调用 setMultipartConfig() 来设置 Servlet3.0 的多路支持。

1.2 添加额外的 servlet 和 filter

实现 WebApplicationInitializer 接口是在注册 servlet、filter、listener 时比较推荐的方式

public class MyServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// 定义servlet
Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);
// 映射servlet
myServlet.addMapping("/custom/**");

// 注册一个filter
javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
// 添加映射
filter.addMappingForUrlPatterns(null, false, "/custom/*");
}
}

如仅需要注册一个 filter 并将其映射到 DispatcherServlet,重写 AbstractAnnotationConfigDispatcherServletInitializer 的 getServletFilters() 方法是一个捷径。

@Override
protected Filter[] getServletFilters() {
return new Filter[] { new MyFilter() };
}

通过 getServletFilters() 返回的 filter 会自动地映射到 DispatcherServlet。

2 处理 multipart 表单数据

Multipart/form-data 将表单分割成独立的部分,每个部分都有各自的类型,可以处理二进制数据

2.1 配置 multipart 解析器

Spring 提供了两种 MultipartResolver 实现类:

  • CommonsMultipartResolver:使用 Jakarta Commons FileUpload 来解析 multipart 请求;
  • StandardServletMultipartResolver:依靠 Servlet 3.0 支持来解析(Spring 3.1及以上);

推荐 StandardServletMultipartResolver,它使用 servlet 容器中现有的支持,并且不需要其他附加的项目依赖。

@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
}
配置 multipart 详情

MultipartConfigElement

  1. 继承自 WebMvcConfigurerAdapter 的 servlet 初始化类中配置的 DispatcherServlet,在 servlet 注册时通过调用 setMultipartConfig() 方法来配置
DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
  1. 继承自 AbstractAnnotationConfigDispatcherServletInitializer 的 servlet 初始化类,重写 customizeRegistration() 方法来进行配置
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}

2.2 处理 multipart 请求

1)表单

标签 enctype=“multipart/form-data” 属性 type=“file”

<form method="POST" th:object="${spitter}" enctype="multipart/form-data">
...
<input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif" />
...
2)controller

使用 @RequestPart 注解 byte 数组

@PostMapping("/register")
public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter,Errors errors) {
}
3)接收 MultipartFile

Spring 提供了 MultipartFile 接口获取富对象

@PostMapping("/register")
public String processRegistration(@RequestPart("profilePicture") MultipartFile profilePicture, ...) {
}

MultipartFile 提供获取上传文件的方法,同时提供了很多其他方法,比如原始文件名称、大小和内容类型等。另外还提供了一个 InputStream 可以将文件数据作为数据流读取。

transferTo() 写入到文件系统

profilePicture.transferTo(new File("/data/spittr/" + profilePicture.getOriginalFilename()));
4)接收 Part

Servlet 3.0 的容器上,可以选择 Part,大多数情况下 Part 接口和 MultipartFile 没什么区别。

@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") Part profilePicture, ...) {

如果使用 Part 作为参数,就不再需要配置 StandardServletMultipartResolverbean,它只需在使用 MultipartFile 时进行配置。

3 异常处理

servlet 请求的输出只能是 servlet 响应。如果请求出现异常,需要将异常转换为 servlet 响应。

Spring 提供了一些将异常转化为响应的方法:

  • 某些 Spring 异常会自动的映射为特定的 HTTP 状态码;
  • 使用@ResponseStatus注解将一个异常映射为 HTTP 状态码;
  • 使用ExceptionHandler注解的方法可以用来处理异常

3.1 @ResponseStatus

内置映射之外,可用@ResponseStatus注解将一个异常映射为 HTTP 状态码

@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Spittle Not Found")
public class SpittleNotFoundException extends Exception {
}

3.2 @ExceptionHandler

@ExceptionHandler 注解的方法在有指定异常抛出时执行,在同一个 controller 里通用

@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle() {
return "error/duplicate";
}

3.3 @ControllerAdvice

控制器增强类,即使用@ControllerAdvice进行注解的类,由以下方法构成:

  • @ExceptionHandler 注解的
  • @InitBinder 注解的
  • @ModelAttribute 注解的

应用于所有 controller 的所有 @RequestMapping 注解的方法。

@ControllerAdvice
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handle(ValidationException exception) {
log.warn("bad request, " + exception.getMessage());
return "bad request, " + exception.getMessage();
}

@ExceptionHandler({BaseException.class})
public ResponseEntity<?> handleBaseException(final Exception ex, WebRequest request) {
BaseException baseEx = (BaseException) ex;
return handleExceptionInternal(ex, ErrorResponse.of(baseEx), new HttpHeaders(), baseEx.getHttpStatus(), request);
}

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, ErrorResponse.of(ex), headers, HttpStatus.BAD_REQUEST, request);
}

@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, new ErrorResponse(ex.getMessage(), "missing_request_parameter"), headers, status, request);
}

@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ErrorResponse {
private final String message;
private final String code;
List<String> errors;

private ErrorResponse(String message, String code) {
this.code = code;
this.message = message;
}

private ErrorResponse(String message, String code, List<String> errors) {
this.message = message;
this.code = code;
this.errors = errors;
}

public static ErrorResponse of(BaseException ex) {
return new ErrorResponse(ex.getMessage(), ex.getCode());
}

public static ErrorResponse of(MethodArgumentNotValidException ex) {
List<String> errors = new ArrayList<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.add(error.getField() + ": " + error.getDefaultMessage());
}
for (ObjectError error : ex.getBindingResult().getGlobalErrors()) {
errors.add(error.getObjectName() + ": " + error.getDefaultMessage());
}
return new ErrorResponse("输入不合法", "bad_request", errors);
}

public static ErrorResponse of(Map<String, Object> errorAttributes) { //其他异常
return new ErrorResponse((String) errorAttributes.get("message"), (String) errorAttributes.get("error"));
}

public String getMessage() {
return message;
}

public String getCode() {
return code;
}

public List<String> getErrors() {
return errors;
}
}
}

BaseException

@Data
public abstract class BaseException extends RuntimeException {

protected HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

protected String code = "unknown_error";

public BaseException(String message) {
super(message);
}

public BaseException(String message, Throwable cause) {
super(message, cause);
}
}

InternalServerErrorException

public class InternalServerErrorException extends BaseException {

{
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
code = "internal_server_error";
}

public InternalServerErrorException() {
super("系统内部发生未知异常");
}

public InternalServerErrorException(String message) {
super(message);
}

public InternalServerErrorException(String message, Throwable cause) {
super(message, cause);
}

}

4 跨 redirect 传递数据

一般的,处理函数结束后,方法中的 model 数据都会作为 request 属性复制到 request 中,并且 request 会传递到视图中进行解析。

  • forward 时,使用同一个 request,request 属性保留。
  • redirect 时,终止老 request,开启新 request。request 属性清空。

redirect 时不能使用 model 传递数据了。但是还有其他方法:

  • 将数据转换为路径参数或者查询参数
  • 在 flash 属性中发送数据

4.1 使用 URL 模版重定向

使用路径参数和查询参数传递简单数据

@PostMapping("/register")
public String processRegistration(Spitter spitter, Model model) {
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
model.addAttribute("spitterId", spitter.getId());
return "redirect:/spitter/{username}";
}

username 作为路径参数,spitterId 转为查询参数

4.2 使用 flash 属性

Spring 通过 Model 的子接口 RedirectAttributes 设置 flash 属性。

重定向前,所有的 flash 属性都会拷贝到 session 中

@PostMapping("/register")
public String processRegistration(Spitter spitter, RedirectAttributes model) {
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
model.addFlashAttribute("spitter", spitter);
return "redirect:/spitter/{username}";
}

重定向后,存储在 session 中的 flash 属性会从 session 中移出到 model 中。

@GetMapping("/{username}")
public String showSpitterProfile(@PathVariable("username") String username, Model model) {
if (!model.containsAttribute("spitter")) {
Spitter spitter = spitterRepository.findByUsername(username);
model.addAttribute(spitter);
}
return "profile";
}

Spring RestTemplate

RestTemplate 属于 Spring-Web,是 Spring 的同步客户端HTTP访问的中心类。简化了与 HTTP 服务器的通信,并应用了 RESTful 原则。

RestTemplate 默认依赖 JDK 的 HttpURLConnection 来建立 HTTP 连接。 可切换到使用不同的 HTTP 库,例如 Apache HttpComponents,Netty 和 OkHttp。

1 组成

RestTemplate 包含以下几个部分:

  • HttpMessageConverter:对象转换器
  • ClientHttpRequestFactory:客户端连接器,默认是 JDK 的 HttpURLConnection
  • ResponseErrorHandler:异常处理
  • ClientHttpRequestInterceptor:请求拦截器

img

2 初始化

初始化时,可以传入 ClientHttpRequestFactory,自定义参数。

@Bean
RestTemplate restTemplate(){
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
//设置超时时间
requestFactory.setConnectTimeout(1000);
requestFactory.setReadTimeout(1000);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}

注入

@Autowired
private RestTemplate restTemplate;

3 访问服务

3.1 HTTP 方法

使用 java.net.URI 代替 String 形式的 URI,不会被 URL 编码两次
以 get 和 post 为例,更多见 官网api

1)GET
  • getForObject()
public <T> T getForObject(URI url, Class<T> responseType)
public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables)
public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> urlVariables)
  • getForEntity()
public <T> ResponseEntity<T> getForEntity(URI url,Class<T> responseType)
public <T> ResponseEntity<T> getForEntity(String url,Class<T> responseType,Object... uriVariables)
public <T> ResponseEntity<T> getForEntity(String url,Class<T> responseType,Map<String,?> uriVariables)
2)POST
  • postForObject()
public <T> T postForObject(URI url,Object request,Class<T> responseType)
public <T> T postForObject(String url,Object request,Class<T> responseType,Object... uriVariables)
public <T> T postForObject(String url,Object request,Class<T> responseType,Map<String,?> uriVariables)
  • postForEntity()
public <T> ResponseEntity<T> postForEntity(String url,@NullableObject request,Class<T> responseType,Object... uriVariables)
public <T> ResponseEntity<T> postForEntity(String url,Object request,Class<T> responseType,Map<String,?> uriVariables)
public <T> ResponseEntity<T> postForEntity(URI url,Object request,Class<T> responseType)
3)实例
HttpHeaders headers = new HttpHeaders();
headers.add("X-Auth-Token", "");

MultiValueMap<String, String> postParameters = new LinkedMultiValueMap<String, String>();
postParameters.add("parameter1", "111");
postParameters.add("parameter2", "222");
postParameters.add("parameter3", "333");

HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(postParameters, headers);

Object result = null;

try {
result = restTemplate.postForObject("http://demo", requestEntity, ParseResultVo.class);
} catch (RestClientException e) {
}

4 异常处理

1)捕获 HttpServerErrorException
int retryCount = 0;  
while (true) {
try {
responseEntity = restTemplate.exchange(requestEntity, String.class);
break;
} catch (HttpServerErrorException e) {
if (retryCount == 3) {
throw e;
}
retryCount++;
}
}
2)自定义异常处理
public class CustomErrorHandler extends DefaultResponseErrorHandler {  

@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return true;
}

@Override
public void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
if(statusCode.isError()){
switch (statusCode.series()) {
case CLIENT_ERROR:
throw new HttpClientErrorException(statusCode, response.getStatusText(), response.getHeaders(), this.getResponseBody(response), this.getCharset(response));
case SERVER_ERROR:
throw new HttpServerErrorException(statusCode, response.getStatusText(), response.getHeaders(), this.getResponseBody(response), this.getCharset(response));
default:
throw new UnknownHttpStatusCodeException(statusCode.value(), response.getStatusText(), response.getHeaders(), this.getResponseBody(response), this.getCharset(response));
}
}
}

}

@Configuration
public class RestClientConfig {

@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new CustomErrorHandler());
return restTemplate;
}

}

5 设置连接池

@Configuration  
public class RestClientConfig {

@Bean
public ClientHttpRequestFactory httpRequestFactory() {
return new HttpComponentsClientHttpRequestFactory(httpClient());
}

@Bean
public RestTemplate restTemplate() {
return new RestTemplate(httpRequestFactory());
}

@Bean
public HttpClient httpClient() {
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);

RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(8000)
.setConnectTimeout(8000)
.setConnectionRequestTimeout(8000)
.build();

return HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.setConnectionManagerShared(true)//设置共享连接池
.build();
}

}

6 处理文件

6.1 发送文件

MultiValueMap<String, Object> multiPartBody = new LinkedMultiValueMap<>();  
multiPartBody.add("file", new ClassPathResource("/tmp/user.txt"));
RequestEntity<MultiValueMap<String, Object>> requestEntity = RequestEntity
.post(uri)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(multiPartBody);

6.2 下载文件

// 小文件  
RequestEntity requestEntity = RequestEntity.get(uri).build();
ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);
byte[] downloadContent = responseEntity.getBody();

// 大文件
ResponseExtractor<ResponseEntity<File>> responseExtractor = new ResponseExtractor<ResponseEntity<File>>() {
@Override
public ResponseEntity<File> extractData(ClientHttpResponse response) throws IOException {
File rcvFile = File.createTempFile("rcvFile", "zip");
FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));
return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(rcvFile);
}
};
File getFile = this.restTemplate.execute(targetUri, HttpMethod.GET, null, responseExtractor);

7 Spring Boot

RestTemplateBuilder

@Component  
public class CustomRestTemplateCustomizer implements RestTemplateCustomizer {
@Override
public void customize(RestTemplate restTemplate) {
new RestTemplateBuilder()
.detectRequestFactory(false)
.basicAuthorization("username", "password")
.uriTemplateHandler(new OkHttp3ClientHttpRequestFactory())
.errorHandler(new CustomResponseErrorHandler())
.configure(restTemplate);
}
}

单独设置

@Service  
public class MyRestClientService {

private RestTemplate restTemplate;

public MyRestClientService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder
.basicAuthorization("username", "password")
.setConnectTimeout(3000)
.setReadTimeout(5000)
.rootUri("http://api.example.com/")
.errorHandler(new CustomResponseErrorHandler())
.additionalMessageConverters(new CustomHttpMessageConverter())
.uriTemplateHandler(new OkHttp3ClientHttpRequestFactory())
.build();
}

public String site() {
return this.restTemplate.getForObject("http://rensanning.iteye.com/", String.class);
}

}

8 参数设置

8.1 指定转换器

RestTemplate 默认注册了一组 HttpMessageConverter 用来处理一些不同的 contentType 的请求。

StringHttpMessageConverter 来处理 text/plain;

MappingJackson2HttpMessageConverter 来处理 application/json;

MappingJackson2XmlHttpMessageConverter 来处理 application/xml。

可实现 org.springframework.http.converter.HttpMessageConverter 接口自己写一个转换器。

替换例子:

RestTemplate restTemplate = new RestTemplate();

//获取RestTemplate默认配置好的所有转换器
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();

//默认的MappingJackson2HttpMessageConverter在第7个 先把它移除掉
messageConverters.remove(6);
//添加上GSON的转换器
messageConverters.add(6, new GsonHttpMessageConverter());

8.2 设置底层连接方式

通过构造参数设置,以切换 HttpClient 为例

//生成一个设置了连接超时时间、请求超时时间、异常最大重试次数的 httpClient
RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(10000).setConnectTimeout(10000).setSocketTimeout(30000).build();

HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(config).setRetryHandler(new DefaultHttpRequestRetryHandler(5, false));

HttpClient httpClient = builder.build();

//使用httpClient创建一个 ClientHttpRequestFactory 的实现
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

//ClientHttpRequestFactory作为参数构造一个使用作为底层的 RestTemplate
RestTemplate restTemplate = new RestTemplate(requestFactory);

8.3 设置拦截器

拦截器需要我们实现 org.springframework.http.client.ClientHttpRequestInterceptor 接口。

public class TokenInterceptor implements ClientHttpRequestInterceptor{ 
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
//请求地址
String checkTokenUrl = request.getURI().getPath();
//请求方法名 POST、GET等
String methodName = request.getMethod().name();
//请求内容
String requestBody = new String(body);
//……
return execution.execute(request, body);
}
}

创建 RestTemplate 实例的时候,添加拦截器

RestTemplate restTemplate = new RestTemplate();

//向restTemplate中添加自定义的拦截器
restTemplate.getInterceptors().add(new TokenInterceptor());

8.4 使用 Proxy

RestTemplate
@Bean
RestTemplate restTemplate(){
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("your.proxy.server", 8080));
requestFactory.setProxy(proxy);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
System properties
Properties props = System.getProperties();
props.put("https.proxyHost", "your.proxy.server");
props.put("https.proxyPort", "8080");
props.put("http.proxyHost", "your.proxy.server");
props.put("http.proxyPort", "8080");

RestTemplate restTemplate = new RestTemplate();
String tt = restTemplate.getForObject("https://baike.baidu.com/",String.class);
System.out.println(tt);

跨域

常用的跨域解决方案有 JSONP 和 CORS, spingboot 2.0 开始不推荐使用 JSONP。

1 CORS

1.1 属性含义

属性 含义
value 指定所支持域的集合, 表示所有域都支持,默认值为 。这些值对应于 HTTP 请求头中的 Access-Control-Allow-Origin
origins @AliasFor(“value”),与 value 属性一样
allowedHeaders 允许请求头中的 headers,在预检请求 Access-Control-Allow-Headers 响应头中展示
exposedHeaders 响应头中允许访问的 headers,在实际请求的 Access-Control-Expose-Headers
methods 支持的 HTTP 请求方法列表,默认和 Controller 中的方法上标注的一致。
allowCredentials 表示浏览器在跨域请求中是否携带凭证,比如 cookies。在预检请求的 Access-Control-Allow-Credentials
maxAge 预检请求响应的最大缓存时间,单位为秒。在预检请求的 Access-Control-Max-Age 响应头中展示

1.2 实现方法

1) CrossOrigin 注解

在 Spring Boot 中为我们提供了一个注解 @CrossOrigin 来实现跨域,这个注解可以加在类或者方法上。

@Controller
public class HomeController {

@GetMapping("/xxx")
@ResponseBody
@CrossOrigin
public String xxx() {
return "xxx";
}
}

2)实现 WebMvcConfigurer 接口

@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(168000)
.allowedHeaders("*");
}
}

3)过滤器

@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}

2 Jsonp

spingboot2.0 已不支持 JSONP

通过 jsonp 调用,对格式重新封装,解决前端跨域。
如:请求http://xxxx?&callback=exec, 那么返回的jsonp格式为exec({"code":0, "message":"success"});

继承AbstractJsonpResponseBodyAdvice,加入@ControllerAdvice注解,basePackages 标识要被处理的 controller。

@ControllerAdvice(basePackages = "xxx.controller")
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {

private final String[] jsonpQueryParamNames;

public JsonpAdvice() {
super("callback", "jsonp");
this.jsonpQueryParamNames = new String[]{"callback"};
}

@Override
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {

HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
     
    //如果不存在callback这个请求参数,直接返回,不需要处理为jsonp
if (ObjectUtils.isEmpty(servletRequest.getParameter("callback"))) {
return;
}
    //按设定的请求参数处理返回结果为jsonp格式
for (String name : this.jsonpQueryParamNames) {
String value = servletRequest.getParameter(name);
if (value != null) {
MediaType contentTypeToUse = getContentType(contentType, request, response);
response.getHeaders().setContentType(contentTypeToUse);
bodyContainer.setJsonpFunction(value);
return;
}
}
}
}

Ehcache 3.8 简单持久化

1 引入依赖

<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.8.1</version>
</dependency>

2 持久化配置

public class EhcacheHelper {

private static CacheManager cacheManager;

public static CacheManager getCacheManager() {
if(!isAvailable()){
cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.with(CacheManagerBuilder.persistence(new File("java.io.tmpdir", "EhcacheData")))
.withCache("xxxCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(2000, EntryUnit.ENTRIES)
.offheap(1, MemoryUnit.MB)
.disk(20, MemoryUnit.MB, true)
)
).build(true);
}
return cacheManager;
}

public static void close() {
if(isAvailable()){
cacheManager.close();
}
}

public static Boolean isAvailable() {
return null != cacheManager && cacheManager.getStatus().equals(Status.AVAILABLE);
}
}

3 使用示例

存放

public void setCache() {
CacheManager persistentCacheManager = EhcacheHelper.getCacheManager();
Cache<Long, String> persistentCache = persistentCacheManager.getCache("threeTieredCache", Long.class, String.class);
for (long i = 0;i < 100;i ++){
persistentCache.put(i, "这是第:-" + i + "-条信息");
}

persistentCache.forEach( cc -> {
log.info("persistentCache,主键:{},值:{}",cc.getKey(),cc.getValue());
});

persistentCacheManager.close();
}

取用

public void getCache() {
CacheManager persistentCacheManager = EhcacheHelper.getCacheManager();
Cache<Long, String> persistentCache = persistentCacheManager.getCache("threeTieredCache", Long.class, String.class);

int count = 0;
Iterator<Cache.Entry<Long,String>> iterator = persistentCache.iterator();
while(iterator.hasNext()){
++ count;
Cache.Entry<Long,String> entry = iterator.next();
log.info("threeTieredCache,主键:{},值:{}",entry.getKey(),entry.getValue());
}
log.info("信息的总条数为:{}",count);
persistentCacheManager.close();
}

4 关闭钩子

public class Application {

public static void main(String[] args) {
SpringApplication.run(SkuSyncApplication.class, args);

//将 hook 线程添加到运行时环境中去
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
EhcacheHelper.close();
System.out.println("ShutdownHook ==> Ehcache closed");
}
});
}

}

Transaction

Spring 事务的本质是数据库对事务的支持。

@EnableTransactionManagement 开启对事务注解的解析

1 声明式事务

1.1 @Transactional 注解

属性 类型 描述
value String 事务管理器
propagation Propagation 传播级别
isolation Isolation 隔离级别
readOnly boolean 读/写与只读事务
timeout int 事务超时(秒)
rollbackFor Class 触发事务回滚的类,默认只对未检查异常有效
noRollbackFor Class 设置不需要进行回滚的异常类数组

1.2 Transactional 特性

  • 类上添加 @Transactional,在每个方法单开一个事务,管理方式相同。
  • @Transactional 注解只在 public 方法上起作用。
  • 默认只对未检查异常回滚
  • 只读事务只在事务启动时应用,否则即使配置也会被忽略。

1.3 传播级别

  • PROPAGATION_REQUIRED
    (默认设置)存在则加入,没有则创建
  • PROPAGATION_REQUIRES_NEW
    创建新事务,如当前事务存在,则挂起当前事务
  • PROPAGATION_SUPPORTS
    存在则加入;没有则以非事务的方式运行
  • PROPAGATION_NOT_SUPPORTED
    以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • PROPAGATION_MANDATORY
    如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • PROPAGATION_NEVER
    以非事务方式运行,如果当前存在事务,则抛出异常
  • PROPAGATION_NESTED
    存在则创建嵌套事务;没有则创建

1.4 隔离级别

  • ISOLATION_DEFAULT
    (默认设置)使用底层数据库的默认隔离级别。
  • ISOLATION_READ_UNCOMMITTED
    可读取修改还没有提交的数据
  • ISOLATION_READ_COMMITTED
    只能读取已经提交的数据。推荐。
  • ISOLATION_REPEATABLE_READ
    可重复读,每次返回相同。
  • ISOLATION_SERIALIZABLE
    逐个执行事务,性能低。

2 编程式事务

2.1 TransactionTemplate

在 doIntransaction 里处理逻辑。如果出异常了,就执行 isRollbackOnly 方法进行回滚。

@Autowired
TransactionTemplate transactionTemplate;

transactionTemplate.execute((TransactionStatus transactionStatus) -> {
try {
//...
} catch (Exception e) {
transactionStatus.isRollbackOnly();
throw e;
}
return null;
});

2.2 TransactionManager

手动 commit,异常就 rollback

TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userRepository.save(user);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
e.printStackTrace();
}

Dynamic DataSource

一、AOP 实现

AOP 实现多数据源,可读写分离

1 配置文件

dynamic-db.master.driver-class-name = com.mysql.jdbc.Driver
dynamic-db.master.jdbc-url = jdbc:mysql://somehost:3306/sometable?characterEncoding=utf8&useSSL=false
dynamic-db.master.username = xxx
dynamic-db.master.password = xxx

dynamic-db.slave.driver-class-name = com.mysql.jdbc.Driver
dynamic-db.slave.jdbc-url = jdbc:mysql://somehost:3306/sometable?characterEncoding=utf8&useSSL=false
dynamic-db.slave.username = xxx
dynamic-db.slave.password = xxx

2 ContextHolder

管理 DataSource

public class DynamicDataSourceContextHolder {
/**
* 存储当前 DataSource
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

/**
* 所有 DataSource 的 key
*/
public static List<Object> dataSourceKeys = new ArrayList<>();

/**
* 切换 DataSource
*/
public static void setDataSourceKey(String key) {
CONTEXT_HOLDER.set(key);
}

/**
* 获取当前 DataSource,默认为 master
*/
public static String getDataSourceKey() {
String key = CONTEXT_HOLDER.get();
return key == null ? "master" : key;
}

/**
* 清空当前 DataSource
*/
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}

/**
* 判断是否当前 DataSource
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
}

3 注册动态配置

继承 AbstractRoutingDataSource

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
//将当前DataSource加入应用上下文
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}

4 加载配置

@Slf4j
@Configuration
public class DataSourceConfigurer {
@Bean("master")
@Primary
@ConfigurationProperties(prefix = "dynamic-db.master")
public DataSource master() {
return DataSourceBuilder.create().build();
}
@Bean("slave")
@ConfigurationProperties(prefix = "dynamic-db.slave")
public DataSource slave() {
return DataSourceBuilder.create().build();
}

/**
* 配置动态 DataSource
*/
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();

//所有DataSource
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put("master", master());
dataSourceMap.put("slave", slave());

//设置默认DataSource
dynamicRoutingDataSource.setDefaultTargetDataSource(master());
//设置所有DataSource
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);

//将所有DataSource的key放入,以供判断
DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
return dynamicRoutingDataSource;
}

/**
* 将数据源添加到 SqlSession 工厂;获取 mybatis 配置
*/
@Bean
@ConfigurationProperties(prefix = "mybatis")
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}

/**
* 将数据源添加到事物管理器
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}

5 注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String value();
}

6 Aspect

要用 @Order(0) 注解让这个切面的优先级最高,以免被其他切面(如事务管理器)影响

@Aspect
@Component
@Order(0)
public class DynamicDataSourceAspect {
//切换
@Before("@annotation(targetDataSource))")
public void switchDataSource(JoinPoint point, TargetDataSource targetDataSource) {
if (DynamicDataSourceContextHolder.containDataSourceKey(targetDataSource.value())){
DynamicDataSourceContextHolder.setDataSourceKey(targetDataSource.value());
}
}
//还原
@After("@annotation(targetDataSource))")
public void restoreDataSource(JoinPoint point, TargetDataSource targetDataSource) {
DynamicDataSourceContextHolder.clearDataSourceKey();
}
}

7 实例

读写分离

@Repository
public interface InterMsgMapper {
@InsertProvider(type = InterMsgProvider.class, method = "insert")
int insert(InterMsg interMsg);

@TargetDataSource("slave")
@SelectProvider(type = InterMsgProvider.class, method = "findMessages")
List<InterMsg> findMessages(Long userId, Integer limit, Long lastId, Long currentTime);
}

二、普通配置

1 配置文件

dynamic-db.master.driver-class-name = com.mysql.jdbc.Driver
dynamic-db.master.jdbc-url = jdbc:mysql://somehost:3306/sometable?characterEncoding=utf8&useSSL=false
dynamic-db.master.username = xxx
dynamic-db.master.password = xxx

dynamic-db.slave.driver-class-name = com.mysql.jdbc.Driver
dynamic-db.slave.jdbc-url = jdbc:mysql://somehost:3306/sometable?characterEncoding=utf8&useSSL=false
dynamic-db.slave.username = xxx
dynamic-db.slave.password = xxx

2 DataSourceConfig

@Configuration
public class DataSourceConfig {
//开启 数据库下划线转驼峰
@Bean
@Scope("prototype") //默认单例会使多数据源失效
@ConfigurationProperties("mybatis.configuration")
public org.apache.ibatis.session.Configuration globalConfiguration(){
return new org.apache.ibatis.session.Configuration();
}

@Bean("masterDataSource")
@Primary
@ConfigurationProperties(prefix = "dynamic-db.master")
public DataSource master() {
return DataSourceBuilder.create().build();
}
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "dynamic-db.slave")
public DataSource slave() {
return DataSourceBuilder.create().build();
}

//多数据源事务管理
@Bean(name="tranMagMaster")
public PlatformTransactionManager bfTransactionManager(@Qualifier("masterDataSource")DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name="tranMagSlave")
public PlatformTransactionManager bfscrmTransactionManager(@Qualifier("slaveDataSource")DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

3 JdbcTemplatesConfig

@Configuration
public class JdbcTemplatesConfig extends DataSourceConfig {

//支持JdbcTemplate实现多数据源
@Bean(name="masterJdbcTemplate")
public JdbcTemplate masterJdbcTemplate(@Qualifier("masterDataSource") DataSource dataSource){
return new JdbcTemplate(dataSource);
}

@Bean(name="slaveJdbcTemplate")
public JdbcTemplate slaveJdbcTemplate(@Qualifier("slaveDataSource") DataSource dataSource){
return new JdbcTemplate(dataSource);
}
}

3 MybatisConfigMaster

@Configuration
@MapperScan(basePackages = "com.xxx.mapper.master",
sqlSessionTemplateRef = "masterSqlSessionTemplate"
)
public class MybatisConfigMaster extends DataSourceConfig {
@Bean
SqlSessionFactory masterSqlSessionFactory(){

SqlSessionFactory sqlSessionFactory = null;
try {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setConfiguration(globalConfiguration());
sqlSessionFactoryBean.setDataSource(masterDataSource());
sqlSessionFactory = sqlSessionFactoryBean.getObject();
} catch (Exception e) {
e.printStackTrace();
}
return sqlSessionFactory;
}

@Bean
SqlSessionTemplate masterSqlSessionTemplate(){
return new SqlSessionTemplate(masterSqlSessionFactory());
}
}

4 MybatisConfigSlave

@Configuration
@MapperScan(basePackages = "com.xxx.mapper.slave",
sqlSessionTemplateRef = "slaveSqlSessionTemplate"
)
public class MybatisConfigSlave extends DataSourceConfig {
@Bean
SqlSessionFactory slaveSqlSessionFactory(){

SqlSessionFactory sqlSessionFactory = null;
try {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setConfiguration(globalConfiguration());
sqlSessionFactoryBean.setDataSource(slaveDataSource());
sqlSessionFactory = sqlSessionFactoryBean.getObject();
} catch (Exception e) {
e.printStackTrace();
}
return sqlSessionFactory;
}

@Bean
SqlSessionTemplate slaveSqlSessionTemplate(){
return new SqlSessionTemplate(slaveSqlSessionFactory());
}
}

5 使用

在 @MapperScan 配置的目录下写 mapper 就行了