开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Linux 三剑客之 grep 使用详解

发表于 2021-02-21

Linux 三剑客之 Grep

Linux 最重要的三个命令在业界被称为三剑客,它们是:awk、sed、grep。sed 已经在**上篇**中讲过,本文要讲的是 grep 命令。

我们在使用 Linux 系统中,grep 命令的使用尤为频繁,熟练掌握 grep 的常见用法,能够极大地提高你的工作效率。

grep 命令是一种强大的文本搜索工具,它能使用正则表达式,按照指定的模式去匹配,并把匹配的行打印出来。需要注意的是,grep 只支持匹配而不能替换匹配的内容,替换的功能可以由 sed 来完成。

整体上 grep 还是比较简单的,文中不会详细列举所有的选项和参数,会以多个具体示例来说明 grep 的使用方法和场景,帮助你快速学会 grep 的常见用法。

示例实战

废话不说了,直接实战。文章中的示例 需要一个样例文件,文件内容如下:

1. 把包含 syslog 的行过滤出来

2. 把以 ntp 开头的行过虑出来

3. 把匹配 ntp 的行以及下边的两行过滤出来

4. 把包含 syslog 及上边的一行过滤出来

5. 把包含 syslog 以及上、下一行内容过滤出来

6. 过滤某个关键词,并输出行号

7. 过滤不包含某关键词,并输出行号

8. 删除掉空行

9. 过滤包含 root 或 syslog 的行

10. 查看当前目录中包含某关键词的所有文件(这个很有用)

简单总结

通过了一些简单案例操作,我们应该已经熟悉了 grep 的常见用法,下边再来简单总结 grep 的常见选项,相信在实战练习后再来总结应该会有更好的学习效果。

  • -A:除了匹配行,额外显示该行之后的N行
  • -B:除了匹配行,额外显示该行之前的N行
  • -C:除了匹配行,额外显示该行前后的N行
  • -c:统计匹配的行数
  • -e:实现多个选项间的逻辑 or 关系
  • -E:支持扩展的正则表达式
  • -F:相当于 fgrep
  • -i:忽略大小写
  • -n:显示匹配的行号
  • -o:仅显示匹配到的字符串
  • -q:安静模式,不输出任何信息,脚本中常用
  • -s:不显示错误信息
  • -v:显示不被匹配到的行
  • -w:显示整个单词
  • --color:以颜色突出显示匹配到的字符串

与 grep 相似的工具还有 egrep、fgrep,实用性并不强,其功能完全可以通过 grep 的扩展参数来实现,所以就不再扩展。

本次分享就到这里了,谢谢大家的阅读,我是肖邦。关注我的公众号「编程修养」,大量的干货文章等你来!

公众号后台回复「1024」有惊喜!

推荐阅读:

  • 写给 Linux 初学者的一封信
  • 全网最详尽的负载均衡原理图解
  • 上古神器 sed 教程详解,小白也能看的懂
  • Linux 三剑客之 grep 教程详解
  • Linux 文件搜索神器 find 实战详解,建议收藏!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spring中那些让你爱不释手的代码技巧

发表于 2021-02-21

前言

最近越来越多的读者认可我的文章,还是件挺让人高兴的事情。有些读者私信我说希望后面多分享spring方面的文章,这样能够在实际工作中派上用场。正好我对spring源码有过一定的研究,并结合我这几年实际的工作经验,把spring中我认为不错的知识点总结一下,希望对您有所帮助。

一 如何获取spring容器对象

1.实现BeanFactoryAware接口

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Service
public class PersonService implements BeanFactoryAware {
private BeanFactory beanFactory;

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

public void add() {
Person person = (Person) beanFactory.getBean("person");
}
}

实现BeanFactoryAware接口,然后重写setBeanFactory方法,就能从该方法中获取到spring容器对象。

2.实现ApplicationContextAware接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Service
public class PersonService2 implements ApplicationContextAware {
private ApplicationContext applicationContext;

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

public void add() {
Person person = (Person) applicationContext.getBean("person");
}

}

实现ApplicationContextAware接口,然后重写setApplicationContext方法,也能从该方法中获取到spring容器对象。

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。
BAT大佬写的刷题笔记,让我offer拿到手软

3.实现ApplicationListener接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Service
public class PersonService3 implements ApplicationListener<ContextRefreshedEvent> {
private ApplicationContext applicationContext;


@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
applicationContext = event.getApplicationContext();
}

public void add() {
Person person = (Person) applicationContext.getBean("person");
}

}

实现ApplicationListener接口,需要注意的是该接口接收的泛型是ContextRefreshedEvent类,然后重写onApplicationEvent方法,也能从该方法中获取到spring容器对象。

此外,不得不提一下Aware接口,它其实是一个空接口,里面不包含任何方法。

它表示已感知的意思,通过这类接口可以获取指定对象,比如:

  • 通过BeanFactoryAware获取BeanFactory
  • 通过ApplicationContextAware获取ApplicationContext
  • 通过BeanNameAware获取BeanName等
    Aware接口是很常用的功能,目前包含如下功能:

二 如何初始化bean

spring中支持3种初始化bean的方法:

  • xml中指定init-method方法
  • 使用@PostConstruct注解
  • 实现InitializingBean接口
    第一种方法太古老了,现在用的人不多,具体用法就不介绍了。

1.使用@PostConstruct注解

1
2
3
4
5
6
7
8
java复制代码@Service
public class AService {

@PostConstruct
public void init() {
System.out.println("===初始化===");
}
}

在需要初始化的方法上增加@PostConstruct注解,这样就有初始化的能力。

2.实现InitializingBean接口

1
2
3
4
5
6
7
8
java复制代码@Service
public class BService implements InitializingBean {

@Override
public void afterPropertiesSet() throws Exception {
System.out.println("===初始化===");
}
}

实现InitializingBean接口,重写afterPropertiesSet方法,该方法中可以完成初始化功能。

这里顺便抛出一个有趣的问题:init-method、PostConstruct 和 InitializingBean 的执行顺序是什么样的?

决定他们调用顺序的关键代码在AbstractAutowireCapableBeanFactory类的initializeBean方法中。

这段代码中会先调用BeanPostProcessor的postProcessBeforeInitialization方法,而PostConstruct是通过InitDestroyAnnotationBeanPostProcessor实现的,它就是一个BeanPostProcessor,所以PostConstruct先执行。

而invokeInitMethods方法中的代码:

决定了先调用InitializingBean,再调用init-method。

所以得出结论,他们的调用顺序是:

三 自定义自己的Scope

我们都知道spring默认支持的Scope只有两种:

  • singleton 单例,每次从spring容器中获取到的bean都是同一个对象。
  • prototype 多例,每次从spring容器中获取到的bean都是不同的对象。
    spring web又对Scope进行了扩展,增加了:
  • RequestScope 同一次请求从spring容器中获取到的bean都是同一个对象。
  • SessionScope 同一个会话从spring容器中获取到的bean都是同一个对象。
    即便如此,有些场景还是无法满足我们的要求。

比如,我们想在同一个线程中从spring容器获取到的bean都是同一个对象,该怎么办?

这就需要自定义Scope了。

第一步实现Scope接口:

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
java复制代码public class ThreadLocalScope implements Scope {

private static final ThreadLocal THREAD_LOCAL_SCOPE = new ThreadLocal();

@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Object value = THREAD_LOCAL_SCOPE.get();
if (value != null) {
return value;
}

Object object = objectFactory.getObject();
THREAD_LOCAL_SCOPE.set(object);
return object;
}

@Override
public Object remove(String name) {
THREAD_LOCAL_SCOPE.remove();
return null;
}

@Override
public void registerDestructionCallback(String name, Runnable callback) {

}

@Override
public Object resolveContextualObject(String key) {
return null;
}

@Override
public String getConversationId() {
return null;
}
}

第二步将新定义的Scope注入到spring容器中:

1
2
3
4
5
6
7
8
java复制代码@Component
public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope("threadLocalScope", new ThreadLocalScope());
}
}

第三步使用新定义的Scope:

1
2
3
4
5
6
7
java复制代码@Scope("threadLocalScope")
@Service
public class CService {

public void add() {
}
}

四 别说FactoryBean没用

说起FactoryBean就不得不提BeanFactory,因为面试官老喜欢问它们的区别。

  • BeanFactory:spring容器的顶级接口,管理bean的工厂。
  • FactoryBean:并非普通的工厂bean,它隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。

如果你看过spring源码,会发现它有70多个地方在用FactoryBean接口。

上面这张图足以说明该接口的重要性,请勿忽略它好吗?

特别提一句:mybatis的SqlSessionFactory对象就是通过SqlSessionFactoryBean类创建的。

我们一起定义自己的FactoryBean:

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
java复制代码@Component
public class MyFactoryBean implements FactoryBean {

@Override
public Object getObject() throws Exception {
String data1 = buildData1();
String data2 = buildData2();
return buildData3(data1, data2);
}

private String buildData1() {
return "data1";
}

private String buildData2() {
return "data2";
}

private String buildData3(String data1, String data2) {
return data1 + data2;
}


@Override
public Class<?> getObjectType() {
return null;
}
}

获取FactoryBean实例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Service
public class MyFactoryBeanService implements BeanFactoryAware {
private BeanFactory beanFactory;

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

public void test() {
Object myFactoryBean = beanFactory.getBean("myFactoryBean");
System.out.println(myFactoryBean);
Object myFactoryBean1 = beanFactory.getBean("&myFactoryBean");
System.out.println(myFactoryBean1);
}
}
  • getBean("myFactoryBean");获取的是MyFactoryBeanService类中getObject方法返回的对象,
  • getBean("&myFactoryBean");获取的才是MyFactoryBean对象。

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。
BAT大佬写的刷题笔记,让我offer拿到手软

五 轻松自定义类型转换

spring目前支持3中类型转换器:

  • Converter<S,T>:将 S 类型对象转为 T 类型对象
  • ConverterFactory<S, R>:将 S 类型对象转为 R 类型及子类对象
  • GenericConverter:它支持多个source和目标类型的转化,同时还提供了source和目标类型的上下文,这个上下文能让你实现基于属性上的注解或信息来进行类型转换。

这3种类型转换器使用的场景不一样,我们以Converter<S,T>为例。假如:接口中接收参数的实体对象中,有个字段的类型是Date,但是实际传参的是字符串类型:2021-01-03 10:20:15,要如何处理呢?

第一步,定义一个实体User:

1
2
3
4
5
6
7
java复制代码@Data
public class User {

private Long id;
private String name;
private Date registerDate;
}

第二步,实现Converter接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class DateConverter implements Converter<String, Date> {

private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Override
public Date convert(String source) {
if (source != null && !"".equals(source)) {
try {
simpleDateFormat.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
}
return null;
}
}

第三步,将新定义的类型转换器注入到spring容器中:

1
2
3
4
5
6
7
8
java复制代码@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new DateConverter());
}
}

第四步,调用接口

1
2
3
4
5
6
7
8
9
java复制代码@RequestMapping("/user")
@RestController
public class UserController {

@RequestMapping("/save")
public String save(@RequestBody User user) {
return "success";
}
}

请求接口时User对象中registerDate字段会被自动转换成Date类型。

六 spring mvc拦截器,用过的都说好

spring mvc拦截器根spring拦截器相比,它里面能够获取HttpServletRequest和HttpServletResponse 等web对象实例。

spring mvc拦截器的顶层接口是:HandlerInterceptor,包含三个方法:

  • preHandle 目标方法执行前执行
  • postHandle 目标方法执行后执行
  • afterCompletion 请求完成时执行

为了方便我们一般情况会用HandlerInterceptor接口的实现类HandlerInterceptorAdapter类。

假如有权限认证、日志、统计的场景,可以使用该拦截器。

第一步,继承HandlerInterceptorAdapter类定义拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class AuthInterceptor extends HandlerInterceptorAdapter {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestUrl = request.getRequestURI();
if (checkAuth(requestUrl)) {
return true;
}

return false;
}

private boolean checkAuth(String requestUrl) {
System.out.println("===权限校验===");
return true;
}
}

第二步,将该拦截器注册到spring容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
public class WebAuthConfig extends WebMvcConfigurerAdapter {

@Bean
public AuthInterceptor getAuthInterceptor() {
return new AuthInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getAuthInterceptor());
}
}

第三步,在请求接口时spring mvc通过该拦截器,能够自动拦截该接口,并且校验权限。

该拦截器其实相对来说,比较简单,可以在DispatcherServlet类的doDispatch方法中看到调用过程:

顺便说一句,这里只讲了spring mvc的拦截器,并没有讲spring的拦截器,是因为我有点小私心,后面就会知道。

最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。

我以往的技术群里技术氛围非常不错,大佬很多。

image.png

加微信:su_san_java,备注:加群,即可加入该群。

七 Enable开关真香

不知道你有没有用过Enable开头的注解,比如:EnableAsync、EnableCaching、EnableAspectJAutoProxy等,这类注解就像开关一样,只要在@Configuration定义的配置类上加上这类注解,就能开启相关的功能。

是不是很酷?

让我们一起实现一个自己的开关:

第一步,定义一个LogFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("记录请求日志");
chain.doFilter(request, response);
System.out.println("记录响应日志");
}

@Override
public void destroy() {

}
}

第二步,注册LogFilter:

1
2
3
4
5
6
7
8
java复制代码@ConditionalOnWebApplication
public class LogFilterWebConfig {

@Bean
public LogFilter timeFilter() {
return new LogFilter();
}
}

注意,这里用了@ConditionalOnWebApplication注解,没有直接使用@Configuration注解。

第三步,定义开关@EnableLog注解:

1
2
3
4
5
6
7
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(LogFilterWebConfig.class)
public @interface EnableLog {

}

第四步,只需在springboot启动类加上@EnableLog注解即可开启LogFilter记录请求和响应日志的功能。

八 RestTemplate拦截器的春天

我们使用RestTemplate调用远程接口时,有时需要在header中传递信息,比如:traceId,source等,便于在查询日志时能够串联一次完整的请求链路,快速定位问题。

这种业务场景就能通过ClientHttpRequestInterceptor接口实现,具体做法如下:

第一步,实现ClientHttpRequestInterceptor接口:

