Spring

Spring MVC

配置文件

application-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<import resource="spring-dao.xml"/>
</beans>

spring-dao.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--DataSource-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://10.112.218.78:3307/study?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>
<!--SqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<!-- SqlSession -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
<!--结合AOP自动加入事务管理-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="add" propagation="REQUIRED"/>
<tx:method name="delete"/>
<tx:method name="update"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* mapper.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>

MyBatis

db.properties

1
2
3
4
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/sams?serverTimezone=UTC
jdbc.username=root
jdbc.password=root

mybatis-config.xml

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"></properties>
<settings>
<!-- 该配置影响的所有映射器中配置的缓存的全局开关。默认值true -->
<setting name="cacheEnabled" value="true"/>
<!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。默认值false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 是否允许单一语句返回多结果集(需要兼容驱动)。 默认值true -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。默认值true -->
<setting name="useColumnLabel" value="true"/>
<!-- 允许 JDBC 支持自动生成主键,需要驱动兼容。 如果设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby)。 默认值false -->
<setting name="useGeneratedKeys" value="false"/>
<!-- 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。 -->
<!-- 默认值PARTIAL -->
<setting name="autoMappingBehavior" value="PARTIAL"/>

<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<!-- 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。默认SIMPLE -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 设置超时时间,它决定驱动等待数据库响应的秒数。 -->
<setting name="defaultStatementTimeout" value="25"/>

<setting name="defaultFetchSize" value="100"/>
<!-- 允许在嵌套语句中使用分页(RowBounds)默认值False -->
<setting name="safeRowBoundsEnabled" value="false"/>
<!-- 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。 默认false -->
<setting name="mapUnderscoreToCamelCase" value="false"/>
<!-- MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。
默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。
若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。 -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 -->
<setting name="jdbcTypeForNull" value="OTHER"/>
<!-- 指定哪个对象的方法触发一次延迟加载。 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</dataSource>
</environment>
</environments>
<!--加载映射文件-->
<mappers>
<mapper resource="mapper/AdminMapper.xml"></mapper>
<mapper resource="mapper/UserMapper.xml"></mapper>
</mappers>
</configuration>

UserMapper.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.UserMapper">
<select id="selectUser" resultType="pojo.User">
select * from user
</select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
String resource = "mybatis-config.xml";
InputStream stream = Resources.getResourceAsStream(resource);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream);
SqlSession session = factory.openSession(true);

UserMapper mapper = session.getMapper(UserMapper.class);
for(User user: mapper.selectUser()){
System.out.println(user.toString());
}

session.close();

动态代理

  • JavaSist - 基于字节码
  • Cglib - 基于类
  • 基于接口
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
// 代理生成器
public class ProxyInvocationHandler implements InvocationHandler {
// 被代理的接口
private Object target;
public void setTarget(Object target) {
this.target = target;
}
// 生成一个代理类
public Object getProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
// 处理一个代理实例,并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 调用特有行为
Object result = method.invoke(target, args);
// 调用特有行为
return result;
}
// 下方加入代理类特有行为
// TODO
}

public static void main(String[] args) {
Hello hello = new HelloImpl();
ProxyInvocationHandler proxyHandler = new ProxyInvocationHandler();
// 所代理的角色
proxyHandler.setTarget(hello);
// 动态生成的代理类
Hello proxy = (Hello) proxyHandler.getProxy();
proxy.say(); // Hello 接口下的行为
}

AOP

导入包

1
aspectjweaver
1
2
3
4
5
6
7
8
9
public class Log implements MethodBeforeAdvice, AfterReturningAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName() + " - " + method.getName());
}
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
}
}

方式一:在Bean中注册Log

1
2
3
4
5
6
7
8
9
<bean id="userService" class=""/>
<bean id="log" class=""/>

<aop:config>
<!-- 切入点 execution(访问权限 返回值 类名 方法名 参数) -->
<aop:pointcut id="pointcut" expression="execution(* com.UserServiceImpl.*(..))"/>
<!-- 在切入点加入 Log -->
<aop:advicor advice-ref="log" pointcut-ref="pointcut"/>
</aop:config>

方式二:使用类

1
2
3
public class PointCut {
public void before() {;}
}
1
2
3
4
5
6
7
8
9
<bean id="pcut" class=""/>
<aop:config>
<aop:aspect ref="pcut">
<!-- 切入点 execution(访问权限 返回值 类名 方法名 参数) -->
<aop:pointcut id="point" expression="execution(* com.UserServiceImpl.*(..))"/>
<!-- 在切入点加入 Log -->
<aop:before method="before" pointcut-ref="point"/>
</aop:aspcet>
</aop:config>

方式三:注解

1
2
3
4
5
@Aspcet
public class PointCut {
@Before("execution(* com.UserServiceImpl.*(..))") // 别导错包
public void before() {;}
}
1
2
<bean id="pointcut" class=""/>
<aop:aspectj-autoproxy/>

Spring Boot

参考

后台:X-Admin

整合:

  • 数据库

    • Mysql
    • Redis
    • ElasticSearch
    • ClickHouse
  • 数据库工具

    • JDBC
    • Mybatis
    • MybatisPlus
    • JTA
    • Druid
    • Flyway
    • Jedis - Redis 客户端,非线程安全,除非使用线程池
    • Lettuce - Redis 客户端,线程安全
    • FastDFS
  • 安全

    • JWT
    • Shiro
    • Security
    • OAuth2
  • 缓存日志

    • Ehcache
  • 基础工具

    • Zip4j
    • Lombok
    • Hutool
    • Fastjson
    • Guava
    • Commons-codes
    • Commons-pool
    • Commons-collections
    • Commons-lang3
    • Excel
    • PDF
    • Xml
    • CSV
  • 分布式

    • Feign
    • Dubbo
    • Zookeeper
    • Spring Cloud
    • Nacos
    • Kafka
    • RocketMQ
  • 其他

    • Swagger2

    • JavaMail

    • QuartJob

    • Drools

    • Elastic Job

总览

Project

  • config
    • application.yaml - 一级配置文件
  • src
    • main
    • test
      • java
        • com…
          • xxxTests.java - 测试文件
  • target - 生成文件
  • application.yaml - 二级配置文件
  • pom.xml - 包管理文件

其中main文件下(二者合并为classpath):
main

  • java
    • com…
      • common
        • advice - 异常的处理
          • BaseAdvice.java - 基础异常处理接口,例如:参数类型异常,未授权异常
          • OpenApiAdvice.java - API 请求异常处理类
        • base - 数据基类,保存公共字段,实现序列化,分页等
        • config - 配置
          • SecurityConfig.java - 安全配置、访问控制
          • InterceptorConfig.java
          • RedisConfig.java
          • MybatisPlusConfig.java
          • DruidDatasourceConfig.java
        • intercepter
          • LoginIntercepter.java - 登录拦截器
        • exception
          • BusinessException.java - 业务异常
          • UnauthorizedException.java - 未授权异常
        • constant - 常量配置
          • BooleanEnum.java
          • CodeEnum.java - 定义状态码
        • service - 服务
          • CacheService.java - 缓存服务,例如:Redis缓存服务
        • utils - 工具类,例如:Zip,Http工具,IP工具等
      • controller - 负责渲染前端页面,或RESTful接口
        • StudentDaoController.java
        • IndexController.java
      • constant - 定义常量
        • UrlConstant - url 常量
        • DemoCodeEnum - 返回码常量
      • dao - 提供增删改查的接口,要被 Spring 接管,可以用Mapper代替
        • StudentDao.java
      • mapper - MyBatis 使用的查询接口,代替Dao
        • StudentMapper.java
      • pojo - 实体类,与数据表保持一致
        • vo
        • entity
          • Student.java
      • service
        • impl
          • StudentServiceImpl.java - 服务实现
        • StudentService.java - 服务接口
      • MainApplication.java - SpringBoot 启动类
  • resources
    • db - 定义数据库迁移文件
      • init
    • config
      • application.yaml
    • i18n - 国际化配置
      • index.properties
      • index_zh_CN.properties
      • index_en_US.properties
    • public
      • index.html
    • resources
    • static
    • mybatis
      • mapper
        • StudentMapper.xml
    • templates
      • common - 公共页面
        • common.html
      • error - 错误页面
        • 404.html - 404 错误页面
      • student - 管理页面,包括增删改查
        • list.html
        • add.html
        • update.html
    • application.yaml
    • banner.txt - SpringBoot 启动打印的logo

注解

1
2
3
4
5
6
7
8
9
10
11
12
13
// 被 Spring 托管,可以用 @Autowired 装载的注解

// 功能组件
@Component
@Repository
@Service
@Controller

// 类上,用于注册配置类
@Configuration

// 用于方法上,注册一个基本组件
@Bean

主配置文件

配置文件名称必须为application.yml,不同环境可以配置不同的后缀,如application-dev.ymlapplication-prod.yml等。位置可以为

  • /config
  • /
  • classpath:/config
  • classpath:/

激活一个SpringBoot环境。

1
2
3
spring:
profiles:
active: dev

此外,配置文件可以与Bean绑定,让Bean自动获取yml文件中的配置。

例如,创建一个Bean,保存被拦截的路由。

1
2
3
4
5
6
@Data
@ConfigurationProperties(prefix = "route")
public class Route {
String[] path;
String[] except;
}

配置文件中配置相关值。

1
2
3
4
5
6
route:
path:
- /users
except:
- /css
- /js

再配置类中注册这个Bean

1
2
3
4
5
6
7
8
@Configuration
@EnableConfigurationProperties(Route.class)
public class RouteConfig {
@Bean
public Route route() {
return new Route();
}
}

再在拦截器中添加该路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private Route routes;

@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns(routes.getPath())
.excludePathPatterns(routes.getExcept());
}
}

数据对象

创建一个Pojo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Lombok
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString
@RequiredArgsConstructor
@Data // 等于以上所有
@AllArgsConstructor
@NoArgsConstructor
@Builder // 静态构造器
// JSR303
@Validated
// MyBatis Plus
@TableName(value = "tb_demo_car")
public class Student implements Serializable{ // Serializable 使 Redis 能够存储该对象
@TableId(type = IdType.AUTO) // MyBatis Plus
private Integer id;
private String name;
@PositiveOrZero() // 验证机制 JSR303
private Integer age;
@Email() // 验证机制 JSR303
private String email;
}

创建传统Dao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Repository  // 被 Spring 托管,可以用 @Autowired 装载
public class StudentDao {
private static Map<Integer, Student> students = null;
static {
students = new HashMap<>();
students.put(100, new Student(100, "name-dog-1", 10, "12345@qq.com"));
students.put(101, new Student(101, "name-dog-2", 15, "54321@qq.com"));
}
public Collection<Student> getAllStudents() {
return students.values();
}
public Student getStudentById(Integer id) {
return students.get(id);
}
public boolean save(Student student) {
return true;
}
public boolean update(Student student) {
return true;
}
public boolean delete(Integer integer) {
return true;
}
}

创建Service

