…
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" > <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 > <bean id ="sqlSessionFactory" class ="org.mybatis.spring.SqlSessionFactoryBean" > <property name ="dataSource" ref ="dataSource" /> <property name ="configLocation" value ="classpath:mybatis-config.xml" /> </bean > <bean id ="sqlSession" class ="org.mybatis.spring.SqlSessionTemplate" > <constructor-arg index ="0" ref ="sqlSessionFactory" /> </bean > <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 > <setting name ="cacheEnabled" value ="true" /> <setting name ="lazyLoadingEnabled" value ="true" /> <setting name ="multipleResultSetsEnabled" value ="true" /> <setting name ="useColumnLabel" value ="true" /> <setting name ="useGeneratedKeys" value ="false" /> <setting name ="autoMappingBehavior" value ="PARTIAL" /> <setting name ="autoMappingUnknownColumnBehavior" value ="WARNING" /> <setting name ="defaultExecutorType" value ="SIMPLE" /> <setting name ="defaultStatementTimeout" value ="25" /> <setting name ="defaultFetchSize" value ="100" /> <setting name ="safeRowBoundsEnabled" value ="false" /> <setting name ="mapUnderscoreToCamelCase" value ="false" /> <setting name ="localCacheScope" value ="SESSION" /> <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; } } public static void main (String[] args) { Hello hello = new HelloImpl (); ProxyInvocationHandler proxyHandler = new ProxyInvocationHandler (); proxyHandler.setTarget(hello); Hello proxy = (Hello) proxyHandler.getProxy(); proxy.say(); }
AOP 导入包
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 > <aop:pointcut id ="pointcut" expression ="execution(* com.UserServiceImpl.*(..))" /> <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" > <aop:pointcut id ="point" expression ="execution(* com.UserServiceImpl.*(..))" /> <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
缓存日志
基础工具
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
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代替
mapper - MyBatis 使用的查询接口,代替Dao
pojo - 实体类,与数据表保持一致
service
impl
StudentServiceImpl.java - 服务实现
StudentService.java - 服务接口
MainApplication.java - SpringBoot 启动类
resources
db - 定义数据库迁移文件
config
i18n - 国际化配置
index.properties
index_zh_CN.properties
index_en_US.properties
public
resources
static
mybatis
templates
common - 公共页面
error - 错误页面
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 @Component @Repository @Service @Controller @Configuration @Bean
主配置文件 配置文件名称必须为application.yml
,不同环境可以配置不同的后缀,如application-dev.yml
,application-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 @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString @RequiredArgsConstructor @Data @AllArgsConstructor @NoArgsConstructor @Builder @Validated @TableName(value = "tb_demo_car") public class Student implements Serializable { @TableId(type = IdType.AUTO) private Integer id; private String name; @PositiveOrZero() private Integer age; @Email() 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 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 不用
也可以通过配置文件修改位置。
首页位置放在静态资源文件夹下,名称为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" ; } }
可以给公共部分传参数。
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/error
,template/error
创建相应文件4xx.html
,5xx.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 { @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: 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 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 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 @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" ); http.logout(); http.csrf().disable(); 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 ; } 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); Map<String, String> filterMap = new LinkedHashMap <>(); filterMap.put("/user/add" , "perms[user:add]" ); filterMap.put("/user/delete" , "authc" ); factoryBean.setFilterChainDefinitionMap(filterMap); factoryBean.setLoginUrl("/login" ); return factoryBean; } }
整合thymeleaf
配置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 ) .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 { @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); 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); 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 Student student = new Student (100 , "ha" , 3 );redisTemplate.opsForValue().set("student" , s);
实际上,应该建立RedisUtils,设置Key的过期时间
整合MyBatisPlus 整合MybatisPlus
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); } @Override public void updateFill (MetaObject metaObject) { log.info("start update fill ...." ); this .strictUpdateFill(metaObject, "updateTime" , () -> LocalDateTime.now(), LocalDateTime.class); } }
乐观锁:只有出问题时才测试加锁。实现方式,在字段中加入version
字段,每次更新一个version
取出记录,得到version
更新时,带上新的version
(where version=old_version)
如果条件测试失败,就更新失败
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 ); 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
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() .inSql("id" , "select id from user where age<3" );
MyBatis Plus 代码生成器
导入mybatis plus
包
配置数据库
编写配置类
测试类中编写生成器
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 ();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); 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); 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); 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 Elastic Search 底层基于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 @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: '*' 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 () { return new Queue (RabbitMQConfig.RABBITMQ_DEMO_TOPIC, true , false , false ); } @Bean public DirectExchange rabbitmqDemoDirectExchange () { 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" ; } } } @RestController @RequestMapping("/mall/rabbitmq") public class RabbitMQController { @Resource private RabbitMQService rabbitMQService; @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 @RabbitListener(queuesToDeclare = @Queue(RabbitMQConfig.RABBITMQ_DEMO_TOPIC)) public class RabbitDemoConsumer { @RabbitHandler public void process (Map<String, Object> map) { System.out.println("队列B收到消息:" + map.toString()); } }
进阶使用 在需要动态增删消息队列的情况下,需要用RabbitAdmin
或AmqpAdmin
管理消息队列和交换机,将它们注入到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 { 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 ); 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 模块,使用方法:
定义服务接口
在服务提供方实现接口
配置文件中注册服务
使用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 @Component 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 @EnableAutoConfiguration
配置/META-INF/spring.factories
实现XXXAutoConfiguration
Web 容器 Web Servlet:
Web Reactive:
在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 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")
异步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 模板引擎,内容协商,异常处理。
内容协商:多个渲染器之间。
异常处理:负责视图错误的处理,例如404等。
REST相关:资源服务,跨域,服务发现等。
资源跨域:
CrossOrigin
WebMvcConfigurer#addCorsMappings -> Spring Framework
传统解决方案:IFrame,JSONP
服务发现:
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 - 重点
服务配置
服务总线
准备过程 新建一个父工程,包含多个子工程。
父工程 使用模板
选好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); } }
多个子模块之间要使用相同的实体类。因此可以通过创建公共模块的方式来做。该公共模块也可以添加一些公用工具。
服务注册中心
组件名
语言
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 端
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/
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: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka instance: instance-id: provider-8001 prefer-ip-address: true
1 2 3 4 5 6 7 8 9 @RestController public class IndexController { @GetMapping("/") public String get () { return "content" ; } }
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: register-with-eureka: false fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka
服务地址改为服务名称
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 集群 服务器之间相互注册,都保存了其他服务器的实例。
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/
服务发现
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: lease-renewal-interval-in-seconds: 1 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
启动类设置
编写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 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 端 启动
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 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 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 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 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 ribbon: ReadTimeout: 5000 ConnectTimeout: 5000
配置日志
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 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" ; } }
主启动类
也可以配置全局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 @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 uri: http://localhost:8001 predicates: - Path=/create/** filters: - RewritePath=/api/(?<segment>.*), /create/$\{segment} - id: service2_route uri: http://localhost:8002 predicates: - Path=/create/**
主启动类
业务类不需要编写。
也可以硬编码配置。
1 2 3 4 5 6 7 8 9 10 11 @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 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
配置过滤器
生命周期
种类
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 uri: lb://provider-1.client predicates: - Path=/create/** filters: - 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
主启动类
配置文件使用规则
/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
登录:guest
,guest
使用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屏蔽细节。采用发布订阅模式。
模块
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 public interface IMessageProvider { public String send () ; } @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 ; } } @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 @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 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
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
下载安装
访问http://localhost:8848/nacos
,nacos
,nacos
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 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: ... 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
找到并修改startup.sh
文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 while getopts ":m:f:s:p:" optdo case $opt in m) ... f) ... s) ... 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:8080
,sentinel
,sentinel
微服务配置
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
也可以结合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 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 @Transactional @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
一次事务案例
创建订单
远程调用库存服务,扣减商品库存
远程调用账户服务,扣减余额
修改订单状态
总共调用3次数据库,2个远程服务。
之后创建数据库3个,每个库下创建数据表,以及1个回滚日志表undo_log
(db_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.conf
,registry.conf
放到resources
下,与配置文件同级。(1.0版本后,该配置写到yml中即可)
编写实体类
编写 DAO / Mapper
编写 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
配置主启动
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) {} }
模式
参考:开源仓库 Github: spring-cloud-gateway-oauth2
在线考试系统
Things Board
One Mall
Macro Zheng
Spring Cloud
参考:其他 分布式WebSocket