1
2
3
4
5
6
7
8
java复制代码public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
request.getHeaders().set("traceId", MdcUtil.get());
return execution.execute(request, body);
}
}

第二步,定义配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Configuration
public class RestTemplateConfiguration {

@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(restTemplateInterceptor()));
return restTemplate;
}

@Bean
public RestTemplateInterceptor restTemplateInterceptor() {
return new RestTemplateInterceptor();
}
}

其中MdcUtil其实是利用MDC工具在ThreadLocal中存储和获取traceId

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class MdcUtil {

private static final String TRACE_ID = "TRACE_ID";

public static String get() {
return MDC.get(TRACE_ID);
}

public static void add(String value) {
MDC.put(TRACE_ID, value);
}
}

当然,这个例子中没有演示MdcUtil类的add方法具体调的地方,我们可以在filter中执行接口方法之前,生成traceId,调用MdcUtil类的add方法添加到MDC中,然后在同一个请求的其他地方就能通过MdcUtil类的get方法获取到该traceId。

最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。

我以往的技术群里技术氛围非常不错,大佬很多。

image.png

加微信:su_san_java,备注:加群,即可加入该群。

九 统一异常处理

以前我们在开发接口时,如果出现异常,为了给用户一个更友好的提示,例如:

1
2
3
4
5
6
7
8
9
10
java复制代码@RequestMapping("/test")
@RestController
public class TestController {

@GetMapping("/add")
public String add() {
int a = 10 / 0;
return "成功";
}
}

如果不做任何处理请求add接口结果直接报错:

what?用户能直接看到错误信息?

这种交互方式给用户的体验非常差,为了解决这个问题,我们通常会在接口中捕获异常:

1
2
3
4
5
6
7
8
9
10
java复制代码@GetMapping("/add")
public String add() {
String result = "成功";
try {
int a = 10 / 0;
} catch (Exception e) {
result = "数据异常";
}
return result;
}

接口改造后,出现异常时会提示:“数据异常”,对用户来说更友好。

看起来挺不错的,但是有问题。。。

如果只是一个接口还好,但是如果项目中有成百上千个接口,都要加上异常捕获代码吗?

答案是否定的,这时全局异常处理就派上用场了:RestControllerAdvice。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
if (e instanceof ArithmeticException) {
return "数据异常";
}
if (e instanceof Exception) {
return "服务器内部异常";
}
retur nnull;
}
}

只需在handleException方法中处理异常情况,业务接口中可以放心使用,不再需要捕获异常(有人统一处理了)。真是爽歪歪。

十 异步也可以这么优雅

以前我们在使用异步功能时,通常情况下有三种方式:

  • 继承Thread类
  • 实现Runable接口
  • 使用线程池

让我们一起回顾一下:

继承Thread类

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class MyThread extends Thread {

@Override
public void run() {
System.out.println("===call MyThread===");
}

public static void main(String[] args) {
new MyThread().start();
}
}

实现Runable接口

1
2
3
4
5
6
7
8
9
10
java复制代码public class MyWork implements Runnable {
@Override
public void run() {
System.out.println("===call MyWork===");
}

public static void main(String[] args) {
new Thread(new MyWork()).start();
}
}

使用线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class MyThreadPool {

private static ExecutorService executorService = new ThreadPoolExecutor(1, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(200));

static class Work implements Runnable {

@Override
public void run() {
System.out.println("===call work===");
}
}

public static void main(String[] args) {
try {
executorService.submit(new MyThreadPool.Work());
} finally {
executorService.shutdown();
}

}
}

这三种实现异步的方法不能说不好,但是spring已经帮我们抽取了一些公共的地方,我们无需再继承Thread类或实现Runable接口,它都搞定了。

如何spring异步功能呢?

第一步,springboot项目启动类上加@EnableAsync注解。

1
2
3
4
5
6
7
8
java复制代码@EnableAsync
@SpringBootApplication
public class Application {

public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
}
}

第二步,在需要使用异步的方法上加上@Async注解:

1
2
3
4
5
6
7
8
9
java复制代码@Service
public class PersonService {

@Async
public String get() {
System.out.println("===add==");
return "data";
}
}

然后在使用的地方调用一下:personService.get();就拥有了异步功能,是不是很神奇。

默认情况下,spring会为我们的异步方法创建一个线程去执行,如果该方法被调用次数非常多的话,需要创建大量的线程,会导致资源浪费。

这时,我们可以定义一个线程池,异步方法将会被自动提交到线程池中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码@Configuration
public class ThreadPoolConfig {

@Value("${thread.pool.corePoolSize:5}")
private int corePoolSize;

@Value("${thread.pool.maxPoolSize:10}")
private int maxPoolSize;

@Value("${thread.pool.queueCapacity:200}")
private int queueCapacity;

@Value("${thread.pool.keepAliveSeconds:30}")
private int keepAliveSeconds;

@Value("${thread.pool.threadNamePrefix:ASYNC_}")
private String threadNamePrefix;

@Bean
public Executor MessageExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

spring异步的核心方法:

根据返回值不同,处理情况也不太一样,具体分为如下情况:

十一 听说缓存好用,没想到这么好用

spring cache架构图:

它目前支持多种缓存:

我们在这里以caffeine为例,它是spring官方推荐的。

第一步,引入caffeine的相关jar包

1
2
3
4
5
6
7
8
9
java复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.0</version>
</dependency>

第二步,配置CacheManager,开启EnableCaching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(){
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
//Caffeine配置
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
//最后一次写入后经过固定时间过期
.expireAfterWrite(10, TimeUnit.SECONDS)
//缓存的最大条数
.maximumSize(1000);
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}

第三步,使用Cacheable注解获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@Service
public class CategoryService {

//category是缓存名称,#type是具体的key,可支持el表达式
@Cacheable(value = "category", key = "#type")
public CategoryModel getCategory(Integer type) {
return getCategoryByType(type);
}

private CategoryModel getCategoryByType(Integer type) {
System.out.println("根据不同的type:" + type + "获取不同的分类数据");
CategoryModel categoryModel = new CategoryModel();
categoryModel.setId(1L);
categoryModel.setParentId(0L);
categoryModel.setName("电器");
categoryModel.setLevel(3);
return categoryModel;
}
}

调用categoryService.getCategory()方法时,先从caffine缓存中获取数据,如果能够获取到数据则直接返回该数据,不会进入方法体。如果不能获取到数据,则直接方法体中的代码获取到数据,然后放到caffine缓存中。

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。
BAT大佬写的刷题笔记,让我offer拿到手软

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】Passport 文档(一)入门

发表于 2021-02-21

本系列一共4篇文章

  • [译]Passport 文档(一)入门 Passport 简介
  • [译]Passport 文档(二)提供者 提供者相关
  • [译]Passport 文档(三)基本 & 摘要 介绍认证策略
  • [译]Passport 文档(四)操作 Passport 登录、登出、授权操作

下面正文开始啦 ^_^

www.passportjs.org/docs/downlo… 原文地址

概览

Passport 是 NodeJS 的认证中间件。他的唯一设计目的是:验证请求。书写模块化的、封装代码是一种美德,所以 Passport 将除了验证请求之外的功能都分发给应用程序来实现。关注点分离使代码能够更加整洁、可维护,同时也使 Passport 能够极易集成到应用中。

现在 Web 程序,认证有多种形式。传统的,用户通过用户名、密码登录。随着社交网络使用上升,使用 OAuth 的单点登录,例如 Facebook 或者 Twitter 已经成为了一种流行的认证方式。暴露一个 API 的服务通常需要基于 token 的证书来保护访问。

Passport 认识到每个应用有自己独特的认证需求。认证机制,也被成为策略,被打包成单独的模块。应用能够选择采用的策略,无需创建不需要的依赖。

不管认证的复杂性,代码能够不变的复杂。

1
js复制代码app.post('/login', passport.authentication('local', {successRedirect: '/', failureRedirect: '/login'}));

安装

1
js复制代码npm install passport

认证

认证请求就像调用 passport.authenticate() 和指定采用哪种策略一样简单。authenticate() 的函数签名是标准的 Connect 中间件,所以能够方便的作为路由中间件在 Express 应用中使用。

1
2
3
4
5
6
7
8
js复制代码app.post('/login',
passport.authenticate('local'),
function(req, res) {
// 如果这个函数被调用,说明认证成功。
// `req.user` 包含认证的 user
res.redirect('/user', + req.user.username);
}
)

默认情况下,如果认证失败,Passport 将返回 401 Unauthorized,后续其他的路由处理器将不会执行。如果认证成功,下个处理器将调用,req.user 属性将设置为认证 user。

注意:在路由使用策略时,一定要预先配置。继续阅读详细配置 todo 章节。

重定向

重定向通常是在认证请求之后发出的。

1
2
3
4
5
6
js复制代码app.post('/login',
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login',
})
)

这种情况下,重定向选项将覆盖默认行为。若成功认证,用户将重定向到主页。若认证失败,用户将重定向返回到登录页去再次尝试。

即时消息(Flash Messages)

重定向通常和即时消息结合来展示用户的状态信息。

1
2
3
4
5
6
7
js复制代码app.post('/login',
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login',
failureFlash: true
});
);

设置 failureFlash 选项为 true,指示 Passport 去发送 来自策略验证回调的 error 信息。这通常是最好的方法,因为验证回调能够最准确的判断为什么认证失败。

或者,即时消息能够自定义设置。

1
2
3
js复制代码passport.authenticate('local', {
failureFlash: '无效用户名或者密码。'
})

successFlash 选项能够发送一个 success 的消息,在认证成功的时候。

1
2
3
js复制代码passport.authenticate('local', {
successFlash: '欢迎!'
})

注意:使用即时消息需要 req.flash() 函数。Express 2.x 提供了这个功能,不过 Express 3.x 移除了。在使用 Express 3.x 时,建议使用 connect-flash 中间件,它提供了这个功能。

禁止 Sessions

成功授权后,Passport 将建立一个持久的登录 session。对于用户通过浏览器访问 web 应用的场景这是有用的。然后,其他情况下,不需要 session 支持。例如,API 服务器通常需要每个请求携带凭证。这种情况下,session 支持能够通过设置 session 选项为 false 来安全的禁用。

1
2
3
4
5
6
js复制代码app.get('/api/users/me',
passport.authenticate('basic', { session: false }),
function(req, res) {
res.json({id: req.user.id, username: req.user.username});
}
)

自定义回调

如果内部选项不足够处理认证请求,自定义回调能够让应用处理成功和失败的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码app.get('/login', function(req, res, next){
passport.authenticate('local', function(err, user, info) {
if(err) {
return next(err);
}
if(!user) {
return res.redirect('/login');
}
req.logIn(user, function(err) {
if(err) {
return next(err)
}
return res.redirect('/users' + user.username)
})
})(req, res, next);
})

这个例子中,注意 authenticate() 是在路由处理器中被调用,而不是作为路由中间件。这通过闭包给了 req和 res 对象回调权限。

如果认证失败,user 将被设置为 false。如果发生异常,err 将被设置。一个可选的 info 参数将传入,包括策略验证回调所提供的附加的详细信息。

这个回调能够使用提供的参数处理预期的认证结果。注意,当使用自定义回调时,需要应用来建立 session(通过调用 req.login()) 和发送响应。

配置

使用 Passport 来认证需要配置三个方面:

1、认证策略
2、应用中间件
3、Session(可选的)

策略

Passport 使用被称为策略的东西来认证请求。策略从验证用户名、密码,使用 OAuth 委托认证或者使用 OpenID 联合认证。

在让 Passport 认证请求前,应用使用的某个策略(或者某些策略)必须要先配置。

策略和他们的配置通过 use() 函数提供。例如,下面的例子使用 LocalStrategy 来进行用户名、密码认证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码var passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;

passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) {return done(err)}
if (!user) {
return done(null, false, { message: '用户名错误' })
}
if (!user.validPassword(password)) {
return done(null, false, { message: '密码错误'})
}
return done(null, user)
})
}
))

验证回调

这个例子引入了一个重要的概念。策略需要一个称为回调的东西。验证回调的目的是找到拥有一套凭证的用户。

当 Passport 认证请求时,它解析请求中的凭证。然后将凭证作为调用回调函数的参数,这个例子中就是 username 和 password。如果凭证有效,回调函数将调用 done 将已认证的用户的信息传入 Passport。

1
js复制代码return done(null, user);

如果验证失败(本例中,比如密码错误),done 函数应该传入 false 而不是用户信息来表明认证失败。

1
js复制代码return done(null, false);

可以提供额外的消息来表明失败原因。这对于展示即时消息,来提示用户再次尝试很有用。

1
js复制代码return done(null, false, { message: '密码错误' });

最后,当验证凭证时发生异常(例如,数据库服务不可用),在常规的 Node 操作中 done 应该被调用来传入错误信息。

1
js复制代码return done(err);

注意,对于区分两种能够发生失败的情况是很重要的。后者是服务端异常,这种情况下 err 被设置为非 null 的值。在服务器正常运行时,认证失败也是很自然的情况。确认 err 包含 null,使用最后一个参数传递详细信息。

验证回调通过委派的方式使 Passport 可以无需数据库支持。应用可以自己决定如何存储用户信息,没有验证层强加的任何假设。

中间件

在基于 Connect 或者 Express 的应用中,需要使用 passport.initialize() 中间件来初始化 Passport。如果你的应用使用了持久化登录 Session,passport.session() 中间件也需要使用。

1
2
3
4
5
6
7
8
9
js复制代码app.configure(function() {
app.use(express.static('public'));
app.use(express.cookieParser());
app.use(express.bodyParser());
app.use(express.session({ secret: 'keyboard cat' }));
app.use(passport.initialize());
app.use(passport.session());
app.use(app.router);
})

注意,开启 session 支持完全是可选的,尽管建议将其用于大多数应用中。如果开启,确认在 passport.session() 前使用 session(),从而确保登录 session 能够按正确的顺序存储。

在 Express 4.x 中,Connect 中间件不再包含于 Express 核心模块中,app.configure() 也被移除了。相同的中间件能够在 npm 模块找到。

1
2
3
4
5
6
7
8
js复制代码var session = require('express-session'),
bodyParser = require('body-parser');