1
2
3
4
public interface StudentService {
String getNameById(int id);
Student getStudentByName(String name);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
StudentMapper studentMapper;
@Override
public String getNameById(int id) {
return studentMapper.getNameById(id);
}
@Override
public Student getStudentByName(String name) {
return studentMapper.getStudentByName(name);
}
}

对于JDBC Template,使用User对象

1
2
3
4
5
6
7
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
List<Long> users = jdbcTemplate.queryForList("select id from user", Long.class);
System.out.println(users.toString());
}

如果使用MyBatis,则设置Mapper,而不必使用DAO。

1
2
3
4
5
6
7
8
9
10
@Mapper
public interface StudentMapper {
List<Student> listStudents();
Student listStudentById(Integer id);
String getNameById(int id);
Student getStudentByName(String name);
int addStudent(Student student);
int updateStudent(Student student);
int deleteStudent(Integer id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.demox.mapper.StudentMapper">
<resultMap id="StudentResultMap" type="com.example.demox.pojo.Student">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
</resultMap>
<select id="listStudents" resultMap="StudentResultMap">
select id, name, age from user
</select>
<select id="getNameById" resultType="String" parameterType="_int">
select name from user where id=#{id}
</select>
<select id="getStudentByName" resultType="Student" parameterType="String">
select * from user where name=#{name}
</select>
</mapper>

也可以不用注解,而是在项目启动位置加入:

1
@MapperScan("com...mapper")

静态资源

静态资源目录

  • /resources/
  • /static/
  • /public/
  • /

Webjars 不用

  • localhost/webjars/

也可以通过配置文件修改位置。

首页位置放在静态资源文件夹下,名称为index.html

templates目录下的文件只能使用Controller调用。

模板引擎

Thymeleaf

导入包

1
spring-boot-starter-thymeleaf

在模板中加入命名空间

1
<html lang="zh-cn" xmlns:th="http://www.thymeleaf.org">

基本用法

1
2
<div th:text="${msg}"></div>
<div>[[ ${msg} ]]</div>

Controller 引用模板

1
2
3
4
5
6
7
8
@Controller
public class IndexController {
@RequestMapping("/student")
public String index(Model model) {
model.addAttribute("msg", "123");
return "home-template"; // return 模板名称
}
}

可以给公共部分传参数。

1
2
3
4
5
6
<!-- 子页面 -->
<div th:replace="~{commons/commons::sidebar(active='main.html')}"></div>
<!-- 公共页面 -->
<div th:fragment="sidebar">
<div th:class="${active=='main.html'?'active':''}"></div>
</div>

配置

1
2
3
spring:
thymeleaf:
cache: false

Freemarker

控制器

返回Json或字符串的控制器

1
2
3
4
5
6
7
@RestController
public class FirstController {
@RequestMapping("/first")
public String First() {
return "Hello";
}
}

返回模板的控制器

1
2
3
4
5
6
7
8
@Controller
public class IndexController {
@RequestMapping("/index")
public String index(Model model) {
model.addAttribute("msg", "123");
return "index";
}
}

接收表单参数的控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class IndexController {
@RequestMapping("/login")
public String login(
@RequestParam("username") String user,
@RequestParam("password") String pwd,
Model model) {
if("right".equals(pwd)){
return "redirect:/success.html";
}
return "index";
}
}

允许的传参方式

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
@PathVariable
@RequestHeader
@RequestBody
@RequestParam
@CookieValue
@ModelAttribute

@RestController
public class TestController {
@GetMapping("/car/{id}")
public Map<String, Object> getCar(
@PathVariable("id") Ingeter id,
@PathVariable Map<String, String> variables,
@RequestHeader Map<String, String> headers, // 或 MultiValueMap / HttpHeaders
@RequestParam("age") Ingeter age,
@RequestParam Map<String, String> params,
@CookieValue("_ga") String _ga,
@RequestBody) {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
return map;
}

@PostMapping("/car/{id}")
public Map<String, Object> postCar(
@PathVariable("id") Ingeter id,
@RequestBody String content) {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
return map;
}
}

控制器的增删改查

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
@Controller
public class StudentController {
@Autowired
StudentService studentService;
@RequestMapping("/student")
public String list(Model model) {
// 渲染前端
return "student/list";
}
@GetMapping("/student/add-{id}")
public String add(Model model) {
// 渲染前端
return "student/add";
}
@PostMapping("/student/add-{id}") // 上传的表单字段必须与类的属性一致
public String doAdd(Student student) {
studentDao.save(student);
return "redirect:/student";
}
@GetMapping("/student/update-{id}")
public String update(@PathVariable("id") Integer id, Model model) {
// 渲染前端
return "student/update";
}
@PostMapping("/student/update-{id}")
public String doUpdate(Student student) {
studentDao.update(student);
return "redirect:/student";
}
@PostMapping("/student/del-{id}")
public String doDelete(@PathVariable("id") Integer id) {
studentDao.delete(id);
return "redirect:/student";
}
@ResponseBody
// 不走视图解析器
}

文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class StudentController {
@PostMapping("/student/upload")
public String upload(@RequestPart("image") MultipartFile image) throws IOException {
if(!image.isEmpty()) {
String filename = image.getOriginalFilename();
// 原生方法
image.getInputStream();
// 传输到其他位置
image.transferTo(new File("/tmp/uploads/image/" + filename));
}
}
}

配置文件上传,还要配置文件大小限制

1
2
3
4
5
spring: 
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB

异常处理

默认情况下,SpringBoot会转发到/error处理错误。

1
2
3
server:
error:
path: /error

如果要自定义错误页面,则只要在public/errortemplate/error创建相应文件4xx.html5xx.html

也可以编写ControllerAdvice,传输一些参数。

1
2
3
4
5
6
7
8
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handleArithException(Exception e) {
log.error(e);
return "login"; // 视图地址
}
}

也可以自定义异常。

1
2
3
4
5
6
7
8
9
@ResponseStatus(value=HttpStatus.FORBIDDEN, reason="xxxx")
public class MyException extends RuntimeException {
public MyException(String msg) {
super(msg);
}
}

// 某处抛出异常
throw new MyException("xxx");

一些其他的异常处理,例如传参异常,不支持的媒体异常。

1
2
3
4
5
6
7
8
9
10
11
@Order(value=)
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Ojbect handler,
Exception ex)
response.sendError(400, "msg");
return new ModelAndView();
}

配置类

例如配置拦截器

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
@Configuration
public class MainMvcConfig implements WebMvcConfigurer {
// 重写WebMvcConfigurer方法,覆盖SpringMVC配置
// 路由控制
@Override
public void addViewControllers(ViewControllerRegistry registry) {
WebMvcConfigurer.super.addViewControllers(registry);
registry.addViewController("/").setViewName("/index");
registry.addViewController("/index.html").setViewName("/index");
}

// 登录拦截器
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return false;
}
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/login")
.excludePathPatterns("/index**")
.excludePathPatterns("/");
}
}

连接数据库

底层使用Spring Data连接各种数据库。首先导入数据包。

1
2
mysql-connector-java
spring-boot-starter-data-jdbc

修改配置文件

1
2
3
4
5
6
spring:
datasource: # Bean 与 Spring 绑定
username: root
password: root
url: "jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf-8mb"
driver-class-name: "com.mysql.cj.jdbc.Driver"

测试数据库连接

1
2
3
4
5
6
7
8
@Autowired
private DataSource dataSource;
@Test
void testDataSource() throws SQLException {
System.out.println(dataSource.getClass());
Connection connection = dataSource.getConnection();
connection.close();
}

使用JDBC方法的控制器

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/info")
public class InfoController {
@Autowired
private JdbcTemplate jdbcTemplate;
@RequestMapping("/jdbc")
public List<Map<String, Object>> jdbc() {
String sql = "select * from user";
return jdbcTemplate.queryForList(sql);
}
}

整合Druid

整合Druid - Alibaba数据源。导入依赖包。

1
2
druid
log4j

修改配置参数

1
2
3
4
5
6
7
spring:
datasource:
username: root
password: root
url: "jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf-8mb"
driver-class-name: "com.mysql.cj.jdbc.Driver"
type: "com.alibaba.druid.pool.DruidDataSource" # 新增

特性:使用配置类配置管理页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class DruidConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
// 添加监控页面
@Bean // 此方法用于替代 web.xml 使用
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean<StatViewServlet> bean =
new ServletRegistrationBean<StatViewServlet>(new StatViewServlet(),"/druid/*");
// 登录用户名与密码
HashMap<String, String> initParameters = new HashMap<>();
initParameters.put("loginUsername", "admin");
initParameters.put("loginPassword", "123");
initParameters.put("allow", "");
bean.setInitParameters(initParameters);
return bean;
}
}

整合MyBatis

整合Mybatis。导入包。

1
mybatis-spring-boot-starter

修改配置文件

1
2
3
4
5
6
7
8
9
10
spring:
datasource:
username: root
password: root
url: "jdbc:mysql://localhost:3306/study?useUnicode=true&character_set_server=utf-8mb4&serverTimezone=Asia/Shanghai"
driver-class-name: "com.mysql.cj.jdbc.Driver"

mybatis:
type-aliases-package: "com.example.demox.pojo"
mapper-locations: "classpath:mybatis/mapper/**/*.xml"

编写Mapper

1
2
3
4
5
6
7
8
9
@Mapper
public interface StudentMapper {
@Select("select * from user")
List<Student> listStudents();
Student listStudentById(Integer id);
int addStudent(Student student);
int updateStudent(Student student);
int deleteStudent(Integer id);
}
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demox.mapper.StudentMapper">
<resultMap id="StudentResultMap" type="com.example.demox.pojo.Student">
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
</resultMap>
<select id="listStudents" resultMap="StudentResultMap">
select id, name, age from user
</select>
<!--其他例子-->
<select id="getAccountById" parameterType="int" resultType="Account">
select * from study.account where _id = #{id}
</select>
<insert id="addAccount" parameterType="Account" useGeneratedKeys="true" keyProperty="id">
insert into study.account (username, password)
values (#{username}, #{password})
</insert>
<update id="modifyAccount" parameterType="Account">
update study.account
set username = #{username}, password = #{password}
where _id = #{id}
</update>
<delete id="deleteAccount" parameterType="int">
delete from study.account where _id = #{id}
</delete>
</mapper>

一般再创建一个Service层。此处略。

在控制器中使用。

1
2
3
4
5
6
7
@Autowired
private StudentMapper studentMapper;
@RequestMapping("/mybatis")
public List<Student> mybatis() {
List<Student> students = studentMapper.listStudents();
return students;
}

安全

Security 模块

可以访问控制和身份验证。

1
spring-boot-starter-security

几个重要的类:

1
2
3
4
5
6
// 自定义安全策略
WebSecurityConfigurerAdapter
// 自定义认证策略
AuthenticationManagerBuilder
// 开启Web安全模式
@EnableWebSecurity

添加配置类

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
@EnableWebSecurity
public class CurrentSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 授权规则
http.authorizeRequests()
.antMatchers("/")
.permitAll()
.antMatchers("/admin/**")
.hasRole("admin");
// 开启登录功能
http.formLogin();
// 但是不建议使用
http.formLogin().loginPage("/toLogin")
.usernameParameter("user")
.passwordParameter("pwd")
.loginProcessingUrl("/login");
// 开启注销功能 默认为 /logout
http.logout();
// 关闭 CSRF
http.csrf().disable();
// 开启 Remeber Me
http.rememberMe().rememberMeParameter("remember");
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 认证,可以来源数据库,也可以是内存
String encodePassword = new BCryptPasswordEncoder().encode("123");
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(encodePassword).roles("admin")
.and()
.withUser("guest").password(encodePassword).roles("guest");
}
}

可以与thymeleaf整合

1
thymeleaf-extras-springsecurity5

在模板中使用

1
2
3
4
5
<!-- 是否登陆 -->
<div sec:authorize="isAuthenticated()">
<!-- 用户名 -->
<div sec:authentication="name"></div>
</div>

也可以利用Security模块制作单点登录模块。利用Security自带的Web拦截器首先登录拦截。

需要创建OSS Server与客户端,所有登录信息由Server保管。

1
2
3
4
spring-boot-starter-web
spring-boot-starter-security
spring-security-oauth2
spring-security-jwt

配置服务的端口和路径

1
2
3
server:
port: 9090
context-path: /server

添加配置类

首先是认证授权服务器

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
@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;

@Autowired
TokenStore tokenStore;

@Autowired
BCryptPasswordEncoder encoder;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置客户端
clients
.inMemory()
.withClient("client")
.secret(encoder.encode("123456")).resourceIds("hi")
.authorizedGrantTypes("password","refresh_token")
.scopes("read");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore)
.authenticationManager(authenticationManager);
}


@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
//允许表单认证
oauthServer
.allowFormAuthenticationForClients()
.checkTokenAccess("permitAll()")
.tokenKeyAccess("permitAll()");
}
}

同时配置一个Controller用于客户端请求认证信息

1
2
3
4
5
6
7
8
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
}

客户端需要在配置类上配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@EnableOAth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${security.oauth2.sso.login-path:}")
private String loginPath;

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
if(StrngUtils.isNotEmpty(loginPath)){
http.formLogin().loginProcessingUrl(loginPath);
}
}
}

并配置SSO服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9090
security:
oauth2:
client:
client-id: client
client-secret: 123456
access-token-uri: http://localhost:9091/oauth/token
user-authorization-uri: http://localhost:9091/oauth/authorize
scope: read
use-current-uri: false
resource:
user-info-uri: http://localhost:9091/oauth/user

对于需要权限的Controller

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/user")
public class UserController {
@PreAuthorize("hasAuthority('admin')")
@GetMapping("/auth/admin")
public Object adminAuth() {
return "Has admin auth!";
}
}

Shiro 模块

支持JavaSE环境。使用方式可以参考官方quickstart

1
shiro-spring-boot-web-starter

有三大对象:用户,用户管理器,数据库连接模块

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
@Configuration
public class ShiroConfig {
public class UserRealm extends AuthorizingRealm {
@Autowired
StudentServiceImpl service;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Student currentStudent = (Student) SecurityUtils.getSubject().getPrincipal();
// 授权,应该在数据库中操作
info.addStringPermission("user:add");
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 连接数据库
Student student = service.getStudentByName(token.getUsername());
if(student == null){
return null; // 提示用户名不存在 UnknownAccountException
}
// 传值给 授权,传入密码
return new SimpleAuthenticationInfo(student, student.getPassword(), ""); // 密码自动认证
}
}
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}
@Bean(name="shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager manager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(manager);
// 为路径添加过滤器,anon,authc,user,perms,role
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/add", "perms[user:add]"); // 用户名为user有add权限才可以进入该目录
filterMap.put("/user/delete", "authc");
factoryBean.setFilterChainDefinitionMap(filterMap);
factoryBean.setLoginUrl("/login");
return factoryBean;
}
}

整合thymeleaf

1
thymeleaf-extras-shiro

配置ShiroConfig

1
2
3
4
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}

使用

1
<div shiro:hasPermission="user:add"></div>

Swagger

后端API框架,RestFul API,文档自动在线生成,可以在线测试API接口

需要SpringFox支持。

引入包

1
2
springfox-swagger2
springfox-swagger-ui

配置

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
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("分组1:基本API接口")
.enable(true) // 是否启用Swagger
.select().apis(RequestHandlerSelectors.basePackage("com.example.demox.controller"))
.build();
}
private ApiInfo apiInfo(){
return new ApiInfo(
"doc",
"api doc",
"1.0",
"https://www.baidu.com",
ApiInfo.DEFAULT_CONTACT,
"Apache 2.0",
"...",
new ArrayList<>()
);
}
}

访问/swagger-ui.html

给API加说明:

1
2
3
4
@ApiModel("Info")
@ApiModelProperty("info")
@ApiOperation("info")
@ApiParam()

配置项

国际化

配置中加入

1
2
3
spring:
messages:
basename: "i18n.index"spring: messages: basename: "i18n.index"

模板中修改:

1
<div th:text=#{index.title}></div>

日期格式化

1
2
3
spring:
mvc:
date-format: "yyyy-MM-dd"

事件机制

实现事件机制需要3个部分:事件,发布者,监听器。

首先定义一个事件,需要继承ApplicationEvent

1
2
3
4
5
6
7
8
9
10
11
12
public class DeviceOnlineEvent extends ApplicationEvent {
private final String deviceId;

public DeviceOnlineEvent(Object source, String deviceId) {
super(source);
this.deviceId = deviceId;
}

public String getDeviceId() {
return deviceId;
}
}

定义一个事件的发布者,注入到Bean中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class DeviceEventPublisher implements ApplicationContextAware {
private ApplicationContext ctx;

@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
this.ctx = applicationContext;
}

public void publishEvent(ConnectionState state, String msg) {
if (state == ConnectionState.CONNECTED) {
ctx.publishEvent(new DeviceOnlineEvent(this, msg));
} else {
ctx.publishEvent(new DeviceOfflineEvent(this,msg)) ;
}
}
}

定义一个事件的监听器。

1
2
3
4
5
@EventListener
public void onDeviceConnected(DeviceOnlineEvent event) {
String deviceId = event.getDeviceId();
deviceManager.putIfAbsent(deviceId, new DeviceItem());
}

在发布事件时,只需要注入事件发布器,发布事件即可。

1
deviceEventPublisher.publishEvent(ConnectionState.CONNECTED, deviceId);

任务

异步任务

首先开启异步功能

1
2
3
4
5
6
7
@SpringBootApplication
@EnableAsync
public class DemoFirstApplication {
public static void main(String[] args) {
SpringApplication.run(DemoFirstApplication.class, args);
}
}

定义异步服务AsyncService.java

1
2
3
4
5
6
7
@Service
public class AsyncService {
@Async
public void hello() {

}
}

邮件任务

导入包javax.mail

1
spring-boot-starter-mail

配置邮件服务

1
2
3
4
5
6
7
8
9
10
spring:
mail:
username: "123@123.com"
password: "123"
host: "smtp.qq.com"
properties:
mail:
smtp:
ssl:
enable: true

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Autowired
JavaMailSenderImpl javaMailSender;

void sendMail() {
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("subject");
message.setText("Content");
message.setFrom("...");
message.setTo("...");
javaMailSender.send(message);

MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("subject");
helper.setText("<div>content</div>", true);
helper.addAttachment("file_name", new File(""));
helper.setFrom("");
helper.setTo("");
javaMailSender.send(helper.getMimeMessage());
}

定时任务

首先开启功能

1
2
3
4
5
6
7
@SpringBootApplication
@EnableScheduling
public class DemoFirstApplication {
public static void main(String[] args) {
SpringApplication.run(DemoFirstApplication.class, args);
}
}

创建定时任务

1
2
3
4
5
6
7
8
@Service
public class ScheduleTask {
// 秒 分 时 日 月 星期 - 每到 0 秒时执行
@Scheduled(cron = "0 * * * * 0-7")
public void hello() {

}
}

其他定时任务框架:Quartz,XXL-Job等。

任务队列

Kue

整合Redis

1
spring-boot-starter-data-redis

配置

1
2
3
4
spring:
redis:
host: "127.0.0.1"
port: 6371

使用

1
2
3
4
5
6
7
8
@Autowired
private RedisTemplate redisTemplate;
void redisTest() {
redisTemplate.opsForValue().set("", "");
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushDb();
connection.flushAll();
}

配置类,自定义序列化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Json 序列化 也可以用 fastjson
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder().allowIfBaseType(Object.class).build();
mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
// String 序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

Pojo对象序列化

1
2
3
public class Student implements Serializable {
// ...
}

存入Redis

1
2
3
// Json 方式
Student student = new Student(100, "ha", 3);
redisTemplate.opsForValue().set("student", s);

实际上,应该建立RedisUtils,设置Key的过期时间

整合MyBatisPlus

整合MybatisPlus

1
mybatis-plus

IDEA可以安装插件MybaitsX

导入时与mybatis二选一,有了mybatis plus就不需要mybaits了。

使用方法

1
2
3
4
mybatis-plus:
configuration:
mapper-location: # 有默认值
log-impl: "org.apache.ibatis.logging.stdout.StdOutImpl" # 日志

实体类看可以用的注解

1
2
@TableName("")
@TableField(exist=false)

编写Mapper

1
2
3
public interface UserMapper extends BaseMapper<User> {
// 完成了
}

编写服务

1
2
3
public interface UserService extends IService<User> {}

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}

在入口处扫描Mapper

1
@MappserScan("com.example.demox.mapper")

主键生成策略:

  • uuid
  • 自增ID
  • 雪花算法:主键中包含记录的存储地域,机器号。
  • Redis 生成
  • Zookeeper 生成

默认方法:全局唯一ID

1
@TableId(type=IdType.ID_WORKER)

可以通过条件自动拼接SQL语句。

自动填充,例如修改时间,创建时间:

数据库方式:字段类型datetime,默认值设置为CURRENT_TIMESTAMP

1
2
3
4
public class User {
private Date createTime;
private Date updateTime;
}

MybatisPlus方式:字段类型datetime

1
2
3
4
5
6
public class User {
@TableField(fill=FieldFill.INSERT)
private Date createTime;
@TableField(fill=FieldFill.INSERT_UPDATE)
private Date updateTime;
}

再编写一个处理器handler.MyMetaObjectHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ....");
this.strictUpdateFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
}

@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ....");
this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
}
}

乐观锁:只有出问题时才测试加锁。实现方式,在字段中加入version字段,每次更新一个version

  1. 取出记录,得到version
  2. 更新时,带上新的version(where version=old_version)
  3. 如果条件测试失败,就更新失败
1
2
@Version  // 乐观锁
private Integer version;

注册组件config.MyBatisPlusConfig

1
2
3
4
5
6
7
8
9
10
11
@EnableTransactionManagement
@Configuration
@MapperScan("...")
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mybatisPlusInterceptor;
}
}

可以使用自旋锁解决乐观锁更新失败的时候。

悲观锁:总是加锁。

分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@MapperScan("scan.your.mapper.package")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}

@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}

使用