app.use(express.static('public'));
app.use(session({ secret: 'cats' }));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());
sessions 会话

在常规的 web 应用中,用于认证用户的凭证仅在登录请求时被发送。如果认证成功,一个session 将被建立和保持,通过设置在浏览器中的 cookie。

每个随后的请求将不再包含凭证,但是会有唯一的 cookie 来确认 session。为了支持登录 session,Passport 将序列化和反序列化 user 实例经由 session。

1
2
3
4
5
6
7
8
9
js复制代码passport.serializeUser(function(user, done) {
done(null, user.id);
})

passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
})
})

这个例子中,仅 user ID 被序列化到 session 中,从而保持存储在 session 中的数据量较小。当后续请求到达时,这个 ID 将用来找到存储在 req.user 中的 user。

序列化和反序列化逻辑由应用来提供,允许应用选择一个合适的数据库和(或者)对象mapper,无需认证层强加。

用户名 和 密码

最广泛使用的网站认证用户的方式是通过用户名和密码。对这种机制的支持是通过提供 passport-local 模块。

安装

1
js复制代码npm install passport-local

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy;

passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function(err, user) {
if(err) {
return done(err)
}
if(!user) {
return done(null, false, { message: '用户名错误。' })
}
if(!user.validPassword(password)) {
return done(null, false, { message: '密码错误。' })
}
return done(null, user);
})
}
))

这个本地认证的验证回调接受 username 和 password 参数,通过应用的登录表单提交上来的。

表单

Web 页面的一个表单,允许用户输入他们凭证然后登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<form action="/login" method="post">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>

路由

登录表单通过 POST 方法提交到服务器。使用 local 策略的 authenticate 函数来处理登录请求。

1
2
3
4
5
6
7
js复制代码app.post('/login',
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login',
failureFlash: true
})
)

设置 failureFlash 选项为 true 表明 Passport 使用验证回调提供的 message 选项来发送一个 error 消息。这对于提示用户再试一次很有用。

参数

默认的,LocalStrategy 预期在参数中找到命名为 username 和 password 的凭证。如果你的网站更喜欢用其他字段命名,有可用的选项支持修改默认值。

1
2
3
4
5
6
7
8
js复制代码passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'passwd',
},
function(username, password, done) {
// ...
}
))

OpenID

OpenID 是一个联合认证的开放标准。当访问网站时,用户使用 OpenID 登录。用户通过他们选择 OpenID 提供者(它发出一个断言来确认用户身份)来认证。网站验证这个断言来让用户登录。

OpenID 的支持通过 passport-openid 模块提供。

安装

1
js复制代码npm install passport-openid

配置

当使用 OpenID 时,返回地址和领域必须设置。 returnURL 是用户在使用 OpenID 提供者认证后重定向的地址。realm 表明 URL空间中验证有效的部分。通常它会是网站的根 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码var passport = require('passport'),
OpenIDStrategy = require('passport-openid').Strategy;

passport.use(new OpenIDStrategy({
returnURL: 'http://www.example.com/auth/openid/return',
realm: 'http://www.example.com/'
},
function(identifier, done) {
User.findOrCreate({openId: identifier}, function(err, user) {
done(err, user);
})
}
))

OpenID认证的验证回调接受一个 identifier 参数,包含用户的声明识别码。

表单

web 页面的表单,允许用户输入 OpenID 然后登录。

1
2
3
4
5
6
7
8
9
html复制代码<form action="/auth/openid" method="post">
<div>
<label>OpenID:</label>
<input type="text" name="openid_identifier"/><br/>
</div>
<div>
<input type="submit" value="Sign In"/>
</div>
</form>

路由

OpenID 认证需要2个路由。第一个路由接受表单提交中包含的 OpenId 识别码。认证期间,用户将被重定向到 OpenID 提供者。第二个路由是用户在使用 OpenID 提供者认证后,将返回的 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// 接受 OpenID 识别码,将用户重定向到 OpenID 提供者处去认证。完成后,提供者将用户重定向到应用:
// /auth/openid/return
app.post('/auth/openid', passport.authenticate('openid'));

// OpenID 提供者已经将用户重定向到应用。
// 通过验证断言来完成认证过程。如果有效,用户将登录。
// 否则,认证失败。
app.get('/auth/openid/return',
passport.authenticate('openid', {
successRedirect: '/',
failureRedirect: '/login',
})
)

个人资料交换

OpenID 能够可选择的设置为取回已认证用户的个人信息。个人资料交换通过设置 profile 选项为 true 开启。

1
2
3
4
5
6
7
8
9
js复制代码passport.use(new OpenIDStrategy({
returnURL: 'http://www.example.com/auth/openid/return',
realm: 'http://www.example.com/',
profile: true
},
function(identifier, profile, done) {
// ...
}
))

当个人资料交换开启后,验证回调函数签名接收额外的 profile 参数,包含 OpenID 提供者提供的用户个人信息;通过 User Profile 来了解更多信息。

OAuth

OAuth 是个标准协议,它允许用户授权 API 使用权给网站、桌面程序或者移动应用。一旦被授权,被授权的应用能够代表用户使用 API。OAuth 也已经成为流行的委托授权机制。

OAuth 有两种主要形式,两种都被广泛的部署。

OAuth 初始版本被一群组织松散的 Web 开发者开发作为开放标准。他们开发了 OAuth 1.0,被 OAuth 1.0a 取代。这项工作现在被 IEFT 作为 RFC 5849 准备被标准化。

最近的工作 – 专注定义 OAuth 2.0,已经被 web 授权协议工作组 承担。由于长期的标准化工作,提供者已经开始部署符合各种草案的实现,每种语义略有不同。

谢天谢地,Passport 隔离了应用处理 OAuth 变体的复杂性。许多情况下,特定策略的提供者能够被使用。而不是下面描述的通用策略。这减少了必须的配置,并且能容纳任何提供者的特定怪癖。查看 Facebook, Twitter 或者列表中的提供者的首选用法。

OAuth 的支持通过 passport-oauth 模块实现

安装

1
js复制代码npm install passport-oauth

OAuth 1.0

OAuth 1.0 是包含多个步骤的代理认证策略。首先,需要获得请求 token。然后,用户被重定向到服务提供者处授权。最后,授权后,用户被重定向回应用并且请求 token 能够用来交换访问 token。请求访问的应用程序(称为消费者)由消费者 key 和 消费者 secret 标识。

配置

当使用通用 OAuth 策略时,key, secret 和 endpoints 作为选项定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码var passport = require('passport'),
OAuthStrategy = require('passport-oauth').OAuthStrategy;

passport.use('provider', new OAuthStrategy({
requestTokenURL: 'https://www.provider.com/oauth/request_token',
accessTokenURL: 'https://www.provider.com/oauth/access_token',
userAuthorizationURL: 'https://www.provider.com/oauth/authorize',
consumerKey: '123-456-789',
consumerSecret: 'shhh-its-a-secret',
callbackURL: 'https://www.example.com/auth/provider/callback'
},
function(token, tokenSecret, profile, done) {
User.findOrCreate(..., function(err, user) {
done(err, user);
})
}
))

基于 OAuth 策略的验证回调接受 token,tokenSecret 和 profile 参数。token 是访问 token,tokenSecret 是它对应的秘钥。profile 包含服务提供者提供的用户个人信息。通过 User Profile 了解更多信息。

路由

OAuth 认证需要2个路由。第一个路由发起一个 OAuth 交换和重定向用户到服务提供者处。第二个路由是个URL,用户在提供者认证后重定向的 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码// 重定向用户到用于认证的 OAuth 提供者处。当认证完成后,
// 提供者将重定向用户回到应用:
// /auth/provider/callback
app.get('/auth/provider', passport.authenticate('provider'));

// OAuth 提供者已经重定向用户回到应用。
// 通过获取访问 token 来结束认证过程。如果已经授权,用户将登录。
// 否则,认证失败。
app.get('/auth/provider/callback',
passport.authenticate('provider', {
successRedirect: '/',
failureRedirect: '/login'
})
)

链接

在网页中可以放置一个链接或者按钮,它们在点击时将开始认证过程。

1
html复制代码<a href="/auth/provider">使用 OAuth 提供者登录</a>

OAuth 2.0

OAuth 2.0 是 OAuth 1.0 的接班人,被设计来克服早期版本的已知缺陷。认证流程本质上是一样的。用户首先被重定向到服务提供者处授权,授权后,用户携带一个能够获取访问 token 的码被重定向回应用。请求访问的应用(作为客户端)由 ID 和 秘钥标识。

配置

使用通用 OAuth 2.0 策略时,client ID,client secret 和 endpoints 作为选项定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码var passport = require('passport'),
OAuth2Strategy = require('passport-oauth').OAuth2Strategy;

passport.use('provider', new OAuth2Strategy({
authorizationURL: '',
tokenURL: '',
clientID: '',
clientSecret: '',
callbackURL: '',
},
function(accessToken, refreshToken, profile, done) {
User.findOrCreate(..., function(err, user) {
done(err, user);
})
}
))

基于 OAuth 2.0 策略的验证回调接受 accessToken,refreshToken 和 profile 参数。refreshToken 能够用来获取新的访问 token,也有可能是 undefined 如果提供者不发行刷新 token。profile 将包含服务提供者提供的用户个人信息。查看 User Profile 了解更多信息。

路由

OAuth 2.0 认证需要两个路由。第一个路由重定向用户到服务提供者。第二个路由是个 URL,用户在提供者处认证后重定向的URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码// 重定向用户到 OAuth 2.0 认证提供者。
// 认证完成后,提供者将重定向用户返回应用:
// /auth/provider/callback
app.get('/auth/provider', passport.authenticate('provider'));

// OAuth 2.0 提供者已经重定向用户到应用中。
// 通过尝试获取访问 token 来完成认证过程。
// 如果已授权,用户将登录。
// 否则,认证失败。
app.get('/auth/provider/callback',
passport.authenticate('provider', {
successRedirect: '/',
failureRedirect: '/login',
})
)

作用域

当使用 OAuth 2.0 请求访问时,访问的作用域通过 scope 选项控制。

1
2
3
js复制代码app.get('/auth/provider',
passport.authenticate('provider', { scope: 'email' })
)

能够使用数组定义多个作用域。

1
2
3
js复制代码app.get('/auth/provider',
passport.authenticate('provider', { scope: ['email', 'sms'] })
)

scope 选项的值时提供者特定的。详情参考提供者文档,了解支持的作用域。

链接

Web 页面的链接或者按钮,当点击的时候能够开始认证过程。

1
js复制代码<a href="/auth/provider">使用 OAuth 2.0 提供者登录</a>

用户个人信息

当使用第三方服务,例如 Facebook 或者 Twitter 认证时,用户个人信息通常会可用。每个服务倾向于有个不同的方式编码这些信息。为了更易集成,Passport 尽可能规范化个人信息。

规范化个人信息符合 [Joseph Smarr][schema-author] 建立的联系模式。下表概述了可用的公共字段。

provider {String}

  提供者,用户认证的地方(facebook, twitter, 等)。

id {String}

  用户的唯一标识,通常由服务提供者生成。

displayName {String}

  用户名,适合展示

name {Object}

  familyName 用户的 family name,或者大多数西方语言的 “last name”。

  givenName 用户的 given name,或者大多数西方语言的 “first name”。

  middleName 用户的 middle name。

emails {Array} [n]

  value {String} 地址的 email 地址。

  type {String} email 地址的类型(家里、工作单位,等)。

photos {Array} [n]

  value {String} 图片地址

注意,上面的字段不是全部都能从每个服务提供者处获得。一些提供者可能包含没在这列出的其他信息。参考特定提供者的文档来了解更多详细信息。

参考

  • developer.mozilla.org/zh-CN/docs/… function signature

感谢阅读

感谢你阅读到这里,翻译的不好的地方,还请指点。希望我的内容能让你受用,再次感谢。by llccing 千里

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SQL调优实战总结 SQL调优实战总结 前言 方法论 实战一

发表于 2021-02-21

SQL调优实战总结

前言

作为开发人员,我们免不了与sql打交道。有些sql可能在业务的最开始,执行是毫无问题的。但是随着业务量的提升以及业务复杂度的加深,可能之前的sql就会逐渐展现出疲惫之势了。这时就会面临sql调优。

那么调优到底如何调?不同的人有不同的姿势。可能大部分人首先想到的就是加索引。

在这里插入图片描述

没错,加索引是一种比较典型,也是一种比较廉价的手段。

但是,索引怎么加?加在哪里?加之后是否会对已有的其他sql产生影响?这些问题都是需要考虑的。

同时也应该意识到,索引是一把双刃剑,就像硬币的两面,加快查询速度的同时,也会拖慢我们插入删除的速度。

当然,除了加索引之外,在创造sql时,也可以借鉴一些成熟的经验总结,去预防一些问题。如遵循最左匹配的原则、select时指明具体字段、不建议使用函数、超过3张表不建议join等等。

但是实际上,不同业务场景面对的问题是不尽相同的。同一条sql在数据量、数据分布不同的情况下,其执行结果可能是截然不同的。当sql无法持续支撑业务继续发展时,我们就需要结合实际进行调优。

方法论

对于sql调优的整个过程,我有一套自己的方法论,可以大致分为以下四个阶段:

1.白盒分析

此阶段我们可以根据自己已有的知识积累和经验,猜想sql可能慢的原因。

2.执行计划解读

通过explain结果,解读并模拟出mysql服务器对sql的真实执行过程。

3.瓶颈点确定

白盒分析+执行计划解读之后,基本可以确定sql可能慢的原因。

4.对症下药

找到瓶颈点后,逐个击破。

接下来,我就通过两个实例(单表查询和连接查询)与大家一起交流下调优的一些心得。

以下的调优过程主要是站在sql层面进行优化。并非站在整个系统方面,如分表、切换存储媒介等。

实战一

主人公sql为单表查询,其执行时间可达到2.3s。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码select
很多字段
from
t_voucher_header
where
period_year = 2020
and period_month = 10
and user_je_source_name = 'xxx'
and `status` in (10, 30, 50)
and employee_number != '1000'
and can_auto_push = 1
and is_delete = 0
and batch_id > xxx
limit
200

sql背景

全表数据量在700w+,batch_id为主键字段。

已存在的相关索引为:

联合索引1:user_je_source_name、voucher_category、record_type、company_code、status
联合索引2:period_year、period_month、user_je_source_name、voucher_category、is_delete、company_code、status
联合索引3:period_year、period_month、user_je_source_name、status

白盒分析

对于单表查询比较简单,主要有两点可以考量。

1.单表查询,是否用到了索引?

2.单表查询,是否用对了索引?

执行计划解读

在这里插入图片描述

以上我们可以读到这些信息:

1.索引使用长度较低,仅仅使用了联合索引的第一个字段来加快查找速度。

2.Using index condition是在5.6之后引入的新特性,索引条件下推。出现情况是在搜索条件中虽出现了索引列,却并不能用到(前缀匹配嘛)。但是由于索引中包含了搜索条件,因此可利用索引进行过滤。
以本sql看,查询条件为period_year、period_month、user_je_source_name、status、employee_number、can_auto_push和is_delete。最终走到的索引是user_je_source_name、voucher_category、record_type、company_code、status。也就是说,利用了索引的user_je_source_name列,找到对应记录后,利用索引的status和batch_id(batch_id是主键)先进行过滤下,然后得到满足条件的主键id,再根据主键id到一级索引进行查询。

3.Using where与Using index condition一同出现,表示按照主键id查询到数据返回给mysql服务器之后,会按照剩余条件进行过滤。
以本sql来看,在Using index condition阶段对条件user_je_source_name和status进行了条件匹配,剩下的period_year、period_month、employee_number、can_auto_push和is_delete则是Using where阶段由mysql服务器进行过滤的。

如此,此sql的执行过程在我们脑海中有了一个大致的轮廓,如下:
在这里插入图片描述

瓶颈点确定

单表查询sql的执行,大体上可以分为两步。“二级索引find”和“回表select”。

“二级索引find”。

有两层含义,一方面是利用二级索引去找,另一方面是利用索引进行过滤(即索引条件下推)。

因此正确利用索引也有两层含义,一是利用的索引长度越多越好,二是经过索引find之后,能过滤掉的数据越多越好。

“回表select”。

因为二级索引毕竟包含的字段列有限,如果我们select的字段不能被索引全包含,在“二级索引find”结束后,根据得到的主键id,需要去一级索引上查询所需的列。这个过程叫做回表。而“二级索引find”阶段得到的主键id并非有序的,意味着回表是一个随机IO的过程,也就注定了回表是一个成本较高的操作。这也就是为什么有时候mysql宁愿全表扫描也不愿走索引的原因。

索引条件下推的出现就是为了节省回表的成本。

当然,如果我们最终select出来的字段能够被二级索引完全包含,“二级索引find”阶段之后,就不需要进行回表操作,这就是索引覆盖。当出现索引覆盖时,执行计划中Extra列将会通过Using index告诉我们。

回到我们的sql中,表中明明存在与查询条件极度匹配的联合索引(period_year、period_month、user_je_source_name、status),但是mysql却不用,这说明了什么?

说明了period_year、period_month、user_je_source_name、status这些条件并不具备很高的区分度,真正具有区分度的其实是在employee_number、can_auto_push和is_delete当中!

通过控制变量法,对employee_number、can_auto_push和is_delete取不同值时的数据量对比,发现当is_delete分别取0和1时,数据量差别极大。
在这里插入图片描述

可以看到,对于is_delete字段,取值1时,数据量在50w+,取值0时,数据量在1000+。

而我们所要查询的正是is_delete=0的数据。

结合我们第二阶段对执行计划的解读。最终可以确定此sql执行时间过长的瓶颈点就在回表。

该sql只想查询is_delete=0对应的1000+条数据,获取这些数据可能几次回表就能得到。但是通过“索引find”阶段,我们并不能对is_delete字段进行过滤,导致“索引find”阶段会得到50w+的id,然后对这50w+的id进行回表。回表后通过Using where对is_delete字段进行过滤。

整个过程慢就慢在回表。

对症下药

以上一通分析,已经确定了瓶颈点。

即表所建立的索引没有能够高效地把数据在回表之前过滤掉,导致回表耗费了大量时间。因此我们在已有的period_year、period_month、user_je_source_name和status组成的联合索引之上,附加一列is_delete。这样,即能解决当前sql的问题,也不会对已有其他sql产生影响。

经过此番调整后,sql查询正确走到了period_year、period_month、user_je_source_name、status和is_delete索引,时间也从2.3秒降到了毫秒级,药到病除。

优化前后对比示意图:
在这里插入图片描述

小结

该条sql的业务场景决定了最终需要查询的数据固定为is_delete=0的。那如果我们需要查询is_delete=1的数据,以上添加is_delete到既有索引列上的方法,并不能解决问题,因为无法减少对is_delete=1的50w+数据进行回表。
在这里插入图片描述

那这时我们就要换个思考方向了,为何业务上会出现如此多被物理删除的数据(is_delete=1)?是偶发还是常态?如果是常态,是否我们的表结构设计出现了问题,需不需要拆表?等等。

而这也正是我想说的,sql调优=业务场景+sql执行分析,两者缺一不可。脱离任何一方去谈sql调优都是不可取的。

实战二

第二条主人公sql为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码SELECT
b.很多字段
FROM
t_voucher_line b
INNER JOIN (
SELECT
batch_id batchId
FROM
t_voucher_header
WHERE
combine_batch_id = 2007044062
AND is_delete = 0
) a ON b.batch_id = a.batchId
AND b.is_delete = 0
WHERE
b.segment3 LIKE '1002%'
and b.id > 18496282
ORDER BY
b.id ASC
LIMIT
300

其执行时间在1s+。

这是一个涉及到多表的连接查询,在进行调优之前,想先简单说一下连接的基本原理。

连接查询的过程

连接本身是一个求笛卡尔积的过程。
在这里插入图片描述

对于一个简单的连接查询sql:

1
sql复制代码SELECT * FROM t1, t2 WHERE t1.c1 > 1 AND t1.c1 = t2.c1 AND t2.c2 < 'd';

可将其条件分为

单表查询条件:t1.c1 > 1和 t2.c2 < ‘d’

多表查询条件:t1.c1 = t2.c1

整个连接的过程大致分为以下:

1.确定驱动表,假定t1表为驱动表。应用驱动表t1的单表条件查询出满足条件的记录。
在这里插入图片描述

2.针对步骤1所匹配到的记录,分别去被驱动表t2中查找数据。因为从驱动表t1中查找到了2条记录,因此会对被驱动表t2执行两次单表查询。涉及多表查询的条件t1.c1 = t2.c1此时登场。

t1.c=2时,t1.c1 = t2.c1等价于t2.c1=2,此时对t2表的单表查询条件为t2.c1=2,t2.c2 < ‘d’

t1.c=3时,t1.c1 = t2.c1等价于t2.c1=3,此时对t2表的单表查询条件为t2.c1=3,t2.c2 < ‘d’

在这里插入图片描述

3.将步骤2查询到的结果合并即为最终结果

对于连接查询来说,驱动表只会访问一次,被驱动表将会访问多次,具体由驱动表执行单表查询后的结果集中的记录条数决定。

以上属于比较直接的方法,遍历步骤1的结果,一次一次去对被驱动表进行查询(虽然查询被驱动表时可以利用被驱动表的索引加快),称之为嵌套循环连接(Nested-Loop Join)。

类似如下的伪代码

1
2
3
4
5
6
java复制代码for each row in t1 {   #此处表示遍历满足对t1单表查询结果集中的每一条记录
for each row in t2 { #此处表示对于某条t1表的记录来说,遍历满足对t2单表查询结果集中的每一条记录
if row satisfies join conditions, send to client
}
}
}

假如从驱动表中读到了m条记录,那么对被驱动表的访问次数为m次。每一次访问被驱动表,都是多次地从硬盘中读取数据页。若被驱动表被分为了n个数据页,那么对于被驱动表的访问,共需要经历m*n次数据页读取,每一次的数据页读取都是IO操作,这在数据量较大情况下是极度耗时的。

基于嵌套循环连接,Mysql对其进行了横向优化,提出了join buffer的概念。

在这里插入图片描述

利用join buffer可以一次将从驱动表得到的多条记录与被驱动表进行连接,这样便可减少被驱动表的访问次数。这种方式称之为基于块的嵌套连接(Block Nested-Loop Join)。如果join buffer足够大,大到能够容纳从驱动表得到的所有记录,这样对被驱动表的访问只需一次即可。

当然纵向扩展,还有其他连接方式,比如合并连接(Merge Join),哈希连接(Hash Join)。他们各有各的用武之地。

嵌套循环连接适用于,外层循环小,内存循环条件列有序,这种方方式对于CPU和内存的要求较低,但是对于IO要求很高。
合并连接比较适用于连接两端都有序的场景,(有点归并排序的味道),对于CPU和内存要求一般,IO的要求也相对较低。
哈希连接比较适用于数据量大,且没有索引的情况,对于CPU和内存要求都比较高,对于IO的要求可能高也可能低。

基于以上分析,继续按照既定的方法论对该条sql开始进行优化。

sql背景

t_voucher_header表在第一条sql优化时已经介绍过了。

t_voucher_line表数据量在1700w+,id为主键。

已存在的相关索引为:以batch_id列所建立的索引。t_voucher_header和t_voucher_line通过batch_id字段进行关联,一条t_voucher_header记录对应多条t_voucher_line记录。

白盒分析

1.sql中出现了子查询,随之带来的就是临时表的消耗。子查询和连接查询的替代关系是,连接查询一定能用子查询替代,子查询不一定能用连接查询替代。对于该条sql,是否能用连接查询替换掉子查询?

2.对于t_voucher_header的查询,涉及到combine_batch_id和is_delete,连接时利用batch_id(主键)字段。是否有包含combine_batch_id和is_delete的索引,从而减少回表?

3.select出来的字段均来自此b表,对b的回表不可避免。对于t_voucher_line的查询,涉及到is_delete、segment3、id(主键),连接时利用了batch_id字段。连接字段batch_id是否具备能够被索引到的条件?查询是否能有效利用到索引?是否能尽量全覆盖查询字段?

4.驱动表和被驱动表的选择是怎样的?

5.order by排序操作能否利用到索引的顺序,从而避免大数据量的排序耗时?

执行计划解读

带着以上的分析,看一下MySQL的执行计划
在这里插入图片描述

1.t_voucher_header驱动表,t_voucher_line被驱动表。

2.t_voucher_header的查询,Using Index表明查询条件和select字段都能命中索引覆盖,无需回表。

3.t_voucher_line的查询,Using index condition+Using where表明仅用索引进行部分过滤,可能存在多次回表,有可能是慢的原因。

4.t_voucher_header的查询,扫描到了10w+行,并全部返回。后续还有order by操作,Using temporary和Using filesort出现,说明用到临时表进行子查询的存储以及排序,这往往是耗时的操作,极有可能是此sql慢的原因。

5.执行计划中并未出现Using join buffer (Block Nested Loop),说明对于被驱动表的查询可利用索引进行加速的。

瓶颈点确定

到此地步,基本可以模拟出mysql的执行过程,大致如下:

在这里插入图片描述

1.子查询占用了额外的临时表去存储,空间的开辟也是需要时间的。

2.访问被驱动表时的回表成本没有降到最低。

3.大数据量的排序很耗时。

对症下药

1.sql等价改写,子查询变连接查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码SELECT
b.很多字段
FROM
t_voucher_line b
INNER JOIN t_voucher_header a
ON b.batch_id = a.batch_id
WHERE
a.combine_batch_id = 2007044062
AND a.is_delete = 0
AND b.is_delete = 0
AND b.segment3 LIKE '1002%'
AND b.id > 18496282
ORDER BY
b.id ASC
LIMIT 300

2.对于被驱动表t_voucher_line的查询,涉及到的字段比较少,除主键id外,有batch_id、segment3、is_delete字段,而目前已存在的索引idx_batch_id仅仅覆盖了batch_id。对于segment3、is_delete则必须通过回表进行判断。那第一步,我们先通过扩宽索引,把回表的多余消耗给避免了。将batch_id扩充到batch_id、segment3、is_delete。再次看看sql的执行计划
在这里插入图片描述

没问题的,对于被驱动表t_voucher_line的查询,Using where已经不见了,即回表消耗已经降到最低,执行时间也降到了0.6s。

3.解决排序

对于连接查询出来的结果集,其是以驱动表的索引字段天然有序的。

此sql对驱动表的查询是通过combine_batch_id索引,对于combine_batch_id字段,其访问类型为const,而batch_id为驱动表的主键字段,因此,最终得到的结果集必然是依照batch_id有序的。而两表的连接正是通过batch_id。是否能够利用batch_id的有序性来避免排序呢?

由于业务上驱动表和被驱动表的连接字段是一对多的,这就导致我们无法利用驱动表的索引顺序进行排序。
在这里插入图片描述

那么看起来似乎避免不了排序了?

回过头再看下该sql,通过(b.id > 18496282)+(ORDER BY b.id ASC)+(limit 300)可以判定,其排序的目的是为了通过(id>?)进行分页。既然无法利用驱动表batch_id的天然有序,不妨换一种分页方法。即通过(get 全量ids)+(select by id)的方式。最终的sql变为:

1
2
3
4
5
6
7
8
9
10
sql复制代码SELECT
b.id
FROM
t_voucher_line b
INNER JOIN t_voucher_header a ON b.batch_id = a.batch_id
WHERE
a.combine_batch_id = 2007044062
AND a.is_delete = 0
AND b.is_delete = 0
AND b.segment3 LIKE '1002%'

同时对应的sql使用方式也同步修改。最终,sql也是从1s降到了毫秒级,药到病除。

当然此种方式我们需要关注的一个点是max_allowed_packet,它限定了服务端和客户端之间返回数据包的大小。以32MB为例,主键为bigint型,那么一次大约能返回3210241024/8≈419w个id。

小结

该sql的业务场景对应为跑批任务,我们通过(get 全量ids)+(select by id)的方式并无大碍,那如果对应的是前端分页查询呢?如仅需要查询具体某一页的数据时,(get 全量ids)+(select by id)的方式该如何适配?