1
2
3
Page<User> page = new Page<>(1, 5); // Page 1, Records = 5
userMapper.selectPage(page, null);
page.getRecored();

逻辑删除:在数据库中添加deleted字段

1
2
@TableLogic
private Integer deleted

配置

1
2
3
public ISqlInjector sqlInjector() {
// ...
}
1
2
3
4
5
mybatis-plus:
global-config:
db-config:
logic-delete-value: 1
logic-not-delete-value: 0

再查询的时候就过滤掉deleted字段。

性能分析插件

首先启动开发环境

1
2
3
spring:
profiles:
active: dev

配置

1
2
3
4
@Profile({"dev", "test"})
public PerformanceInterceptor performanceInterceptor() {
.setMaxTime(); // sql 的最大执行时间
}

条件构造器:用于构建复杂SQL

1
2
3
4
5
6
7
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name").isNotNull("email").ge("age", 10);
userMapper.selectList(wrapper);
wrapper.eq("name", "john");
userMapper.selectOne(wrapper);
.notLike().likeRight() // xxx%
.inSql("id", "select id from user where age<3"); // 子查询

MyBatis Plus 代码生成器

  1. 导入mybatis plus
  2. 配置数据库
  3. 编写配置类
  4. 测试类中编写生成器
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
AutoGenerator mpg = new AutoGenerator();
// -> 配置策略
// 1 全局
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.getOutputDir(projectPath + "/src/main/java");
gc.setAuthor("author name");
gc.setOpen(false); // 是否打开资源管理器
gc.setFileOverride(false); // 是否覆盖
gc.setServiceName("%sService"); // 正则表达式 服务命名
gc.setIdType(IdType.ID_WORKER)
gc.setDateType(DateType.ONLY_DATE);
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);

// 2 数据源
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:13306/study?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSourcee(dsc);

// 3 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("blog");
pc.setParent("com.example");
pc.setEntity("entity");
pc.setMapper("mapper");
pc.setService("service");
pc.setController("controller");
mpg.setPackageInfo(pc);

// 4 策略
StrategyConfig sc = new StrategyConfig();
sc.setInclude("user"); // 设置要映射的表
sc.setNaming(NamingStrategy.underline_to_camel);
sc.setColumnNaming(NamingStrategy.underline_to_camel);
sc.setEntityLombokModel(true);
sc.setLogicDeleteFieldName("deleted"); // 逻辑删除
TableFill cre = new TableFill("gmt_create", FieldFill.INSERT);
TableFill mod = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE);
sc.setTableFillList(Arrays.asList(cre, mod)); // 自动填充时间
sc.setVersionFieldName("version"); // 乐观锁
sc.setRestControllerStyle(true); // 驼峰命名
sc.setControllerMappingHyphenStyle(true); // 方法名:下划线分割
mpg.setStrategy();

// <- 配置策略
mpg.execute();

Fluent Mybatis

底层基于Lucene,用于搜索检索,数据分析等,具有RESTful接口。

1
spring-boot-starter-data-elasticsearch

存入数据

1
2
3
4
5
6
7
8
9
PUT /index/entity/id

{
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}

其他操作

1
2
3
4
5
GET    查询
POST 条件查询
PUT 新增或修改
DELETE 删除
HEAD 检查是否存在

配置

1
2
3
4
spring:
elasticsearch:
rest:
uris: ...

使用

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
@Autowired
RestHighLevelClient restHighLevelClient;

@Test
void add() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("id", "20190909");
map.put("name", "测试");
map.put("age", 22);
// 存入
IndexRequest indexRequest = new IndexRequest("content", "doc", map.get("id").toString()).source(map);
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
System.out.println(indexResponse.toString());
// 检索
SearchRequest searchRequest = new SearchRequest().indices("content").types("doc");
// 获取
GetRequest request = new GetRequest("content", "doc", "20190909");
GetResponse getResponse = this.restHighLevelClient.get(request, RequestOptions.DEFAULT);
// 更新
UpdateRequest request = new UpdateRequest("content", "doc", map.get("id").toString()).doc(map);
UpdateResponse updateResponse = restHighLevelClient.update(request, RequestOptions.DEFAULT);
// 删除
DeleteRequest request = new DeleteRequest("content", "doc", "20190909");
DeleteResponse deleteResponse = this.restHighLevelClient.delete(request, RequestOptions.DEFAULT);

}

缓存

1
spring-boot-starter-cache

开启缓存,首先要在主类上打开开关。

1
2
3
4
5
6
@EnableCaching
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}

相关注解

@Cacheable用于方法上,能够根据传入的参数缓存结果。

1
2
3
// key 表示缓存关键字,为空表示根据所有关键字作为缓存的定义
@Cacheable(cacheNames="user", key="#id",, condition="#id > 0")
public User selectUserById(Integer id) {}

@CachePut强行更新缓存,类和方法都可以用

1
2
@CachePut("user")
public User update(Integer id) {}

@CacheEvict清除缓存,可以用在类和方法上。

1
2
@CacheEvict("user")
public User delete(Integer id) {}

@Caching相对于以上三个注解。

1
2
3
4
5
6
7
8
9
@Caching(
cacheable = @Cacheable("user"),
evict = {
@CacheEvict("cache2"),
@CacheEvict(value = "cache3", allEntries = true)
})
public User find(Integer id) {
return null;
}

@CacheConfig用在类上,用作公共的缓存配置。

指标监控

1
spring-boot-starter-actuator

默认在/actuator路径下。

1
2
3
4
HTTP默认开启Endpoint
/actuator/info
/actuator/health
JMX默认开启所有Endpoint

配置Endpoint

1
2
3
4
5
6
7
8
9
10
11
management:
endpoints:
enable-by-default: true # 默认开启所有监控端点
web:
exposure:
include: '*' # web 开启所有监控端点
endpoint: # 配置某个端点
health:
show-details: always
enable: true # 如果关闭所有,则手动开启一个

自定义Health端点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class MytHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if(true) {
builder.up();
builder.status(Status.UP);
}else {
builder.down();
}
Map<String, Object> map = new HashMap<>();
map.put("key", 0);
builder.withDetails(map).withDetail("code", 100);
return builder;
}
}

配置应用信息

1
2
3
4
5
info:
appName: name
appVersion: 1.0.0
mavenProjectName: @project.artifactId@
mavenProjectVersion: @project.version@

自定义其他端点

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Endpoint(id="container")
pubilc class DockerEndpoint() {
@ReadOperation
public Map getDockerInfo() {
return null; // ...
}
@WriteOperation
public void restartDocker() {
// ...
}
}

消息队列

RabbitMQ

基本使用

1
spring-boot-starter-amqp

消息队列内部包含两个部分,交换机和消息队列。交换机负责将消息路由到某一个消息队列,消息队列负责存储消息。

消息队列的四种工作模式:

  • 点对点 - Direct Exchange - 交换机将消息交给目标队列
  • 广播 - Fanout Exchange - 交换机广播消息到所有队列上
  • 通配符交换 - Topic Exchange - 交换机通过通配符将消息发送给某些队列,通配符有*#
  • 头部交换 - Headers Exchange - 根据头部转发消息

此外,消息队列还支持负载均衡和事务机制。

配置消息队列,最好是能创建一个公共项目,为所有项目提供统一的队列配置。

配置文件如下:

1
2
3
4
5
6
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest

对于生产者,创建配置类

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
@Configuration
public class DirectRabbitConfig {
@Bean
public Queue rabbitmqDemoDirectQueue() {
/**
* 1、name: 队列名称
* 2、durable: 是否持久化
* 3、exclusive: 是否独享、排外的。如果设置为true,定义为排他队列。则只有创建者可以使用此队列。也就是private私有的。
* 4、autoDelete: 是否自动删除。也就是临时队列。当最后一个消费者断开连接后,会自动删除。
* */
return new Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC, true, false, false);
}

@Bean
public DirectExchange rabbitmqDemoDirectExchange() {
//Direct交换机
return new DirectExchange(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, true, false);
}

@Bean
public Binding bindDirect() {
//链式写法,绑定交换机和队列,并设置匹配键
return BindingBuilder
//绑定队列
.bind(rabbitmqDemoDirectQueue())
//到交换机
.to(rabbitmqDemoDirectExchange())
//并设置匹配键
.with(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING);
}
}

之后创建生产者

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
// 创建服务,用于发送消息
@Service
public class RabbitMQServiceImpl implements RabbitMQService {
//日期格式化
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Resource
private RabbitTemplate rabbitTemplate;

@Override
public String sendMsg(String msg) throws Exception {
try {
String msgId = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
String sendTime = sdf.format(new Date());
Map<String, Object> map = new HashMap<>();
map.put("msgId", msgId);
map.put("sendTime", sendTime);
map.put("msg", msg);
rabbitTemplate.convertAndSend(RabbitMQConfig.RABBITMQ_DEMO_DIRECT_EXCHANGE, RabbitMQConfig.RABBITMQ_DEMO_DIRECT_ROUTING, map);
return "ok";
} catch (Exception e) {
e.printStackTrace();
return "error";
}
}
}

// 创建Controller,调用服务发送消息
@RestController
@RequestMapping("/mall/rabbitmq")
public class RabbitMQController {
@Resource
private RabbitMQService rabbitMQService;
/**
* 发送消息
* @author java技术爱好者
*/
@PostMapping("/sendMsg")
public String sendMsg(@RequestParam(name = "msg") String msg) throws Exception {
return rabbitMQService.sendMsg(msg);
}
}

对于消费者,使用@RabbitListener监听一个或多个消息队列。当消息队列中没有消息时,消费者可能会报错。

1
2
3
4
5
6
7
8
9
@Component
//使用queuesToDeclare属性,如果不存在则会创建队列
@RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC))
public class RabbitDemoConsumer {
@RabbitHandler
public void process(Map<String, Object> map) {
System.out.println("队列B收到消息:" + map.toString());
}
}

进阶使用

在需要动态增删消息队列的情况下,需要用RabbitAdminAmqpAdmin管理消息队列和交换机,将它们注入到Bean中。

对于directRabbitTemplate,可以设置接收消息的功能。默认情况下,Spring已经注册了一个用于接收返回消息的消息队列,当然也可以自己注册一个替换默认的接收消息的消息队列。这里使用了replyQueue。

在调用directRabbitTemplate等待消息的这一过程,可以设置为同步调用和异步调用两种方式。异步则是套一层Future。

对于Listener,一般是用Container管理一个线程池,用于接收并处理消息。这里定义了如下几种

  • directRabbitTemplate的replyQueue使用的replyListenerContainer
  • pointListenerContainer
  • broadcastListenerContainer

这些Container可以动态绑定Listener,可以动态设置监听的消息队列。

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
79
80
81
82
83
84
85
@EnableRabbit
@Configuration
public class AmqpConfig {
@Autowired
CachingConnectionFactory connectionFactory;

@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}

@Bean
public Queue replyQueue() {
return new Queue("reply");
}

@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("exchange.fanout");
}

@Bean
public DirectExchange directExchange() {
return new DirectExchange("exchange.direct");
}

@Bean
public RabbitTemplate fanoutRabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setExchange(fanoutExchange().getName());
return rabbitTemplate;
}