还是之前的那句话,sql调优=业务场景+sql执行分析,两者缺一不可。脱离任何一方去谈sql调优都是不可取的。

总结

经过对以上两条sql的优化,也总结了一些经验,希望能够帮到大家:

1.sql的优化,如果可能,回表的消耗需要降到最低。

2.在进行连表查询时,如果需要排序操作,尽量按照驱动表的索引字段进行排序,因为其天然有序,可以避免大数据量下,mysql创建临时表去排序,这是非常耗资源的。

3.当遇到慢sql后,执行计划是我们进行优化的切入点,我们能清楚了解整个sql的具体执行过程,就能从中找到瓶颈点,从而对症下药。

4.彼时的调优可以解决彼时的问题,随着业务的发展,数据量会上来,数据分布也会变得更加不均匀,待到那时,可能会再次面临调优。

5.在写sql时可以遵从一些成熟的经验总结,提前避免一些问题。但是系统总是向前发展的,不会说可以用一条sql应对各种场景,调优是个持续的过程。

6.细节很重要,单条sql执行时间过慢的原因可能是多个细节累加造成。10个0.1s就是1s。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试常备,字符串三剑客 String、StringBuffe

发表于 2021-02-21

🎓 尽人事,听天命。博主东南大学研究生在读,热爱健身和篮球,正在为两年后的秋招准备中,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步

🎁 本文已收录于 CS-Wiki(Gitee 官方推荐项目,现已 1.0k+ star),致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习

字符串操作毫无疑问是计算机程序设计中最常见的行为之一,在 Java 大展拳脚的 Web 系统中更是如此。

全文脉络思维导图如下:

  1. 三剑客之首:不可变的 String

概述

Java 没有内置的字符串类型, 而是在标准 Java 类库中提供了一个预定义类 String。每个用双引号括起来的字符串都是 String 类的一个实例:

1
2
java复制代码String e = ""; // 空串
String str = "hello";

看一下 String 的源码,在 Java 8 中,String 内部是使用 char 数组来存储数据的。

1
2
3
4
5
java复制代码public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}

可以看到,String 类是被 final 修饰的,因此 String 类不允许被继承。

而在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

1
2
3
4
5
6
7
8
java复制代码public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;

/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}

不过,无论是 Java 8 还是 Java 9,用来存储数据的 char 或者 byte 数组 value 都一直是被声明为 final 的,这意味着 value 数组初始化之后就不能再引用其它数组了。并且 String 内部没有改变 value 数组的方法,因此我们就说 String 是不可变的。

所谓不可变,就如同数字 3 永远是数字 3 —样,字符串 “hello” 永远包含字符 h、e、1、1 和 o 的代码单元序列, 不能修改其中的任何一个字符。当然, 可以修改字符串变量 str, 让它引用另外一个字符串, 这就如同可以将存放 3 的数值变量改成存放 4 一样。

我们看个例子:

1
2
java复制代码String str = "asdf";
String x = str.toUpperCase();

toUpperCase 用来将字符串全部转为大写字符,进入 toUpperCase 的源码我们发现,这个看起来会修改 String 值的方法,实际上最后是创建了一个全新的 String 对象,而最初的 String 对象则丝毫未动。

空串与 Null

空串 "" 很好理解,就是长度为 0 的字符串。可以调用以下代码检查一个字符串是否为空:

1
2
3
java复制代码if(str.length() == 0){
// todo
}

或者

1
2
3
java复制代码if(str.equals("")){
// todo
}

空串是一个 Java 对象, 有自己的串长度( 0 ) 和内容(空),也就是 value 数组为空。

String 变量还可以存放一个特殊的值, 名为 null,这表示目前没有任何对象与该变量关联。要检查一个字符串是否为 null,可如下判断:

1
2
3
java复制代码if(str == null){
// todo
}

有时要检查一个字符串既不是 null 也不为空串,这种情况下就需要使用以下条件:

1
2
3
java复制代码if(str != null && str.length() != 0){
// todo
}

有同学就会觉得,这么简单的条件判断还用你说?没错,这虽然简单,但仍然有个小坑,就是我们必须首先检查 str 是否为 null,因为如果在一个 null 值上调用方法,编译器会报错。

字符串拼接

上面既然说到 String 是不可变的,我们来看段代码,为什么这里的字符串 a 却发生了改变?

1
2
3
java复制代码String a = "hello";
String b = "world";
a = a + b; // a = "helloworld"

实际上,在使用 + 进行字符串拼接的时候,JVM 是初始化了一个 StringBuilder 来进行拼接的。相当于编译后的代码如下:

1
2
3
4
5
6
java复制代码String a = "hello";
String b = "world";
StringBuilder builder = new StringBuilder();
builder.append(a);
builder.append(b);
a = builder.toString();

关于 StringBuilder 下文会详细讲解,大家现在只需要知道 StringBuilder 是可变的字符串类型就 OK 了。我们看下 builder.toString() 的源码:

显然,toString方法同样是生成了一个新的 String 对象,而不是在旧字符串的内容上做更改,相当于把旧字符串的引用指向的新的String对象。这也就是字符串 a 发生变化的原因。

另外,我们还需要了解一个特性,当将一个字符串与一个非字符串的值进行拼接时,后者被自动转换成字符串(任何一个 Java 对象都可以转换成字符串)。例如:

1
2
java复制代码int age = 13;
String rating = "PG" + age; // rating = "PG13"

这种特性通常用在输出语句中。例如:

1
2
java复制代码int a = 12;
System.out.println("a = " + a);

结合上面这两特性,我们来看个小问题,空串和 null 拼接的结果是啥?

1
2
3
java复制代码String str = null;
str = str + "";
System.out.println(str);

答案是 null 大家应该都能猜出来,但为什么是 null 呢?上文说过,使用 + 进行拼接实际上是会转换为 StringBuilder 使用 append 方法进行拼接,编译后的代码如下:

1
2
3
4
5
6
java复制代码String str = null;
str = str + "";
StringBuilder builder = new StringBuilder();
builder.append(str);
builder.append("");
str = builder.toString();

看下 append 的源码:

可以看出,当传入的字符串是 null 时,会调用 appendNull 方法,而这个方法会返回 null。

检测字符串是否相等

可以使用 equals 方法检测两个字符串是否相等。比如:

1
2
java复制代码String str = "hello";
System.out.println("hello".equals(str)); // true

equals 其实是 Object 类中的一个方法,所有的类都继承于 Object 类。讲解 equals 方法之前,我们先来回顾一下运算符 == 的用法,它存在两种使用情况:

  • 对于基本数据类型来说, == 比较的是值是否相同;
  • 对于引用数据类型来说, == 比较的是内存地址是否相同。

举个例子:

1
2
3
java复制代码String str1 = new String("hello"); 
String str2 = new String("hello");
System.out.println(str1 == str2); // false

对 Java 中数据存储区域仍然不明白的可以先回去看看第一章《万物皆对象》。对于上述代码,str1 和 str2 采用构造函数 new String() 的方式新建了两个不同字符串,以 String str1 = new String("hello"); 为例,new 出来的对象存放在堆内存中,用一个引用 str1 来指向这个对象的地址,而这个对象的引用 str1 存放在栈内存中。str1 和 str2 是两个不同的对象,地址不同,因此 == 比较的结果也就为 false。

而实际上,Object 类中的原始 equals 方法内部调用的还是运算符 ==,判断的是两个对象是否具有相同的引用(地址),和 == 的效果是一样的:

也就是说,如果你新建的类没有覆盖 equals 方法,那么这个方法比较的就是对象的地址。而 String 方法覆盖了 equals 方法,我们来看下源码:

可以看出,String 重写的 equals 方法比较的是对象的内容,而非地址。

总结下 equals()的两种使用情况:

  • 情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过 == 比较这两个对象(比较的是地址)。
  • 情况 2:类覆盖了 equals() 方法。一般来说,我们都覆盖 equals() 方法来判断两个对象的内容是否相等,比如 String 类就是这样做的。当然,你也可以不这样做。

举个例子:

1
2
3
4
5
6
7
java复制代码String a = new String("ab"); // a 为一个字符串引用
String b = new String("ab"); // b 为另一个字符串引用,这俩对象的内容一样

if (a.equals(b)) // true
System.out.println("aEQb");
if (a == b) // false,不是同一个对象,地址不同
System.out.println("a==b");

字符串常量池

字符串 String 既然作为 Java 中的一个类,那么它和其他的对象分配一样,需要耗费高昂的时间与空间代价,作为最基础最常用的数据类型,大量频繁的创建字符串,将会极大程度的影响程序的性能。为此,JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:

  • 为字符串开辟了一个字符串常量池 String Pool,可以理解为缓存区
  • 创建字符串常量时,首先检查字符串常量池中是否存在该字符串
  • 若字符串常量池中存在该字符串,则直接返回该引用实例,无需重新实例化;若不存在,则实例化该字符串并放入池中。

举个例子:

1
2
3
4
java复制代码String str1 = "hello";
String str2 = "hello";

System.out.printl("str1 == str2" : str1 == str2 ) //true

对于上面这段代码,String str1 = "hello";, 编译器首先会在栈中创建一个变量名为 str1 的引用,然后在字符串常量池中查找有没有值为 “hello” 的引用,如果没找到,就在字符串常量池中开辟一个地址存放 “hello” 这个字符串,然后将引用 str1 指向 “hello”。

需要注意的是,字符串常量池的位置在 JDK 1.7 有所变化:

  • JDK 1.7 之前,字符串常量池存在于常量存储(Constant storage)中
  • JDK 1.7 之后,字符串常量池存在于堆内存(Heap)中。


另外,我们还可以使用 String 的 intern() 方法在运行过程中手动的将字符串添加到 String Pool 中。具体过程是这样的:

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串的值相等,那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

看下面这个例子:

1
2
3
4
java复制代码String str1 = new String("hello"); 
String str3 = str1.intern();
String str4 = str1.intern();
System.out.println(str3 == str4); // true

对于 str3 来说,str1.intern() 会先在 String Pool 中查看是否已经存在一个字符串和 str1 的值相等,没有,于是,在 String Pool 中添加了一个新的值和 str1 相等的字符串,并返回这个新字符串的引用。

而对于 str4 来说,str1.intern() 在 String Pool 中找到了一个字符串和 str1 的值相等,于是直接返回这个字符串的引用。因此 s3 和 s4 引用的是同一个字符串,也就是说它们的地址相同,所以 str3 == str4 的结果是 true。

🚩 总结:

  • String str = "i" 的方式,java 虚拟机会自动将其分配到常量池中;
  • String str = new String(“i”) 则会被分到堆内存中。可通过 intern 方法手动加入常量池

new String(“hello”) 创建了几个字符串对象

下面这行代码到底创建了几个字符串对象?仅仅只在堆中创建了一个?

1
java复制代码String str1 = new String("hello");

显然不是。对于 str1 来说,new String("hello") 分两步走:

  • 首先,”hello” 属于字符串字面量,因此编译时期会在 String Pool 中查找有没有值为 “hello” 的引用,如果没找到,就在字符串常量池中开辟地址空间创建一个字符串对象,指向这个 “hello” 字符串字面量;
  • 然后,使用 new 的方式又会在堆中创建一个字符串对象。

因此,使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “hello” 字符串对象)。

  1. 双生子:可变的 StringBuffer 和 StringBuilder

String 字符串拼接问题

有些时候, 需要由较短的字符串构建字符串, 例如, 按键或来自文件中的单词。采用字符串拼接的方式达到此目的效率比较低。由于 String 类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象。既耗时, 又浪费空间。例如:

1
2
java复制代码String s = "Hello";
s += "World";

这段简单的代码其实总共产生了三个字符串,即 "Hello"、"World" 和 "HelloWorld"。”Hello” 和 “World” 作为字符串常量会在 String Pool 中被创建,而拼接操作 + 会 new 一个对象用来存放 “HelloWorld”。

使用 StringBuilder/ StringBuffer 类就可以避免这个问题的发生,毕竟 String 的 + 操作底层都是由 StringBuilder 实现的。StringBuilder 和 StringBuffer 拥有相同的父类:

但是,StringBuilder 不是线程安全的,在多线程环境下使用会出现数据不一致的问题,而 StringBuffer 是线程安全的。这是因为在 StringBuffer 类内,常用的方法都使用了synchronized 关键字进行同步,所以是线程安全的。而 StringBuilder 并没有。这也是运行速度 StringBuilder 大于 StringBuffer 的原因了。因此,如果在单线程下,优先考虑使用 StringBuilder。

初始化操作

StringBuilder 和 StringBuffer 这两个类的 API 是相同的,这里就以 StringBuilder 为例演示其初始化操作。

StringBuiler/StringBuffer 不能像 String 那样直接用字符串赋值,所以也不能那样初始化。它需要通过构造方法来初始化。首先, 构建一个空的字符串构建器:

1
java复制代码StringBuilder builder = new StringBuilder();

当每次需要添加一部分内容时, 就调用 append 方法:

1
2
3
4
5
java复制代码char ch = 'a';
builder.append(ch);

String str = "ert"
builder.append(str);

在需要构建字符串 String 时调用 toString 方法, 就能得到一个 String 对象:

1
java复制代码String mystr = builder.toString(); // aert
  1. String、StringBuffer、StringBuilder 比较

可变性 线程安全
String 不可变 因为不可变,所以是线程安全的
StringBuffer 可变 线程安全的,因为其内部大多数方法都使用 synchronized 进行同步。其效率较低
StringBuilder 可变 不是线程安全的,因为没有使用 synchronized 进行同步,这也是其效率高于 StringBuffer 的原因。单线程下,优先考虑使用 StringBuilder。

关于 synchronized 保证线程安全的问题,我们后续文章再说。

📚 References

  • 《Java 核心技术 - 卷 1 基础知识 - 第 10 版》
  • 《Thinking In Java(Java 编程思想)- 第 4 版》

🎉 关注公众号 | 飞天小牛肉,即时获取更新

  • 博主东南大学研究生在读,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 基础和面试指南的相关原创技术好文。本公众号的目的就是让大家可以快速掌握重点知识,有的放矢。希望大家多多支持哦,和小牛肉一起成长 😃
  • 并推荐个人维护的开源教程类项目: CS-Wiki(Gitee 推荐项目,现已 1.0k+ star), 致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习 ~ 😊

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式:七大原则之里氏替换原则

发表于 2021-02-20

OO中的继承性的思考和说明

  • 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
  • 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障
    问题的提出:在编程中,如何正确的使用继承(里氏替换原则)?

基本介绍

  • 里氏替换原则(Liskov Substitution Principle)在1988年,由麻省理工学院的一位姓里的女士提出。
  • 如果对每个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都代换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类。换句话说,所有引用基类的地方必须透明地使用其子类对象。
  • 在使用继承时,遵循里氏替换原则在子类中尽量不要重写父类得方法
  • 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当得情况下,可以通过聚合,组合,依赖来解决问题

应用实例

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
csharp复制代码package com.braveway.principle.liskov;

public class Liskov {

public static void main(String[] args) {
A a = new A();
System.out.println("11-3="+a.func1(11,3));
System.out.println("1-8="+a.func1(1,8));

System.out.println("------------");
B b = new B();
System.out.println("11-3="+b.func1(11, 3)); //这里本意是求出11-3
System.out.println("1-8="+b.func1(1, 8)); //1-8
System.out.println("11+3+9="+b.func2(11, 3));
}
}

//A 类
class A{
// 返回两个数的差
public int func1(int num1,int num2) {
return num1 - num2;
}
}

//B类继承了A
//增加了一个新功能:完成两个数的相加,然后和9求和
class B extends A{

//这里,重写了A类的方法,可能是无意识
public int func1(int a,int b) {
return a + b;
}

public int func2(int a,int b) {
return func1(a,b)+9;
}
}

解决方法

  • 我们发现原来运行正常的相减共发生了错误。愿意在于类B无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候。
  • 通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,才有依赖,聚合,组合等关系代替

应用实例

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
csharp复制代码package com.braveway.principle.liskov.improve;

public class Liskov {

public static void main(String[] args) {
A a = new A();
System.out.println("11-3="+a.func1(11, 3));
System.out.println("1-8="+a.func1(1, 8));

System.out.println("-------------");

B b = new B();
//因为B类不再继承A类,
System.out.println("11+3="+b.func1(11, 3)); //这里本意是求出11+3
System.out.println("1+8="+b.func1(1, 8)); //1+8
System.out.println("11+3+9="+b.func2(11, 3));

//使用组合仍然可以使用到A类相关方法
System.out.println("11-3="+b.func3(11, 3)); //这里本意是求出11-3
}
}

//创建一个更加基础的基类
class Base{
//把更加基础的方法和成员写到Base类
}

//A 类
class A extends Base{
//返回两个数的差
public int func1(int num1,int num2) {
return num1-num2;
}
}

//B类继承了Base类
class B extends Base{
//如果B需要使用A类的方法,使用组合的关系
private A a = new A();

//这里,重写A类的方法,可能是无意识
public int func1(int a,int b) {
return a+b;
}

public int func2(int a,int b) {
return func1(a,b)+9;
}

//我们仍然想使用A的方法
public int func3(int a,int b) {
return this.a.func1(a, b);
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

GitHub 近两万 Star,无需编码,可一键生成前后端代

发表于 2021-02-20

项目介绍:

JeecgBoot 是一款基于代码生成器的低代码开发平台!前后端分离架构 SpringBoot2.x,SpringCloud,Ant Design&Vue,Mybatis-plus,Shiro,JWT,支持微服务。强大的代码生成器让前后端代码一键生成,实现低代码开发!关于Java项目整理了100+Java项目视频+源码+笔记,地址:100+Java项目视频+源码+笔记

JeecgBoot 引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE), 帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!

JeecgBoot 提供了一系列低代码模块,实现在线开发真正的零代码:Online表单开发、Online报表、报表配置能力、在线图表设计、大屏设计、移动配置能力、表单设计器、在线设计流程、流程自动化配置、插件能力(可插拔)等等!

JEECG宗旨是: 简单功能由OnlineCoding配置实现,做到零代码开发;复杂功能由代码生成器生成进行手工Merge 实现低代码开发,既保证了智能又兼顾灵活;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!

JEECG业务流程: 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案:表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计(松耦合)、并支持任务节点灵活配置,既保证了公司流程的保密性,又减少了开发人员的工作量。

适用项目

Jeecg-Boot低代码开发平台,可以应用在任何J2EE项目的开发中,尤其适合SAAS项目、企业信息管理系统(MIS)、内部办公系统(OA)、企业资源计划系统(ERP)、客户关系管理系统(CRM)等,其半智能手工Merge的开发方式,可以显著提高开发效率70%以上,极大降低开发成本。

技术架构:

开发环境

  • 语言:Java 8
  • IDE(JAVA):IDEA / Eclipse安装lombok插件
  • IDE(前端):WebStorm 或者 IDEA
  • 依赖管理:Maven
  • 数据库:MySQL5.7+ & Oracle 11g & Sqlserver2017
  • 缓存:Redis

后端

  • 基础框架:Spring Boot 2.3.5.RELEASE
  • 微服务框架:Spring Cloud Alibaba 2.2.3.RELEASE
  • 持久层框架:Mybatis-plus 3.4.1
  • 安全框架:Apache Shiro 1.7.0,Jwt 3.11.0
  • 微服务技术栈:Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywarking
  • 数据库连接池:阿里巴巴Druid 1.1.22
  • 缓存框架:redis
  • 日志打印:logback
  • 其他:fastjson,poi,Swagger-ui,quartz, lombok(简化代码)等。

前端

  • Vue 2.6.10,Vuex,Vue Router
  • Axios
  • ant-design-vue
  • webpack,yarn
  • vue-cropper - 头像裁剪组件
  • @antv/g2 - Alipay AntV 数据可视化图表
  • Viser-vue - antv/g2 封装实现
  • eslint,@vue/cli 3.2.1
  • vue-print-nb - 打印

功能模块

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
scss复制代码├─系统管理
│ ├─用户管理
│ ├─角色管理
│ ├─菜单管理
│ ├─权限设置(支持按钮权限、数据权限)
│ ├─表单权限(控制字段禁用、隐藏)
│ ├─部门管理
│ ├─我的部门(二级管理员)
│ └─字典管理
│ └─分类字典
│ └─系统公告
│ └─职务管理
│ └─通讯录
│ └─多租户管理
├─消息中心
│ ├─消息管理
│ ├─模板管理
├─代码生成器(低代码)
│ ├─代码生成器功能(一键生成前后端代码,生成后无需修改直接用,绝对是后端开发福音)
│ ├─代码生成器模板(提供4套模板,分别支持单表和一对多模型,不同风格选择)
│ ├─代码生成器模板(生成代码,自带excel导入导出)
│ ├─查询过滤器(查询逻辑无需编码,系统根据页面配置自动生成)
│ ├─高级查询器(弹窗自动组合查询条件)
│ ├─Excel导入导出工具集成(支持单表,一对多 导入导出)
│ ├─平台移动自适应支持
├─系统监控
│ ├─Gateway路由网关
│ ├─性能扫描监控
│ │ ├─监控 Redis
│ │ ├─Tomcat
│ │ ├─jvm
│ │ ├─服务器信息
│ │ ├─请求追踪
│ │ ├─磁盘监控
│ ├─定时任务
│ ├─系统日志
│ ├─消息中心(支持短信、邮件、微信推送等等)
│ ├─数据日志(记录数据快照,可对比快照,查看数据变更情况)
│ ├─系统通知
│ ├─SQL监控
│ ├─swagger-ui(在线接口文档)
│─报表示例
│ ├─曲线图
│ └─饼状图
│ └─柱状图
│ └─折线图
│ └─面积图
│ └─雷达图
│ └─仪表图
│ └─进度条
│ └─排名列表
│ └─等等
│─大屏模板
│ ├─作战指挥中心大屏
│ └─物流服务中心大屏
│─常用示例
│ ├─自定义组件
│ ├─对象存储(对接阿里云)
│ ├─JVXETable示例(各种复杂ERP布局示例)
│ ├─单表模型例子
│ └─一对多模型例子
│ └─打印例子
│ └─一对多TAB例子
│ └─内嵌table例子
│ └─常用选择组件
│ └─异步树table
│ └─接口模拟测试
│ └─表格合计示例
│ └─异步树列表示例
│ └─一对多JEditable
│ └─JEditable组件示例
│ └─图片拖拽排序
│ └─图片翻页
│ └─图片预览
│ └─PDF预览
│ └─分屏功能
│─封装通用组件
│ ├─行编辑表格JEditableTable
│ └─省略显示组件
│ └─时间控件
│ └─高级查询
│ └─用户选择组件
│ └─报表组件封装
│ └─字典组件
│ └─下拉多选组件
│ └─选人组件
│ └─选部门组件
│ └─通过部门选人组件
│ └─封装曲线、柱状图、饼状图、折线图等等报表的组件(经过封装,使用简单)
│ └─在线code编辑器
│ └─上传文件组件
│ └─验证码组件
│ └─树列表组件
│ └─表单禁用组件
│ └─等等
│─更多页面模板
│ ├─各种高级表单
│ ├─各种列表效果
│ └─结果页面
│ └─异常页面
│ └─个人页面
├─高级功能
│ ├─系统编码规则
│ ├─提供单点登录CAS集成方案
│ ├─提供APP发布方案
│ ├─集成Websocket消息通知机制
├─Online在线开发(低代码)
│ ├─Online在线表单 - 功能已开放
│ ├─Online代码生成器 - 功能已开放
│ ├─Online在线报表 - 功能已开放
│ ├─Online在线图表(暂不开源)
│ ├─Online图表模板配置(暂不开源)
│ ├─Online布局设计(暂不开源)
│ ├─多数据源管理 - 功能已开放
├─积木报表设计器(低代码)
│ ├─打印设计器
│ ├─数据报表设计
│ ├─图形报表设计(支持echart)
│ ├─大屏设计器(暂不开源)
│─流程模块功能 (暂不开源)
│ ├─流程设计器
│ ├─在线表单设计
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─。。。
└─其他模块
└─更多功能开发中。。

微服务整体解决方案(2.4+版本)

1、服务注册和发现 Nacos √

2、统一配置中心 Nacos √

3、路由网关 gateway(三种加载方式) √

4、分布式 http feign √

5、熔断和降级 Sentinel √

6、分布式文件 Minio、阿里OSS √

7、统一权限控制 JWT + Shiro √

8、服务监控 SpringBootAdmin√

9、链路跟踪 Skywarking

10、消息中间件 RabbitMQ √

11、分布式任务 xxl-job √

12、分布式事务 Seata

13、分布式日志 elk + kafa

14、支持 docker-compose、k8s、jenkins

15、CAS 单点登录 √

16、路由限流 √

微服务架构图

Jeecg Boot 产品功能蓝图

image

后台开发环境和依赖

  • java
  • maven
  • jdk8
  • mysql
  • redis
  • 数据库脚本:jeecg-boot/db/jeecgboot-mysql-5.7.sql
  • 默认登录账号: admin/123456

前端开发环境和依赖