@Bean
public RabbitTemplate directRabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setReplyAddress(replyQueue().getName());
rabbitTemplate.setReplyTimeout(3 * 1000);
rabbitTemplate.setUseDirectReplyToContainer(false);
return rabbitTemplate;
}

@Bean
public AsyncRabbitTemplate directAsyncRabbitTemplate() {
return new AsyncRabbitTemplate(directRabbitTemplate());
}

@Bean
public SimpleMessageListenerContainer replyListenerContainer(){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueues(replyQueue());
container.setMessageListener(directRabbitTemplate());
return container;
}

@Bean
public ThreadPoolTaskExecutor rabbitListenerTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(10);
executor.setCorePoolSize(2);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("RabbitListenerExecutor-");
return executor;
}

@Bean
public SimpleMessageListenerContainer pointListenerContainer(
SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) {
SimpleMessageListenerContainer container = rabbitListenerContainerFactory.createListenerContainer();
container.setConcurrentConsumers(1);
container.setMaxConcurrentConsumers(10);
container.setTaskExecutor(rabbitListenerTaskExecutor());
container.setMessageListener(new MessageListenerAdapter(new MessageHandler()));
return container;
}

@Bean
public SimpleMessageListenerContainer broadcastListenerContainer(
SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) {
SimpleMessageListenerContainer container = rabbitListenerContainerFactory.createListenerContainer();
container.setMessageListener(new MessageListenerAdapter(new BroadcastHandler()));
return container;
}
}

消息Listener的执行器定义为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class BroadcastHandler {
public void handleMessage(String message, Object obj){
System.out.println(message);
}
}

@Component
public class MessageHandler {
public String handleMessage(String message) {
System.out.println(message);
return "r " + message;
}
}

原生写法

参考:RabbitMQ六种队列模式-发布订阅模式 - niceyoo - 博客园 (cnblogs.com)

RabbitMQ有三种交换机:Direct(点对点),Fanout(广播),Topic(发布订阅)。

默认情况下是点对点的通信方式。下面实现一种点对点的方式。

定义接收方的接口。

1
2
3
public interface ReceiverHandler {
void receive(String message);
}

然后定义RabbitMQ的收发方法。

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
public class RabbitManager {
// 这里注入了ConnectionFactory,使用RabbitMQ的默认配置,也可以手动设置配置。
private final ConnectionFactory connectionFactory = new ConnectionFactory();

public void send(String queue, String message) throws IOException, TimeoutException {
Connection connection = connectionFactory.newConnection();
// 通过通道发送消息
Channel channel = connection.createChannel();
// 声明接收者的队列
channel.queueDeclare(queue, false, false, false, null);
channel.basicQos(1);
// 参数 1 为空,使用默认交换机
channel.basicPublish("", queue, null, message.getBytes(StandardCharsets.UTF_8));
channel.close();
connection.close();
}

public void registerReceiver(String queue, ReceiverHandler messageHandler) throws IOException, TimeoutException {
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(queue, false, false, false, null);
channel.basicQos(1);
// 定义消息的消费者
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@SneakyThrows
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msgString = new String(body, StandardCharsets.UTF_8);
// 调用接收消息的接口处理消息
messageHandler.receive(msgString);
// 向生产者响应消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume(queue, false, defaultConsumer);
}
}

如果使用广播模式,则有

1
2
3
public interface SubscribeHandler {
void subscribe(String message);
}
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
public class RabbitManager {
private static final String fanoutExchange = "exchange.basic.fanout";

private final ConnectionFactory connectionFactory = new ConnectionFactory();

public void publish(String message) throws IOException, TimeoutException, InterruptedException {
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(topicExchange, "fanout");
channel.basicPublish(topicExchange, "", null, message.getBytes(StandardCharsets.UTF_8));
channel.close();
connection.close();
}

public void registerSubscribe(String queue, SubscribeHandler messageHandler) throws IOException, TimeoutException {
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(queue, false, false, false, null);
channel.basicQos(1);
channel.queueBind(queue, fanoutExchange, "");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msgString = new String(body, StandardCharsets.UTF_8);
messageHandler.subscribe(msgString);
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume(queue, false, defaultConsumer);
}
}

ActiveMQ

基于 JMS 标准接口定义。

RocketMQ

由阿里开发的消息队列。

Kafka

大数据平台下的消息队列。

WebSocket

Java自带原生的WebSocket,但是底层是BIO的。要想使用基于Netty这种NIO的WebSocket,需要引入第三方库。

1
netty-websocket-spring-boot-starter

该库实现了类似于原生WebSocket响应式开发的开发方法,但是没有Ping-Pong机制。

WebSocket的生命周期:

  • Http1.1 -> WebSocket 协议升级 - 该部分由框架自动完成。

  • Before Handshake - 此时需要验证连接是否合法,包括校验连接的ID,Token等。

  • OnOpen - 当连接建立后,需要将连接注册到连接管理器中,可以利用事件机制通知其他模块,告知它们有新连接加入。

  • OnMessage - 当收到字符串消息后触发,需要对session、消息格式进行检查,以便防止非法连接和异常消息。

  • OnBinary - 当收到二进制包时触发,WebSocket帧头部会有消息类型字段,用于告知接接收者数据包是字符串包还是二进制包。

  • OnPong - 当收到Pong触发,此时应当告知连接管理器,该连接是健康的。

  • OnError - 当出现错误时触发。一般是指Channel级别的Error。

  • OnEvent - 当有读、写、读写空闲时触发。可以开启空闲监听,当达到一定时长的空闲时,就会触发事件。触发事件后,服务器应当主动Ping或发消息到客户端,以检查连接是否健康。

  • OnClose - 当关闭连接时触发,此时应当利用事件机制告知其他模块,连接断开。

  • Send - 发送字符串消息。

  • SendBinary - 发送二进制消息。

  • SendPing - 发送Ping消息。

WebSocket的同步与异步发收:

正常流程:利用CompletableFuture、消息ID机制,处理发送和接收消息。在消息发送之前,先将消息ID-Future存入待处理消息的队列中,定义好Future用于等待消息,并将Future返回给调用者。调用者拿到Future后可以选择等待IO,也可以利用回调处理返回的消息。消息发出后,客户端处理消息,并将消息ID原封不动的返回。服务端拿到消息ID后,到队列中找到相应的Future,为Future填充返回值,结束IO等待或调用回调。

连接断开:在连接断开后,需要找到与该连接有关的消息ID,将这些消息的Future全部填入Exception,指明连接断开导致消息提前返回。

消息超时:如果消息发出后,客户端不愿意响应,则服务端需要启动一个定时任务,主动处理那些过期的Future,填入Exception,指明消息发送超时。这里的Future需要能够获取创建事件。

分布式开发

RPC

两个核心:通信,序列化。

Dubbo

一个 RPC 模块,使用方法:

  1. 定义服务接口
  2. 在服务提供方实现接口
  3. 配置文件中注册服务
  4. 使用Zookeeper作为注册中心

编辑服务端

导入依赖

1
2
dubbo-spring-boot-starter
zkclient

配置

1
2
3
4
5
6
7
dubbo:
applicaion:
name: "service-name-provider"
registry:
address: "zookeeper://localhost:2181"
scan:
base-packages: "com.example.demox"
1
2
3
4
5
6
7
8
9
10
11
12
public interface TicketService {
public String getTicket();
}

@Service // Dubbo 中的,表示被扫描
@Component // Dubbo 中尽量不用 Service
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket() {
return "ticket";
}
}

编辑客户端

配置

1
2
3
4
5
dubbo:
applicaion:
name: "service-name-consumer"
registry:
address: "zookeeper://localhost:2181"

使用者调用方法一,在与服务端相同位置(相同包)建立接口文件。

1
2
3
4
5
@Reference
TicketService service;
public void buyTicket() {
service.getTicket();
}

方法二成常用,使用Pom坐标。

Zookeeper

存储键值对,类似Redis。

Spring Cloud

自动装配

  1. 激活:
1
@EnableAutoConfiguration
  1. 配置/META-INF/spring.factories
  2. 实现XXXAutoConfiguration

Web 容器

Web Servlet:

  • Tomcat
  • Jetty
  • Undertow

Web Reactive:

  • Netty

在Spring Boot中使用Servlet:

  • Servlet注解
  • Spring Bean:将Servlet部署为Bean
  • RegistrationBean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Servlet注解方法
package com.withz.empty.web.servlet;

@WebServlet(urlPatterns = "/one/servlet")
public class OneServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("one: ");
}
}

// 主类入口
@ServletComponentScan(basePackages = "com.withz.empty.web.servlet")


// Spring Bean方式


// RegistrationBean

异步Servlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebServlet(urlPatterns = "/one/servlet", asyncSupported = true)
public class OneServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
AsyncContext asyncContext = req.startAsync();
asyncContext.start(()->{
try {
resp.getWriter().println("two: ");
} catch (IOException e) {
e.printStackTrace();
}
asyncContext.complete();
});
}
}

非阻塞Servlet:

切换容器:

  • Tomcat->Jetty:使用Maven包含Jetty,排除Tomcat
  • Servlet->WebFlux:使用Maven包含webflux,去掉SpringBootWeb

自定义容器:

  • Servlet:实现WebServerFactoryCustomizer接口
  • Reactive:ReactiveWebServerFactoryCustomizer接口

Web MVC

模板引擎,内容协商,异常处理。

  • ViewResolver
  • View

内容协商:多个渲染器之间。

异常处理:负责视图错误的处理,例如404等。

REST相关:资源服务,跨域,服务发现等。

资源跨域:

  • CrossOrigin
  • WebMvcConfigurer#addCorsMappings -> Spring Framework
  • 传统解决方案:IFrame,JSONP

服务发现:

  • HATEOS

Web Flux

Reactor基础:Java Lambda / Mono / Flux

核心:MVC注解,函数式声明,异步非阻塞

函数式声明:RouterFunction

JPA

Hibernate

Java 持久化:

  • 实体映射关系
  • 实体操作
  • 自动装配

配置

  • 外部配置:ConfigurationProperty
  • @Profile
  • @Conditional
  • 配置属性:PropertySources

Spring Cloud

版本:

  • D版,Spring Boot 1.5
  • H版,Spring Boot 2.2 2.3
  • I版,Spring Boot 2.4 2.5
  • Alibaba版本

组件:

  • 服务注册中心:可以将多个微服务注册到此处,调用方可以从这里获取可以调用的服务。

    • Eureka - 挂了
    • Zookeeper
    • Consul
    • Nacos - 重点,阿里
  • 服务调用:是调用方使用的客户端,具有多种调用策略,如按比重,轮询等。

    • Ribbon - 即将弃用
    • LoadBalancer - 刚刚开始
    • Feign - 挂了
    • OpenFeign
  • 服务降级:是服务的监控者,负责在服务宕机时代替服务返回一个可预期的结果,而不是让调用方无限等待。

    • Hystrix - 即将弃用

    • Resilience4j

    • Sentinel - 推荐,阿里

  • 服务网关

    • Zuul - 原生
    • Zuul2 - 没出来
    • Gateway - 重点
  • 服务配置

    • Config - 不再使用
    • Nacos
  • 服务总线

    • Bus - 原生,不用
    • Nacos

准备过程

新建一个父工程,包含多个子工程。

父工程

使用模板

1
maven-archetype-site

选好Maven版本。

UTF8编码。

编辑POM文件。

开启Annotation处理。

修改Java 版本。

统一管理Jar包版本。

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
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12-an1</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.18.20</lombok.version>
<mysql.version>5.1.49</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>2.2.0</mybatis.spring.boot.version>
</properties>

dependencyManagement 不负责引入

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.4.6</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR5</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.externalAnnotations.junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

子工程

需要同样再引入一遍依赖

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

无Spring Cloud 调用远程服务

使用HTTP服务 RestTemplate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 配置类

@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

// 使用

@RestController
public class IndexController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/")
public String get() {
return restTemplate.getForObject("https://www.baidu.com", String.class);
}
}

多个子模块之间要使用相同的实体类。因此可以通过创建公共模块的方式来做。该公共模块也可以添加一些公用工具。

  • 创建Common包

  • Maven : Clean - Build - Install

  • POM包中引入Common包依赖

服务注册中心

组件名 语言 CAP 健康检查 对外接口 Spring Cloud 集成
Eureka Java AP 支持,可配置 HTTP
Consul Go CP 支持 HTTP/DNS
Zookeeper Java CP 支持 客户端
Nacos Java AP / CP 支持 HTTP/DNS/UDP

注:

  • C - 一致性
  • A - 可用性
  • P - 分区容错性

Nacos:支持AP与CP的切换。AP模式下为了可用性削弱了一致性,仅支持临时实例。CP模式下是服务级别的编辑或存储配置信息,注册持久化实例,一般是集群模式,如K8S,DNS等。

Eureka

服务注册中心,将服务消费者与服务提供者联系起来。Eureka已经停止更新。

Server 端

  • 新建一个子模块
  • 引入Server模块
1
spring-cloud-starter-netflix-eureka-server
  • 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 7001

eureka:
instance:
hostname: localhost
client:
# 不向注册中心注册自己
register-with-eureka: false
# 不需要检索服务,因为自己是注册中心
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  • 写好主启动类
  • 主启动类启动
1
@EnableEurekaServer

Client 端 - 服务提供者

可以搭建为集群,以保证服务的可靠性。集群时,服务的配置没有区别。

  • 引入包
1
spring-cloud-starter-netflix-eureka-client
  • 配置
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
server:
port: 8001

spring:
application:
# 服务提供者 名称
name: provider.client
datasource:
type: ...
driver-class-name: ...
url: ...
username: ...
password: ...

mybatis:
mapperLocations: classpath:mapper/**/*.xml
type-aliases-package: coom.example.demo.entites

eureka:
client:
# 将自己注册到 Server 端
register-with-eureka: true
# 从 Server 端抓取已有的注册信息
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
instance:
# 避免使用主机名
instance-id: provider-8001
# 显示 IP
prefer-ip-address: true
  • 提供服务的Controller
1
2
3
4
5
6
7
8
9
// 使用
@RestController
public class IndexController {
// 提供的服务
@GetMapping("/")
public String get() {
return "content";
}
}
  • 启动类配置
1
@EnableEurekaClient 

Client 端 - 服务消费者

步骤基本同服务提供者。调用的底层采用HttpClient。

  • 引入包
1
spring-cloud-starter-netflix-eureka-client
  • 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8001

spring:
application:
# 服务消费者 名称
name: consumer.client

eureka:
client:
# 可以不必将自己注册到 Server 端
register-with-eureka: false
# 从 Server 端抓取已有的注册信息
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
  • 启动类配置
1
@EnableEurekaClient

服务地址改为服务名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 配置类

@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 默认是轮询
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

// 使用

@RestController
public class IndexController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/")
public String get() {
return restTemplate.getForObject("http://provider.client", String.class);
}
}

Server 集群

服务器之间相互注册,都保存了其他服务器的实例。

  • 新建多个Server模块,配置端口号为7001,7002,7003。

  • 配置,例如7001

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7001

eureka:
instance:
hostname: a.server
client:
# 不向注册中心注册自己
register-with-eureka: false
# 不需要检索服务,因为自己是注册中心
fetch-registry: false
service-url:
# 注册到其他服务中
defaultZone: http://b.server:7002/eureka/, http://c.server:7003/eureka/

此时Client端只需修改

1
defaultZone: http://a.server:7001/eureka/, http://b.server:7002/eureka/, http://c.server:7003/eureka/

服务发现

  • 提供服务的Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用

@RestController
public class IndexController {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/services")
public Object services() {
List<String> services = discoveryClient.getServices();
// 或
discoveryClient.getInstances("provider.client");
return this.discoveryClient;
}
}
  • 启动类配置
1
2
@EnableEurekaClient  // 以后可以省了
@EnableDiscoveryClient // 一定会用

自我保护机制

一定时间内没有收到心跳包,也不会注销微服务。

如果关闭,则配置 Server 端

1
2
3
4
eureka:
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 2000

Client 端 - 服务提供者

1
2
3
4
5
6
eureka:
instance:
# 发送心跳的间隔,默认是30秒
lease-renewal-interval-in-seconds: 1
# 最后一次心态等待时间上限,超时剔除服务,默认90秒
lease-expiration-duration-in-seconds: 2

Zookeeper

Server 端

需要自行搭建。

命令行客户端

1
2
# 查看服务节点的服务
ls /services/provider.client

Client 端 - 服务提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring-cloud-starter-zookeeper-discovery
配置版本与服务端相同
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>...</version>
</dependency>

配置

1
2
3
4
5
6
7
8
9
server:
port: 8004

spring:
application:
name: provider.client
cloud:
zookeeper:
connect-string: zookeeper_ip:port

启动类设置

1
@EnableDiscoveryClient

编写Controller

1
2
3
4
5
6
7
8
@RestController
public class IndexController {
// 提供的服务
@GetMapping("/")
public String get() {
return "content";
}
}

服务节点有临时节点和持久节点。如果服务挂掉,就会清除服务,之后再重新注册。

Clien 端 - 服务消费者

基本过程和服务提供者相同。

1
2
3
4
5
6
7
8
9
server:
port: 8004

spring:
application:
name: consumer.client
cloud:
zookeeper:
connect-string: zookeeper_ip:port

配置启动类

1
@EnableDiscoveryClient

配置业务类,访问服务接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 配置类

@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 默认是轮询
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

// 使用

@RestController
public class IndexController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/")
public String get() {
return restTemplate.getForObject("http://provider.client/", String.class);
}
}

Consul

有可视化工具。

Server 端

启动

1
2
consul.exe
# 默认在 http://lcoalhost:8500

Client 端 - 服务提供者

1
spring-cloud-starter-consul-discovery

配置

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8006

spring:
application:
name: provider.client
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}

启动类

1
@EnableDiscoveryClient

提供服务

1
2
3
4
5
6
7
8
@RestController
public class IndexController {
// 提供的服务
@GetMapping("/")
public String get() {
return "content";
}
}

Client 端 - 服务消费者

配置

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8006

spring:
application:
name: consumer.client
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}

启动类

1
@EnableDiscoveryClient

获取服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 配置类

@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 默认是轮询
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

// 使用

@RestController
public class IndexController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/")
public String get() {
return restTemplate.getForObject("http://provider.client/", String.class);
}
}

服务调用

Ribbon

主要是客户端用来负载均衡,服务调用的。

现在已经进入维护模式了。

与Nginx区别:

  • Nginx 是服务端的,集中式的负载均衡
  • Ribbon 是本地的,进程内的负载均衡

可以和多个注册中心结合使用。例如和Eureka。

默认情况下,Eureka自带Ribbon,因此此时不比引入。

也是基于restTemplate使用

1
2
3
4
5
6
// 基本就是 Json
restTemplate.getForObject
// 还包含响应头,响应体等
restTemplate.getForEntity
// 因此还可以获取请求体
getForEntity().getBody()

选择策略IRule,自带7种,可以扩展:

  • RoundRobinRule - 轮询
  • RandomRule - 随机
  • RetryRule - 轮询,访问速度块的权重越大
  • WeightedResponseTimeRule - 先轮询,如果访问失败则重试
  • BestAvailableRule - 先过滤故障节点,再选择并发量最小的服务
  • AvailabilityFilteringRule - 过滤故障实例,再选择并发小的实例
  • ZoneAvoidanceRule - 复合判断Server的性能和可用性

为了不让配置类在整个应用下其效果,应该将配置类放在应用外面。

1
2
3
4
5
6
7
@Configuration
public class MySelfRule{
@Bean
public IRule myRule(){
return new RandomRule();
}
}

主启动类添加

1
@RibbonClient(name="被访问的服务", cinfiguration=MySelfRule.class)

OpenFeign

1
spring-cloud-starter-openfeign

主启动类启动

1
@EnableFeignClients

创建服务

1
2
3
4
5
6
@Component
@FeignClient(value="服务名")
public interface FeignService {
@GetMapping(value="/url")
public int create();
}

创建调用者

1
2
3
4
5
6
7
8
9
@RestController
public class IndexController {
@Autowired
private FeignService feignService;
@GetMapping("/")
public String index() {
return feignService.create();
}
}

配置中超时时间

1
2
3
4
# 默认等待服务提供者 1 秒
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000

配置日志

  • NONE
  • BASIC
  • HEADERS
  • FULL
1
2
3
4
5
6
7
@Configuration
public class FeignConfig {
@Bean
Logger.Level feginLoggerLevel() {
return Logger.Level.FULL
}
}
1
2
3
logging:
level:
com.example.springcloud.service.FeignService: debug

服务降级

当服务单元故障时,断路器给服务调用方返回一个符合预期的备选响应(Fallback),而不是无限等待。

  • 服务降级:Fallback - 提供兜底方案。
  • 服务熔断:Break - 直接拒绝访问,再调用服务降级。当降级次数过多,就触发熔断,也就是直接调用fallback。等到服务正常后再恢复调用链。
  • 服务限流:Flowlimit - 限制高并发,要排队

Hystrix

1
spring-cloud-starter-netflix-hystrix

可以用在服务侧,也可以用在消费侧。一般都用在消费侧。可以使用Jmeter压测。

构建一个带熔断的服务方

1
2
3
4
5
6
7
8
9
10
11
12
public class MainService {
@HystrixCommand(fallbackMethod="createHandler", commandProperties={
// 新增的触发熔断的条件。(新增一种超时异常)
@HystrixProperty(name="exception.isolation.thread.timeoutInMilliseconds", value="3000")
})
public String create() {
return "";
}
public String createHandler() {
return "500";
}
}

主启动类

1
@EnableCircuitBreaker

或构建一个带熔断的消费者

1
2
3
feign:
hystrix:
enabled: true