  • node
  • yarn
  • webpack
  • eslint
  • @vue/cli 3.2.1
  • ant-design-vue - Ant Design Of Vue 实现
  • vue-cropper - 头像裁剪组件
  • @antv/g2 - Alipay AntV 数据可视化图表
  • Viser-vue - antv/g2 封装实现
  • jeecg-boot-angular 版本

项目下载和运行

拉取项目代码

1
2
bash复制代码git clone https://github.com/zhangdaiscott/jeecg-boot.git
cd jeecg-boot/ant-design-jeecg-vue
  1. 安装node.js
  2. 切换到ant-design-jeecg-vue文件夹下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码# 安装yarn
npm install -g yarn

# 下载依赖
yarn install

# 启动
yarn run serve

# 编译项目
yarn run build

# Lints and fixes files
yarn run lint

系统效果

大屏模板

PC端

在线接口文档

报表

流程

手机端

PAD端

1
arduino复制代码 github地址:https://github.com/zhangdaiscott/jeecg-boot

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Activiti60 驳回到任意节点(跳回驳回节点)

发表于 2021-02-20

最近接手公司的crm系统,针对原先的工作流模块方面进行业务拓展
新增了转办、驳回到任意节点(跳回驳回节点)、跳过相同审批人等业务。

原系统activiti版本是6.0,之前只接触过5.x版本。acitviti6.x版本把pvm包都删了,执行计划的代码也进行了改造

参考了segmentfault.com/a/119000001… 大侠的博客

1:获取历史执行节点

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
scss复制代码/**
* 记录当前操作人为节点审批人 记录审批时长
* @Author: ruiwu
* @param param 转办他人参数
* @return 状态值
* @CreateDate: 2021/2/20 10:17
* @version: 1.0
* @status: done
*/
public List<HisNodeVo> getRunNodes(String procInstId) {
//procInstId 流程实例id
//查询流程运行到哪个节点了
Execution execution = runtimeService.createExecutionQuery()
.processInstanceId(procInstId)
.orderByProcessInstanceId()
.desc()
.list().get(0);
String activityId = execution.getActivityId();


Map<String,String> map= new LinkedHashMap<>();
// 获取流程历史中已执行节点,并按照节点在流程中执行先后顺序排序
List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(procInstId)
//用户任务
.activityType("userTask")
//已经执行的任务节点
.finished()
.orderByHistoricActivityInstanceEndTime()
.asc()
.list();

// 已执行的节点ID集合
if(CollUtil.isNotEmpty(historicActivityInstanceList)){
for (HistoricActivityInstance historicActivityInstance:historicActivityInstanceList){
String hisActId = historicActivityInstance.getActivityId();
if (hisActId.equals(activityId)){
break;
}
if(!map.containsKey(historicActivityInstance.getActivityId())){
map.put(historicActivityInstance.getActivityId(),historicActivityInstance.getActivityName());
}
}
}
if (CollUtil.isNotEmpty(map)){
return map.entrySet()
.stream()
.map(HisNodeVo::new)
.collect(Collectors.toList());
}
return Collections.emptyList();
}

此处引用了lombox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@Data
@ApiModel("历史节点")
@NoArgsConstructor
public class HisNodeVo {
@ApiModelProperty("节点id")
private String activityId;
@ApiModelProperty("节点名")
private String activityName;

public HisNodeVo(Entry<String, String> node) {
this.activityId = node.getKey();
this.activityName = node.getValue();
}
}

实现效果

2:驳回

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
scss复制代码public BaseResult<String> approve(ApproveParam param) {
// 办理人userId
String taskId = param.getTaskId();
// 审核意见
String opinion = param.getOpinion();
// 审批人的姓名+审批意见
Map<String, Object> map = new LinkedHashMap<>();
// 设置审批人
map.put(Constants.APPROVER_KEY,
Optional.ofNullable(SecurityUser.getUserId()).orElse(""));
// 审批 批准
if (param.getApproveStatus() == 0) {
//当前任务
Task currentTask = taskService.createTaskQuery().taskId(taskId).singleResult();
String executionId = currentTask.getExecutionId();
//获取流程定义
Object variable = runtimeService.getVariable(executionId,"turnDownNode");
if (variable != null){
String nodeAssign = runtimeService
.getVariable(executionId,"turnDownNodeAssign").toString();
map.put(Constants.COMMENTS_KEY, "【批准】" + opinion);
//设置流程变量
setVariablesByTaskIdAsMap(taskId, map);
//驳回重新提交
String targetNodeId = String.valueOf(variable);
managementService.executeCommand(new SetFlowNodeAndGoCmd(
currentTask.getId(), targetNodeId,nodeAssign));
runtimeService.removeVariable (executionId, "turnDownNode");
}else{
//审批结果
map.put(Constants.COMMENTS_KEY, "【批准】" + opinion);
map.put("approvalResult", "同意");
boolean mark = StringUtils.isNotBlank(param.getProcInstType()) &&
(param.getProcInstType().contains(Constants.KEY_NEWCLASS) ||
param.getProcInstType().contains(Constants.KEY_PRECLASS));
taskService.setAssignee(taskId,SecurityUser.getUserId());
// 设置流程变量
setVariablesByTaskIdAsMap(taskId, map);
setVariablesByTaskId(taskId, "批准");

//审批结果添加到缓存
cacheService.set(Constants.COMMENTS_KEY + param.getProcInstId(), Constant.ONE + "");
//完成任务
completeMyPersonalTask(taskId);
}
} else {
//驳回
map.put(Constants.COMMENTS_KEY, "【驳回】" + opinion);
map.put("approvalResult", "驳回");
//设置流程变量
setVariablesByTaskIdAsMap(taskId, map);
if (param.getApproveType() == Constant.FAIL){
setVariablesByTaskId(taskId, "驳回");
//审批结果添加到缓存
cacheService.set(Constants.COMMENTS_KEY + param.getProcInstId(), Constant.FAIL + "");
//完成任务
completeMyPersonalTask(taskId);
//记录流程状态-可忽略
this.updateProcessStatus(param.getProcInstId(),Constant.TWO);
}else{
//驳回到任意节点
//历史节点执行人
String targetNodeAssign = this.getRunNodes(param.getProcInstId(), param.getRunNodeId());
log.info("驳回 历史节点执行人= " + targetNodeAssign);
//当前任务
if (param.getTurnType() == Constant.ONE){
Task currentTask = taskService.createTaskQuery().taskId(taskId).singleResult();
String executionId = currentTask.getExecutionId();

//驳回到目标节点携带参数,标记当前节点,然后直接跳转
runtimeService.setVariable (executionId, "turnDownNode",
currentTask.getTaskDefinitionKey());
runtimeService.setVariable (executionId, "turnDownNodeAssign",
currentTask.getAssignee());
}
managementService.executeCommand(new SetFlowNodeAndGoCmd(
param.getTaskId(), param.getRunNodeId() ,targetNodeAssign));
}

//驳回推送钉钉消息-可忽略
ruTaskService.sendRejectProcessNotice(param.getProcInstId());
}

return new BaseResult<String>().ok();
}


/**
* 根据提供节点和执行对象id,进行跳转命令
* @author yzyx
*/
@Slf4j
public class SetFlowNodeAndGoCmd implements Command<Void> {

/**
* 任务id
*/
private String taskId;
/**
* 目标节点
*/
private String runNodeId;
/**
* 目标节点审批人
*/
private String targetNodeAssign;


public SetFlowNodeAndGoCmd(String taskId,String runNodeId,String targetNodeAssign){
this.taskId = taskId;
this.runNodeId = runNodeId;
this.targetNodeAssign = targetNodeAssign;
}

@Override
public Void execute(CommandContext commandContext){
ActivitiEngineAgenda agenda = commandContext.getAgenda();
TaskEntityManager taskEntityManager = commandContext.getTaskEntityManager();
TaskEntity taskEntity = taskEntityManager.findById(taskId);
// 执行实例 id
String executionId = taskEntity.getExecutionId();
log.info("executionId = " + executionId);
String processDefinitionId = taskEntity.getProcessDefinitionId();
ExecutionEntityManager executionEntityManager = commandContext.getExecutionEntityManager();
HistoryManager historyManager = commandContext.getHistoryManager();
// 执行实例对象
ExecutionEntity executionEntity = executionEntityManager.findById(executionId);
Process process = ProcessDefinitionUtil.getProcess(processDefinitionId);
FlowElement flowElement = process.getFlowElement(runNodeId);
if (flowElement == null) {
throw new RuntimeException("目标节点不存在");
}
executionEntity.setVariable("USER_ID",targetNodeAssign);
//去掉无用的变量,不去掉,会导致很多莫名奇妙的问题
executionEntity.removeVariable("loopCounter");
//去掉多实例的变量,如果变量不知道是啥,自己从节点定义里查
executionEntity.removeVariable("cdp_atuser");
//要激活交路径
executionEntity.setActive(true);
// 将历史活动表更新
historyManager.recordActivityEnd(executionEntity, "jump");
// 设置当前流程
executionEntity.setCurrentFlowElement(flowElement);
// 跳转, 触发执行实例运转
agenda.planContinueProcessInCompensation(executionEntity);

// 从runtime 表中删除当前任务
taskEntityManager.delete(taskId);
// 将历史任务表更新, 历史任务标记为完成
historyManager.recordTaskEnd(taskId, "jump");
return null;
}

}

/***
* 查询历史节点执行人
* @param procInstId procInstId
* @param targetNodeId 目标节点
* @return 执行人
*/
private String getRunNodes(String procInstId, String targetNodeId) {
String assignee =SecurityUser.getUserId();

// 获取流程历史中已执行节点,并按照节点在流程中执行先后顺序排序
List<HistoricActivityInstance> instanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(procInstId)
//用户任务
.activityId(targetNodeId)
//已经执行的任务节点
.finished()
.orderByHistoricActivityInstanceEndTime()
.asc()
.list();
if (CollUtil.isNotEmpty(instanceList)){
assignee = instanceList.get(0).getAssignee();
}
Date endTime = instanceList.get(0).getEndTime();
if (CollUtil.isNotEmpty(instanceList)){
for (HistoricActivityInstance activityInstance : instanceList) {
if (activityInstance.getActivityType().equals("olduserTask")){
assignee = activityInstance.getAssignee();
break;
}else{
if (endTime.getTime() < activityInstance.getEndTime().getTime()){
endTime = activityInstance.getEndTime();
assignee = activityInstance.getAssignee();
}
}
}
}

return assignee;
}

@Data
@ApiModel("审批参数")
public class ApproveParam {

@ApiModelProperty("任务Id")
private String taskId;

@ApiModelProperty("流程实例id")
private String procInstId;

@ApiModelProperty("通过-0,拒绝-1")
private int approveStatus;

@ApiModelProperty("0 驳回到发起节点 即原先的驳回重新提交 1 驳回到非发起节点")
private int approveType = 0;

@ApiModelProperty("审核意见")
private String opinion;

@ApiModelProperty(value = "流程类型")
private String procInstType;

@ApiModelProperty("开班申请需-传教师Id")
private String teacherId;

@ApiModelProperty("驳回 指定的运行节点id")
private String runNodeId;

@ApiModelProperty("0 驳回重新流转 1驳回审批通过后返回本节点")
private int turnType;

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务-注册中心与网关 1 环境 2 服务注册与发现(C

发表于 2021-02-19
  1. 环境

中间件 作用 备考
consul 服务注册与发现 简单安装方式
  1. 服务注册与发现(Consul)

docker-compose.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
30
31
32
33
34
35
36
37
38
39
40
41
42
yml复制代码version: '3'

networks:
consul-net:

services:
consul1:
image: consul
container_name: node1
command: agent -server -bootstrap-expect=3 -node=node1 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1
networks:
- consul-net

consul2:
image: consul
container_name: node2
command: agent -server -retry-join=node1 -node=node2 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1
depends_on:
- consul1
networks:
- consul-net

consul3:
image: consul
container_name: node3
command: agent -server -retry-join=node1 -node=node3 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1
depends_on:
- consul1
networks:
- consul-net

consul4:
image: consul
container_name: node4
command: agent -retry-join=node1 -node=ndoe4 -bind=0.0.0.0 -client=0.0.0.0 -datacenter=dc1 -ui
ports:
- 8500:8500
depends_on:
- consul2
- consul3
networks:
- consul-net

2.1 启动服务

1
bash复制代码docker-compose up

2.2 管理界面

点击[管理 ]http://localhost:8500

  1. 注册中心

从上图所示,具体思路如下:

  • 首先来完成需要完成的微服务
  • 其次向consul注册服务
  1. 首先做两个微服务提供者

  • 微服务1
    gitee.com/actual-comb…
  • 微服务2
    gitee.com/actual-comb…

3.1 各自下载后,使用Springboot启动

3.2 Spring Cloud Gateway注册到服务中心(Consul)

启动后,就应该注册到服务中心
application.properties

1
2
3
4
5
6
7
8
9
properties复制代码spring.application.name=spring-cloud-provider-01
server.port=9001
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
#注册到consul的服务名称
spring.cloud.consul.discovery.serviceName=service-provider
#以下两项如果不配置健康检查一定失败
spring.cloud.consul.discovery.prefer-ip-address=true
spring.cloud.consul.discovery.health-check-path=/actuator/health

3.3 查看consul管理终端

下面出现两个微服务

3.4 调用注册中心的微服务

client: gitee.com/actual-comb…

1
2
3
4
5
6
properties复制代码spring.application.name=spring-cloud-consul-client
server.port=9003
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
#设置不需要注册到 consul 中
spring.cloud.consul.discovery.register=false

3.4.1 先看一种调用方式:

TestConsul.java

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
java复制代码package com.cloud.consul.client.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.cloud.consul.client.service.GatewayRemote;
import com.cloud.consul.client.service.ServiceProviderRemote;

@RestController
public class TestConsul {

@Autowired
ServiceProviderRemote remote;

...
@RequestMapping("/TestHello")
public String TestHello(){
String first = remote.Hello("first-SWS");
String second = remote.Hello("second-SWS");
return first + " | " + second;
}

@RequestMapping("/Test")
public String Test(){
return "OK";
}

......
}

ServiceProviderRemote.java

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.cloud.consul.client.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name= "service-provider")
public interface ServiceProviderRemote {

@RequestMapping("/hello")
public String Hello(@RequestParam String name);
}

看结果

从结果来看,以及Client调用代码来看只是连接了注册中心,并不知道服务的IP和端口是多少,就能得到想要的结果了。

3.4.2 通过网关来调用

TestConsul.java

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
java复制代码package com.cloud.consul.client.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.cloud.consul.client.service.GatewayRemote;
import com.cloud.consul.client.service.ServiceProviderRemote;

@RestController
public class TestConsul {
...
@Autowired
GatewayRemote gatewayRemote;

...
@RequestMapping("/Test")
public String Test(){
return "OK";
}

@RequestMapping("/TestGW")
public String TestGW(){
String first = gatewayRemote.Hello("first-SWS");
String second = gatewayRemote.Hello("second-SWS");
return first + " | " + second;
}
}

服务网关:gitee.com/actual-comb…

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
yml复制代码server:
port: 9000
spring:
cloud:
consul:
host: 127.0.0.1
port: 8500
discovery:
register: true
prefer-ip-address: true
health-check-path: /actuator/health
gateway:
routes:
- id: test_route
uri: lb://service-provider
predicates:
- Path=/service-provider/{segment}
filters:
- SetPath=/{segment}
- name: Hystrix
args:
name: service-provider-fallback
fallbackUri: forward:/service-provider-error
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,BAD_REQUEST
default-filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/default-error
application:
name: PC-ApiGateWay

看结果

看网关配置了负载均衡,熔断机制

4 访问网关

4.1 负载均衡

下面地址反复刷新,看下运行结果
http://localhost:9000/service-provider/hello?name=luds

刷新

如果此时把service-provider

再次刷新上面的地址

截至到这里,注册中心就练习完毕了
(这里是不是感觉配置文件是不是挺多的)

参考:github.com/sunweisheng…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring RSocket:基于服务注册发现的 RSock

发表于 2021-02-19

头图.png

作者 | 雷卷
来源|阿里巴巴云原生公众号

RSocket 分布式通讯协议是 Spring Reactive 的核心内容,从 Spring Framework 5.2 开始,RSocket 已经是 Spring 的内置功能,Spring Boot 2.3 也添加了 spring-boot-starter-rsocket,简化了 RSocket 的服务编写和服务调用。RSocket 通讯的核心架构中包含两种模式,分别是 Broker 代理模式和服务直连通讯模式。

Broker 的通讯模式更灵活,如 Alibaba RSocket Broker,采用的是事件驱动模型架构。而目前更多的架构则是面向服务化设计,也就是我们常说的服务注册发现和服务直连通讯的模式,其中最知名的就是 Spring Cloud 技术栈,涉及到配置推送、服务注册发现、服务网关、断流保护等等。在面向服务化的分布式网络通讯中,如 REST API、gRPC 和 Alibaba Dubbo 等,都与 Spring Cloud 有很好地集成,用户基本不用关心服务注册发现和客户端负载均衡这些底层细节,就可以完成非常稳定的分布式网络通讯架构。

RSocket 作为通讯协议的后起之秀,核心是二进制异步化消息通讯,是否也能和 Spring Cloud 技术栈结合,实现服务注册发现、客户端负载均衡,从而更高效地实现面向服务的架构?这篇文章我们就讨论一下 Spring Cloud 和 RSocket 结合实现服务注册发现和负载均衡。

服务注册发现

服务注册发现的原理非常简单,主要涉及三种角色:服务提供方、服务消费者和服务注册中心。典型的架构如下:

1.png

服务提供方,如 RSocket Server,在应用启动后,会向服务注册中心注册应用相关的信息,如应用名称,ip 地址,Web Server 监听端口号等,当然还会包括一些元信息,如服务的分组(group),服务的版本号(version),RSocket 的监听端口号,如果是 WebSocket 通讯,还需要提供 ws 映射路径等,不少开发者会将服务提供方的服务接口列表作为 tags 提交给服务注册中心,方便后续的服务查询和治理。

在本文中,我们采用 Consul 作为服务注册中心,主要是 Consul 比较简单,下载后执行 consul agent -dev 就可以启动对应的服务,当然你可以使用 Docker Compose,配置也非常简单,然后 docker-compose up -d 就可以启动 Consul 服务。

当我们向服务中心注册和查询服务时,都需要有一个应用名称,对应到 Spring Cloud 中,也就是 Spring Boot 对应的 spring.application.name 的值,这里我们称之为应用名称,也就是后续的服务查找都是基于该应用名称进行的。如果你调用 ReactiveDiscoveryClient.getInstances(String serviceId); 查找服务实例列表时,这个 serviceId 参数其实就是 Spring Boot 的应用名称。考虑到服务注册和后续的 RSocket 服务路由的配合以及方便大家理解,这里我们打算设计一个简单的命名规范。

假设你有一个服务应用,功能名称为 calculator,同时提供两个服务: 数学计算器服务(MathCalculatorService)和汇率计算器服务(ExchangeCalculatorService), 那么我们该如何来命名该应用及其对应的服务接口名?

这里我们采用类似 Java package 命名规范,采用域名倒排的方式,如 calculator 应用对应的则为 com-example-calculator 样式,为何是中划线,而不是点?. 在 DNS 解析中作为主机名是非法的,只能作为子域名存在,不能作为主机名,而目前的服务注册中心设计都遵循 DNS 规约,所以我们采用中划线的方式来命名应用。这样采用域名倒排和应用名结合的方式,可以确保应用之间不会重名,另外也方便和 Java Package 名称进行转换,也就是 - 和 . 之间的相互转换。

那么应用包含的服务接口应该如何命名?服务接口全名是由应用名称和 interface 名称组合而成,规则如下:

String serviceFullName = appName.replace("-", ".") + "." + serviceInterfaceName;

例如以下的服务命名都是合乎规范的:

  • com.example.calculator.MathCalculatorService
  • com.example.calculator.ExchangeCalculatorService

而 com.example.calculator.math.MathCalculatorService 则是错误的, 因为在应用名称和接口名称之间多了 math。为何要采用这种命名规范?首先让我们看一下服务消费方是如何调用远程服务的。假设服务消费方拿到一个服务接口,如 com.example.calculator.MathCalculatorService,那么他该如何发起服务调用呢?

  • 首先根据 Service 全面提取处对应的应用名称(appName),如 com.example.calculator.MathCalculatorService 服务对应的 appName 则为 com-example-calculator。如果应用和服务接口之间不存在任何关系,那么想要获取服务接口对应的服务提供方信息,你可能还需要应用名称,这会相对来说比较麻烦。如果接口名称中包含对应的应用信息,则会简单很多,你可以理解为应用是服务全面中的一部分。
  • 调用 ReactiveDiscoveryClient.getInstances(appName) 获取应用名对应的服务实例列表(ServiceInstance),ServiceInstance 对象会包含诸如 IP 地址,Web 端口号、RSocket 监听端口号等其他元信息。
  • 根据 RSocketRequester.Builder.transports(servers) 构建具有负载均衡能力的 RSocketRequester 对象。
  • 使用服务全称和具体功能名称作为路由进行 RSocketRequester 的 API 调用,样例代码如下:

rsocketRequester .route("com.example.calculator.MathCalculatorService.square") .data(number) .retrieveMono(Integer.class)

通过上述的命名规范,我们可以从服务接口全称中提取出应用名,然后和服务注册中心交互查找对应的实例列表,然后建立和服务提供者的连接,最后基于服务名称进行服务调用。该命名规范,基本做到到了最小化的依赖,开发者完全是基于服务接口调用,非常简单。

RSocket 服务编写

有了服务的命名规范和服务注册,编写 RSocket 服务,这个还是非常简单,和编写一个 Spring Bean 没有任何区别。引入 spring-boot-starter-rsocket 依赖,创建一个 Controller 类,添加对应的 MessagMapping annotation 作为基础路由,然后实现功能接口添加功能名称,样例代码如下:

@Controller @MessageMapping("com.example.calculator.MathCalculatorService") public class MathCalculatorController implements MathCalculatorService { @MessageMapping("square") public Mono<Integer> square(Integer input) { System.out.println("received: " + input); return Mono.just(input * input); } }

上述代码看起来好像有点奇怪,既然是服务实现,添加 @Controller 和 @MessageMapping,看起来好像有点不伦不类的。当然这些 annotation 都是一些技术细节体现,你也能看出,RSocket 的服务实现是基于 Spring Message 的,是面向消息化的。这里我们其实只需要添加一个自定义的 @SpringRSocketService annotation 就可以解决这个问题,代码如下:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @MessageMapping() public @interface SpringRSocketService { @AliasFor(annotation = MessageMapping.class) String[] value() default {}; }

回到服务对应的实现代码,我们改为使用 @SpringRSocketService annotation,这样我们的代码就和标准的 RPC 服务接口完全一模一样啦,也便于理解。此外 @SpringRSocketService 和 @RSocketHandler 这两个 Annotation,也方便我们后续做一些 Bean 扫描、IDE 插件辅助等。

@SpringRSocketService("com.example.calculator.MathCalculatorService") public class MathCalculatorImpl implements MathCalculatorService { @RSocketHandler("square") public Mono<Integer> square(Integer input) { System.out.println("received: " + input); return Mono.just(input * input); } }

最后我们添加一下 spring-cloud-starter-consul-discovery 依赖,设置一下 bootstrap.properties,然后在 application.properties 设置一下 RSocket 监听的端口和元信息,我们还将该应用提供的服务接口列表作为 tags 传给服务注册中心,当然这个也是方便我们后续的服务管理。样例如下:

spring.application.name=com-example-calculator spring.cloud.consul.discovery.instance-id=com-example-calculator-${random.uuid} spring.cloud.consul.discovery.prefer-ip-address=true server.port=0 spring.rsocket.server.port=6565 spring.cloud.consul.discovery.metadata.rsocketPort=${spring.rsocket.server.port} spring.cloud.consul.discovery.tags=com.example.calculator.ExchangeCalculatorService,com.example.calculator.MathCalculatorService

RSocket 服务应用启动后,我们在 Consul 控制台就可以看到服务注册上来的信息,截屏如下:

2.png

RSocket 客户端接入

客户端接入稍微有一点复杂,主要是要基于服务接口全面要做一系列相关的操作,但是前面我们已经有了命名规范,所以问题也不大。客户端应用同样会接入服务注册中心,这样我们就可以获得 ReactiveDiscoveryClient bean,接下来就是根据服务接口全名,如 com.example.calculator.ExchangeCalculatorService 构建出具有负载均衡的 RSocketRequester。

原理也非常简单,前面说过,根据服务接口全称,获得其对应的应用名称,然后调用 ReactiveDiscoveryClient.getInstances(appName) 获得服务应用对应的实例列表,接下来将服务实例(ServiceInstance)列表转换为 RSockt 的 LoadbalanceTarget 列表,其实就是 POJO 转换,最后将转 LoadbalanceTarget 列表进行 Flux 封装(如使用 Sink 接口),传递给 RSocketRequester.Builder 就完成具有负载均衡能力的 RSocketRequester 构建,详细的代码细节大家可以参考项目的代码库。

这里要注意的是接下来如何感知服务端实例列表的变化,如应用上下线,服务暂停等。这里我采用一个定时任务方案,定时查询服务对应的地址列表。当然还有其他的机制,如果是标准的 Spring Cloud 服务发现接口,目前是需要客户端轮询的,当然也可以结合 Spring Cloud Bus 或者消息中间件,实现服务端列表变化的监听。如果客户端感知到服务列表的变化,只需要调用 Reactor 的 Sink 接口发送新的列表即可,RSocket Load Balance 在感知到变化后,会自动做出响应,如关闭即将失效的连接、创建新的连接等工作。

在实际的应用之间的相互通讯,会存在一些服务提供方不可用的情况,如服务方突然宕机或者其网络不可用,这就导致了服务应用列表中部分服务不可用,那么 RSocket 这个时候会如何处理?不用担心,RSocket Load Balance 有重试机制,当一个服务调用出现连接等异常,会重新从列表中获取一个连接进行通讯,而那个错误的连接也会标识为可用性为 0,不会再被后续请求所使用。服务列表推送和通讯期间的容错重试机制,这两者保证了分布式通讯的高可用性。

最后让我们启动 client-app,然后从客户端发起一个远程的 RSocket 调用,截屏如下:

3.png

上图中 com-example-calculator 服务应用包括三个实例,服务的调用会在这三个服务实例交替进行(RoundRobin 策略)。

开发体验的一些考量

虽然服务注册和发现、客户端的负载均衡这些都完成啦,调用和容错这些都没有问题,但是还有一些使用体验上的问题,这里我们也阐述一下,让开发体验做的更好。

  1. 基于服务接口通讯

大多数 RPC 通讯都是基于接口的,如 Apache Dubbo、gRPC 等。那么 RSocket 能否做到?答案是其实完全可以。在服务端,我们已经是基于服务接口来实现 RSocket 服务啦,接下来我们只需要在客户端实现基于该接口的调用就可以。对于 Java 开发者来说,这不是大问题,我们只需要基于 Java Proxy 机制构建就可以,而 Proxy 对应的 InvocationHandler 会使用 RSocketRequester 来实现 invoke() 的函数调用。详细的细节请参考应用代码中的的 RSocketRemoteServiceBuilder.java 文件,而且在 client-app module 中也已经包含了解基于接口调用的 bean 实现。

  1. 服务接口函数的单参数问题

使用 RSocketRequester 调用远程接口时,对应的处理函数只能接受单个参数,这个和 gRPC 的设计是类似的,当然也考虑了不同对象序列化框架的支持问题。但是考虑到实际的使用体验,可能会涉及到多参函数的情况,让调用方开发体验更好,那么这个时候该如何处理?其实从 Java 1.8 后,interface 是允许增加 default 函数的,我们可以添加一些体验更友好的 default 函数,而且还不影响服务通讯接口,样例如下:

public interface ExchangeCalculatorService { double exchange(ExchangeRequest request); default double rmbToDollar(double amount) { return exchange(new ExchangeRequest(amount, "CNY", "USD")); } }

通过 interface 的 default method,我们可以为调用方提供给便捷函数,如在网络传输的是字节数组 (byte[]),但是在 default 函数中,我们可以添加 File 对象支持,方便调用方使用。Interface 中的函数 API 负责服务通讯规约,default 函数来提升使用方的体验,这两者的配合,可以非常容易解决函数多参问题,当然 default 函数在一定程度上还可以作为数据验证的前哨来使用。

  1. RSocket Broker 支持

前面我们说到,RSocket 还有一种 Broker 架构,也就是服务提供方是隐藏在 Broker 之后的,请求主要是由 Broker 承接,然后再转发给服务提供方处理,架构样例如下:

4.png

那么基于服务发现的机制负载均衡,能否和 RSocket Broker 模式混合使用呢?如一些长尾或者复杂网络下的应用,可以注册到 RSocket Broker,然后由 Broker 处理请求调用和转发。这个其实也不不复杂,前面我们说到应用和服务接口命名规范,这里我们只需要添加一个应用名前缀就可以解决。假设我们有一个 RSocker Broker 集群,暂且我们称之为 broker0 集群,当然该 broker 集群的实例也都注册到服务注册中心(如 Consul)啦。那么在调用 RSocket Broker 上的服务时,服务名称就被调整为 broker0:com.example.calculator.MathCalculatorService,也就是服务名前添加了 appName: 这样的前缀,这个其实是 URI 的另一种规范形式,我们就可以提取冒号之前的应用名,然后去服务注册中心查询获得应用对应的实例列表。

回到 Broker 互通的场景,我们会向服务注册中心查询 broker0 对应的服务列表,然后和 broker0 集群的实例列表创建连接,这样后续基于该接口的服务调用就会发送给 Broker 进行处理,也就是完成了服务注册发现和 Broker 模式的混合使用的模式。

借助于这种定向指定服务接口和应用间的关联,也方便我们做一些 beta 测试,如你想将 com.example.calculator.MathCalculatorService 的调用导流到 beta 应用,你就可以使用 com-example-calculator-beta1:com.example.calculator.MathCalculatorService 这种方式调用服务,这样服务调用对应的流量就会转发给 com-example-calculator-beta1 对应的实例,起到 beta 测试的效果。

回到最前面说到的规范,如果应用名和服务接口的绑定关系你实在做不到,那么你可以使用这种方式实现服务调用,如 calculator-server:com.example.calculator.math.MathCalculatorService,只是你需要更完整的文档说明,当然这种方式也可以解决之前系统接入到目前的架构上,应用的迁移成本也比较小。如果你之前的面向服务化架构设计也是基于 interface 接口通讯的,那么通过该方式迁移到 RSocket 上完全没有问题,对客户端代码调整也最小。

总结

通过整合服务注册发现,结合一个实际的命名规范,就完成了服务注册发现和 RSocket 路由之间的优雅配合,当然负载均衡也是包含其中啦。对比其他的 RPC 方案,你不需要引入 RPC 自己的服务注册中心,复用 Spring Cloud 的服务注册中心就可以,如 Alibaba Nacos, Consul, Eureka 和 ZooKeeper 等,没有多余的开销和维护成本。如果你想更多了解 RSocket RPC 相关的细节,可以参考 Spring 官方博客 《Easy RPC with RSocket》。

更多详细的代码细节,可以点击链接查看文章对应的代码库!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…718719720…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%