编辑服务调用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IndexController {
// 热部署不会更新注解内容,需要重启服务
@HystrixCommand(fallbackMethod="indexHandler", commandProperties={
// 新增的触发熔断的条件。(新增一种超时异常)
@HystrixProperty(name="exception.isolation.thread.timeoutInMilliseconds", value="3000")
})
@GetMapping("/")
public String index() {
return mainService.create();
}

public String indexHandler() {
return "500 error";
}
}

主启动类

1
@EnableHystrix

也可以配置全局Fallback,防止和业务代码混在一起。如果不指明,则用默认的兜底方法,否则用特指的兜底方法。

1
2
3
4
5
6
@DefaultProperties(defaultFallback="methodName")
public class IndexController {
@HystrixCommand
@GetMapping("/")
public String index() {}
}

1
2
3
4
5
6
7
8
9
10
11
12
@FeignClient(value="", fallback=MainFallbackService.class)
public interface MainService{
// ...
}

@Component
public class MainFallbackService implements MainService {
@OVerride
public String create() {
return "this is fallback";
}
}

使用断路器

服务提供者

1
2
3
4
5
6
7
8
9
10
11
// Service 层
@HystrixCommand(fallbackMethod="", commandProperties={
// 是否开启断路器
@HystrixProperty(name="circuitBreaker.enabled", value="true"),
// 请求次数
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"),
// 尝试恢复周期
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="10000"),
// 失败率阈值
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="60")
})

服务网关

基于过滤链过滤请求。

Zuul 基于阻塞IO的。

Gateway 基于非阻塞异步IO的。支持Reactor,WebFlux。

Gateway

1
spring-cloud-starter-gateway

但是要移除

1
2
spring-boot-starter-web
spring-boot-starter-actuator

三大核心

  • 路由 - 匹配路由
  • 断言 - 判断条件是否满足
  • 过滤器 - 请求前后修改请求

配置一个网关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 9527

spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: service1_route # 路由ID
uri: http://localhost:8001
predicates:
- Path=/create/** # 匹配 http://localhost:8001/create/**
filters:
- RewritePath=/api/(?<segment>.*), /create/$\{segment} # 路径/api重写为/create
- id: service2_route
uri: http://localhost:8002
predicates:
- Path=/create/**

# 要注册到 注册中心

主启动类

1
@EnableEurekaClient

业务类不需要编写。

也可以硬编码配置。

1
2
3
4
5
6
7
8
9
10
11
// GatewayConfig.java

@Configuration
public class GateWayConfig {
@Bean
public RouteLocator cunstomRouteLocator(RouteLocatorBuilder builder) {
RouteLocatorBuilder.Burilder routes = builder.routes();
routes.route("path_route_id", r -> r.path("/path").uri("/target")).build();
return routes.build();
}
}

配置动态路由,记得引入nacos依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 9527

spring:
application:
name: gateway
cloud:
gateway:
# 从注册中心动态获取路由
discovery:
locator:
enabled: true
routes:
- id: service1_route # 路由ID
uri: lb://provider-1.client
predicates:
- Path=/create/** # 微服务上的路由
- id: service2_route
uri: lb://provider-2.client
predicates:
- Path=/create/**

predicates 可以接受的参数

  • Path - 微服务上的路由
  • Before
  • Between
  • After - 此时间之后匹配( ZonedDateTime.now() )
  • Cookie - 匹配Cookie
  • Header - 匹配头,使用正则表达式
  • Host
  • Method
  • Query

配置过滤器

生命周期

  • Pre - 之前
  • Post - 之后

种类

  • GatewayFilter 30+ 种
  • GlobalFilter 10+ 种
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server:
port: 9527

spring:
application:
name: gateway
cloud:
gateway:
# 从注册中心动态获取路由
discovery:
locator:
enabled: true
routes:
- id: service1_route # 路由ID
uri: lb://provider-1.client
predicates:
- Path=/create/** # 微服务上的路由
filters:
# 一种 GatewayFilter
- AddRequestHeader=X-Request-red, blue
- id: service2_route
uri: lb://provider-2.client
predicates:
- Path=/create/**

自定义全局过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
@Slf4j
public class LogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("...");
exchange.getRequest().getQueryParams().getFirst("username");
if(false) {
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

// 加载过滤器的顺序,越小优先级越高
@Override
public int getOrder() {
return 0;
}
}

服务配置

  • 能够管理所有微服务的配置。
  • 能够管理不同环境下的配置。(开发环境,生产环境,预发布环境)
  • 运行期间动态调整配置

Config

在Git上新建仓库,得到仓库地址。

编辑配置文件

  • config-dev.yml
  • config-prod.yml
  • config-test.yml
  • README.MD

安装

1
spring-cloud-config-server

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 3344

spring:
application:
name: config.center
cloud:
config:
server:
git:
uri: ...
search-paths:
- springcloud-config # 仓库
label: master # 分支

# 注册到服务注册中心

主启动类

1
@EnableConfigServer

配置文件使用规则

  • /label/application-profile.yml
    • http://config.server:3344/master/config-dev.yml
  • /application-profile.yml - 默认是 master 分支
    • http://config.server:3344/config-dev.yml
  • /application/profile/label.yml - 得到 Json
    • http://config.server:3344/config/dev/master

其他项目获取配置文件

1
spring-cloud-starter-config

配置bootstrap.yml(系统级配置,比application.yml优先级高)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 3355

spring:
application:
name: config.client
cloud:
config:
label: master
name: config
profile: dev
uri: http://localhost:3344

# 注册到服务注册中心

之后将从Config Server端拿到config-dev.yml配置。

让客户端可以动态刷新配置。

引入模块

1
spring-boot-starter-actuator

暴露Actuator

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: "*"

写一个业务类

1
2
3
4
5
6
7
8
9
10
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String info;
@GetMapping("/config/info")
public String getConfigInfo() {
return info;
}
}

再向该应用发送一个Post请求,刷新配置。

1
curl -X POST "http://localhost:3355/actuator/refresh"

服务总线

配合Config,实现所有应用全部批量刷新。

Bus

仅支持RabbitMQ,Kafka。

需要安装RabbitMQ(基于Erlang环境)

1
rabbitmq-plugins enable rabbitmq_management

访问 http://localhost:15672

登录:guestguest

使用Bus两种方式

  • 触发一个客户端,链式传播其他客户端
  • 触发一个 Config 服务端,分发给客户端

这里采用第二种方法。

在 Config 服务端引入包

1
spring-cloud-starter-bus-amqp

添加配置

1
2
3
4
5
6
7
8
9
10
11
12
spring: 
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest

management:
endporints:
web:
exposure:
include: 'bus-refresh'

同时客户端也要配置

1
spring-cloud-starter-bus-amqp
1
2
3
4
5
6
spring: 
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest

通知 Config Server 更新

1
curl -X POST "http://localhost:3344/actuator/bus-refresh"

如果需要定点通知

1
curl -X POST "http://localhost:3344/actuator/bus-refresh/{destination}"

例如

1
curl -X POST "http://localhost:3344/actuator/bus-refresh/config.client:3355"

消息驱动

用于屏蔽底层不同的消息队列的实现细节。

Stream

仅支持RabbitMQ,Kafka。

传统MQ概念

  • Message
  • MessageChannel
  • SubscribalChannel -> MessageHandler

Stream使用Binder屏蔽细节。采用发布订阅模式。

  • Input 消费者
  • Output 生产者

模块

  • Srouce - 发送端
  • Sink - 接收端
  • Channel - 通道,队列的一种抽象
  • Binder - 绑定器,连接中间件

使用

消息生产者

1
spring-cloud-starter-stream-rabbit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 8801
spring:
application:
name: provider.stream
cloud:
binders:
# 被绑定的消息队列信息
defaultRabbit: # 名称
type: rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings:
output:
destination: studyExchange # 通道名称
content-type: application/json
binder: defaultRabbit

业务类

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
// Service IMessageProvider
public interface IMessageProvider{
public String send();
}

// Impl
// 注意,这里不同于 Spring MVC
@EnableBinding(Source.class)
public class MessageProvider implements IMessageProvider{
@Autowired
private MessageChannel output;
@Override
public String send() {
String msg = "123";
output.send(MessageBuilder.withPayload(msg).build());
return null;
}
}

// Controller
@RestController
public class SendMessageController {
@Autowired
private IMessageProvider messageProvider;
@GetMapping("/send")
public String send(){
return messageProvider.send();
}
}

消息消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 8802
spring:
application:
name: consumer.stream
cloud:
binders:
# 被绑定的消息队列信息
defaultRabbit: # 名称
type: rabbit
environment:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings:
input:
destination: studyExchange # 通道名称
content-type: application/json
binder: defaultRabbit

业务类

1
2
3
4
5
6
7
8
9
// Controller
@RestController
@EnableBinding(Sink.class)
public class ReceiveMessageController {
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
String msg = message.getPayload();
}
}

当有多个消费者时,会有问题:

  • 重复消费 - 同一个Group下的消费者是竞争关系,在同一个组下可保证消息只被消费一次。
  • 消息持久化

也就是默认每个消费者都在不同的组,需要配置到相同组内。设置分组后,还会消息持久化。

修改配置,设置为轮询接收消息

1
2
3
4
5
6
bindings:
input:
destination: studyExchange # 通道名称
content-type: application/json
binder: defaultRabbit
group: groupA # 添加组名

分布式请求拦路跟踪

跟踪微服务之间的调用请求。

Sleuth

可视化框架:Zipkin

搭建

1
2
# 下载运行 zipkin 
java -jar zipkin-server-2.xx.xx-exec.jar

访问:http://localhost:9411/zipkin

Trace是一条树结构,一条链路标识一个Trace ID。

Span标识发起的请求信息,Span之间通过Parent ID关联,就像链表。每个Span节点表示一个微服务。

使用(同时包含 zipkin sleuth)

1
spring-cloud-starter-zipkin
1
2
3
4
5
6
spring:
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 # 采样率,1 为全部采集

Spring Cloud Alibaba

由于Spring Cloud Netflix进入维护模式,因此需要Alibaba

https://spring.io/projects/spring-cloud-alibaba

内容包括

  • 服务降级,限流与熔断,支持Servlet,Feign,RestTemplate,Dubbo,RocketMQ,还可以监控
  • 服务注册与发现,默认集成了Ribbon
  • 分布式配置管理,支持自动刷新
  • 消息驱动能力,基于Stream
  • 对象存储
  • 分布式任务调度

使用

1
2
3
4
5
6
7
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

模块包含

  • Sentinel - 流量控制,熔断降级,负载均衡
  • Nacos - 服务发现,配置管理
  • RocketMQ - 分布式消息和流计算平台
  • Dubbo - Java RPC 框架
  • Seata - 微服务分布式事务解决方案
  • OSS - 对象存储
  • SchedulerX - 任务调度产品

Nacos

包括服务管理,配置管理,服务发现,自带负载均衡。

相当于:Eureka + Config + Bus

下载安装

1
./bin/startup.cmd

访问http://localhost:8848/nacosnacosnacos

1
spring-cloud-starter-alibaba-nacos-discovery

服务提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 9001
spring:
application:
name: nacos.provider
cloud:
nacos:
discovery:
# 注册到该服务器上
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: '*'

主启动类

1
@EnableDiscoveryClient

编写业务类(略)

消费者

配置

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: nacos.consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848

# 可以不配
service-url:
# 要访问的微服务
nacos-user-service: http://nacos.provider

因为使用了Ribbon,要写一个配置类

1
2
3
4
5
6
7
8
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate;
}
}

编写业务类

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class OrderNacosController {
@Autowried
private RestTemplate restTemplate;
@Value(${service.nacos-user-service})
private String serverURL;
@GetMapping("/get")
public String get(){
return restTemplate.getForObject(serverURL + "/get");
}
}

当然也可以在这里整合OpenFeign。

作为配置中心:可以直接在Web界面上配置。

1
spring-cloud-starter-alibaba-nacos-config

配置客户端bootstrap.yml

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: nacos.config.client
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yaml

以及application.yml

1
2
3
spring:
profiles:
active: dev

编写业务类,用于动态刷新

1
2
3
4
5
6
7
8
9
10
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String info;
@GetMapping("/config/info")
public String getConfigInfo() {
return info;
}
}

配置服务器上的配置DataID格式为:

1
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

分类设计:采用命名空间 + 分组 + 配置ID设计。默认值为:

  • Namespace - public
  • Group - DEFAULT_GROUP
  • Cluster - DEFAULT

功能:

  • Namespace - 划分运行环境:dev prod test
  • Group - 划分不同的微服务到一个分组
  • Cluster - 一个Cluster可以包含多个微服务,一个Cluster工作在一个机房内。
  • Instance - 微服务实例。

采用不同组下同一个配置文件的方式,此时会根据组名加载不同组下的同一个文件nacos.config.client-info.yml

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: nacos.config.client
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yaml
group: TEST_GROUP

配置相应的配置文件

1
2
3
spring:
profiles:
active: info

采用不同命名空间配置方式,创建命名空间,例如dev,test,public等:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: nacos.config.client
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yaml
namespace: ... # 命名空间的ID号
group: DEFAULT_GROUP

实践:

  • Namespace - 例如可以隔离开发环境、测试环境和生产环境,因为它们的配置可能各不相同,或者是隔离不同的用户,不同的开发人员使用同一个nacos管理各自的配置,可通过namespace隔离。
  • Group - 可用于区分不同的项目或应用。是一个项目。
  • DataID - 一个配置集可能包含了数据源、线程池、日志级别等配置项。是一个工程的主配置文件。

集群和持久化

  • 单机模式:采用嵌入式数据库derby,可以切换到MySQL。
  • 集群模式:采用Nginx集群、Nacos集群(至少3个)、MySQL集群搭建,确保高可用。
  • 多集群模式

切换MySQL

找到脚本文件/nacos/conf/nacos-mysql.sql,放到MySQL数据库中执行。

找到配置文件/nacos/conf/application.application添加内容:

1
2
3
4
5
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos/config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=UTC
db.user=root
db.password=root

配置集群

修改/nacos/conf/cluster.conf文件。

1
2
3
4
192.168.100.101:3001
192.168.100.101:3002
192.168.100.101:3003
# 必须是 hostname -I 能够识别的地址

找到并修改startup.sh文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 第一步
# 添加 p 参数
while getopts ":m:f:s:p:" opt
do
case $opt in
m)
...
f)
...
s)
...
# 添加 p 参数
p)
PORT=$OPTARG;;
?)
echo "Unkonwn parameter"
exit1;;
done

# 第二步
nohup $JAVA -Dserver.port=${PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_IDR}/logs/start.out 2>&1 &
echo "nacos is starting ...."

使用启动脚本

1
2
3
./startup.sh -p 3001
./startup.sh -p 3002
./startup.sh -p 3003

再配置Nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
upstream cluster {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}

server {
listen 3000;
server_name localhost;

location / {
proxy_pass http://cluster;
}
}

Sentinel

主要负责熔断与限流。

1
spring-cloud-starter-alibaba-sentinel

主要分为前台和后台两部分。

使用

1
java -jar sentinel-dashboard-1.x.x.jar

打开http://localhost:8080sentinelsentinel

微服务配置

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: sentinel.service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719 # sentinel 检查心跳、健康状态的端口

也可以结合OpenFeign(对客户端请求做负载均衡)使用。

1
2
3
feign:
sentinel:
enabled: true

编辑业务类

1
2
3
4
5
6
7
@RestController
public class FlowLimitController{
@GetMapping("/test")
public String test(){
return "A test";
}
}

由于是懒加载机制,因此需要手动请求一次接口才能看到服务。

在簇点链路中,可以控制接口流量。

流量控制规则

  • 流控模式
    • 直连:默认,API达到限流条件,直接限流
    • 关联:当关联的资源A达到阈值,就限流自己B,例如支付接口挂了,就限流下订单的接口。
    • 链路:链路A-Z上的流量达到阈值,就限流自己A
  • 流控效果
    • 快速失败:直接失败,抛出异常。com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
    • WarmUP:慢启动。根据 阈值 / codeFactor(default=3)设置阈值,设置冷启动时间慢慢达到最大阈值。
    • 排队等待:匀速排队(必须是QPS模式)。漏桶算法。

降级规则,默认快速失败,抛出DegradeException;没有半开状态

  • RT:平均响应时间(毫秒)超出阈值且窗口内请求数超过5,触发降级;窗口期过后关闭降级
  • 异常比例:QPS超过5且异常比例(秒)超过阈值,触发降级;窗口期过后关闭降级
  • 异常数:异常数(分钟)超过阈值,触发降级;窗口期过后关闭降级

热点Key限流:根据热点参数进行限流,例如根据用户ID限流。

1
2
3
4
5
@GetMapping("/testHotKey")
@SentinelResource(value="testHotKey", blockHandler="testHandler") // 不处理运行时错误
public String test(@RequestParam(value="id") String id){}

public String testHandler(String id, BlockException exception) {}

配置

  • 资源名:testHotKey

  • 参数索引:0 - 第零个

  • 阈值:1

  • 参数例外项:当参数是某个特殊值时,不限流

系统规则:能够自适应限流。是从整体维度从入口进行控制。

  • LOAD(Linux)
  • RT
  • 线程数
  • 入口QPS
  • CPU使用率

SentinelResource

按资源名称限流

1
2
3
4
5
@GetMapping("/testHotKey")
@SentinelResource(value="testHotKey", blockHandler="testHandler")
public String test(@RequestParam(value="id") String id){}

public String testHandler(String id, BlockException exception) {}

但是要解决耦合和代码膨胀问题,使用统一的兜底类handlers/CustomerBlockHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CustomerBlockHandler{
public static String handlerException(BlockException exception) {
return "404";
}
}

public class CustomerFallbackHandler{
public static String handlerException(Throwable exception) {
return "404";
}
}

// 使用
@SentinelResource(
value="testHotKey",
blockHandlerClass=CustomerBlockHandler.class,
blockHandler="handlerException", // 触发限流处理函数
fallbackClass=CustomerFallbackHandler.class,
fallback="handlerException", // 业务出错处理函数
exceptionToIgnore={IllegalArgumentException.class}) // 忽略异常

使用OpenFeign,将内核替换为Sentinel

1
2
3
feign:
sentinel:
enabled: true

编写服务

1
2
3
4
5
6
7
8
9
10
11
12
@FeignClient(
value="service-name",
fallback=xx.class
)
public interface MyService{
// 调用的接口
@GetMapping(value="/serv/{id}")
public String serv(@PathVariable("id") Long id){}
}

@Service
public class MyServiceImpl implements MyService{}

持久化

默认情况下配置是临时的,服务关闭,配置就消失。因此需要持久化。

1
sentinel-datasource-nacos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: sentinel.service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719 # sentinel 检查心跳、健康状态的端口
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow

Seata

处理分布式事务问题:保障全局数据的一致性(多中心,多数据库)。

概念(XA协议)

  • 全局事务ID
  • TC - 事务协调者,维护全局和分支事务状态,负责提交和回滚
  • TM - 事务管理器,定义全局事务的范围
  • RM - 资源管理器,与TC交谈以注册分支事务和报告分支事务状态

控制事务,在业务方法上添加如下注解

1
2
3
4
5
// 本地事务 Spring
@Transactional

// 全局事务 Seata
@GlobalTransactional

下载配置

修改file.conf文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
事务组名称
service {
...
vgroup_mapping.my_test_tx_group = "xxx_tx_group"
...
}

存储模块方式
store {
...
mode = "db"
...
url = ""
user = ""
password = ""
...
}

然后再创建数据库,使用db_store.sql

修改registry.conf

1
2
3
4
5
6
7
8
registry {
type = "nacos"
...
nacos{
serverAddr = "localhost:8848"
...
}
}

启动seata-server.bat

一次事务案例

  1. 创建订单
  2. 远程调用库存服务,扣减商品库存
  3. 远程调用账户服务,扣减余额
  4. 修改订单状态

总共调用3次数据库,2个远程服务。

之后创建数据库3个,每个库下创建数据表,以及1个回滚日志表undo_logdb_undo_log.sql)。

创建模块

配置 POM

1
2
3
spring-cloud-starter-alibaba-nacos-discovery
spring-cloud-starter-alibaba-nacos-seata 剔除 seata-all 引入 seata-all 其他版本
spring-cloud-starter-openfeign

配置 YML

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
server:
port: 2001
spring:
application:
name: seata.order.service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://...
username: ...
password: ...
cloud:
alibaba:
seata:
tx-service-group: xxx_tx_group
nacos:
discovery:
server-addr: localhost:8848

feign:
hystrix:
enabled: false

logging:
level:
io:
seata: info

mybatis:
mapperLocations: classpath:mapper/*.xml

将上面的file.confregistry.conf放到resources下,与配置文件同级。(1.0版本后,该配置写到yml中即可)

编写实体类

1
2
CommonResult<>;
Order; // 订单

编写 DAO / Mapper

1
2
// 创建订单
// 修改订单状态

编写 Service 接口 实现类

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
@
public void create(Order order) {
// 创建订单
orderMapper.create(order);
// 修改库存
storageService.decrease(productId, count);
// 修改余额
...
// 修改订单状态
orderMapper.update(order);
}

// 调用库存服务
@FeignClient(value="storage-service")
public interface StorageService {
@PostMapping(value="/decrease")
CommonResult decrease(
@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}

// 调用支付服务
@FeignClient(value="")
public interface AccountService {
...
}

编写 Controller

1
2
3
public class OrderController{
// 调用服务层
}

编写 Config

  • Mybatis
  • Druid

配置主启动

1
2
@EnableDiscoveryClient
@EnableFeignClients

配置事务

由于Feign的超时重试机制,可能会导致账户多次扣钱。

1
2
3
4
5
6
public class OrderServiceImpl implements OrderService {
...
@Override
@GlobalTransactional(name="xxx-create-order", rollbackFor=Exception.class)
public void create(Order order) {}
}

模式

  • AT
  • TCC
  • SAGA
  • XA

参考:开源仓库

Github: spring-cloud-gateway-oauth2

在线考试系统

Things Board

One Mall

Macro Zheng

Spring Cloud

参考:其他

分布式WebSocket