唯's Blog

笔者是一个热爱编程的 Java 程序员。

0%

项目开发中遇到的一些问题

1. DependencyManagement

  • DependencyManagement 用于在父项目中定义依赖版本,子项目在引入依赖时无需指定版本

  • 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
28
29
30
31
32
33
34
35

<!-- parent -->

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

<version>2.5.1</version>

</dependency>

</dependencies>

</dependencyManagement>



<!-- 子项目 -->



<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

</dependency>

2. 中的 的作用

  • import 在 中起作用
1
2
3



  • 使用 <scope>import</scope> 解决Maven项目单继承问题
  • 这个标签值只能在dependencyManagement标签下使用!

并且仅用于type为”pom”的dependency,其意义为引入该dependency的pom中定义的所有dependency定义。

  • test在本地会引入,不过打包的时候不会被引入,用于maven test结构中代码的正常执行,但不打入包中,以减小线上部署包的尺寸。

  • compile为正常引入模式,表示编译会用它,那么大概率情况,执行也会用它,package会打包进去,至少会把依赖的class打包进去。

  • runtime的作用,是为了本地编译不出错,但package时不打包进去,但按照字面理解,这个玩意儿是要运行时用的啊,那怎么办呢?

    那就在运行环境中,必须有这个包。
    
    比如tomcat中,会有servlet/jsp api,这种api是在运行时使用的,tomcat中自带;
    
    但你项目中编译也会用到,为了本地编译不报错,就要引入,但为了不和线上tomcat发生版本冲突,就要标记为runtime,这样本地编译可用,但打包不引入
    

  • 查看所有线程,top默认显示所有进程第一行

top -H

  • 查看所有存在的线程

ps xH

  • 通过进程id查看线程数量

ps -mp

  • 查看某个进程线程数

ps -T -p pid

  • 查看特定某个进程线程使用内存情况

top -H -p pid

  • 查看进程相关的线程信息

top -Hp pid

JavaAgent 又称 Java 探针,是在 JDK5 引进的一种动态修改字节码的技术。它在 JVM 在加载字节码之前,获取字节码信息 通过字节码转换器修改字节码来实现一些额外的功能。JavaAgent本质可以理解成一个Jar包插件,使用 JVMTI(JVM Tool Interface)完成加载,利用 JVM 提供的 Instrumentatiion 接口来实现字节码加载前、加载后的修改。-javaagent 参数来指定一个 agent jar包,实现启动jvm时加载 agent。利用这一特性可以实现虚拟机级别的 AOP ,实时监控、分析虚拟机,干预虚拟机的执行。

Instrumentation(核心)JDK 官方提供

提供了注入字节码转换器的入口

ClassFileTransformer

字节码转换器接口,想要对字节码的转换需要实现该接口

Java 能做什么?

  • 在 JVM 加载字节码之前对字节码修改

  • 在 JVM 加载字节码之后对字节码修改

  • 获取当前 JVM 加载的所有类信息

  • 获取 JVM 初始化的类信息

  • 修改 native 方法

  • 获取某个对象的大小

  • 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载

  • 将某个jar加入到classpath里供AppClassloard去加载

实现方式

启动 JVM 时加载 Agent(premain)

定义 Agent
1
2
3
4
5
6
7
8
9
10
11

public class MyAgent {

public static void premain(String[] args, Instrumentation instrumentation) {

System.out.println("JVM starting, load agent");

}

}

定义 MANIFEST.MF,放在 resource/ META-INF/
1
2
3
4
5
6
7
8
9
10
11
12
13

Manifest-Version: 1.0

Premain-Class: org.example.agent.MyAgent

Agent-Class: org.example.agent.MyAgent

Can-Redefine-Classes: true

Can-Retransform-Classes: true

Can-Set-Native-Method-Prefix: true

修改 POM 文件
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

<build>

<plugins>

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

</plugin>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-assembly-plugin</artifactId>

<configuration>

<descriptorRefs>

<descriptorRef>jar-with-dependencies</descriptorRef>

</descriptorRefs>

<archive>

<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>

</archive>

</configuration>

</plugin>

</plugins>

</build>

打包 mvn assembly:assembly 进行打包,会生成2个jar包,my-agent-1.0-SNAPSHOT.jar中不带这个项目所依赖的jar包,my-agent-1.0-SNAPSHOT-jar-with-dependencies.jar中包含这个项目所依赖的jar包,后面需要使用my-agent-1.0-SNAPSHOT-jar-with-dependencies.jar这个jar
项目工程引入自定义 agent ,在项目启动时 添加 vm 参数,-javaagent={agentpath}/myagent.jar

JVM 运行时加载 agent(attachemain)

  • 该方法就不是使用 -javaagent 参数引入,需要使用 virtualMachine 下的 attche 方法引入,该类不是 jdk 官方提供,又sun公司提供,需要引入依赖
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

public class Attacher {



public static void main(String[] args)

throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {

// 指定进程号

VirtualMachine vm = VirtualMachine.attach(args[0]);

// 指定agent包位置和要传入的参数

vm.loadAgent(

"/Users/fisher/Documents/code/gitee/myAgent/target/my-agent-1.0-SNAPSHOT-jar-with-dependencies.jar",

"Hello JVM Attach");

vm.detach();

}

}

打包 POM

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

<build>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-assembly-plugin</artifactId>

<version>2.5.5</version>

<configuration>

<archive>

<manifest>

<mainClass>org.example.attach.Attacher</mainClass>

</manifest>

</archive>

<descriptorRefs>

<descriptorRef>jar-with-dependencies</descriptorRef>

</descriptorRefs>

</configuration>

</plugin>

</plugins>

</build>

利用 jps 查看进程号,选择要加载agent的进程号,启动 agent 传入 进程号,完成 agent 的载入

NioEndpoint

设计模式

  • 模板设计模式 ( NioEndpoint extends AbstractEndpoint)

线程模型

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

Acceptor ( getPoller0().register(channel); )



-> poller register() add(NioChannel , SelectionKey) (for in NioChannelList)



-> selector.selectedKeys() 判断 channel 状态 可写 可读

Executor executor = getExecutor();

if (dispatch && executor != null) {

executor.execute(sc);

} else {

sc.run();

}

Tomcat 线程池

重写了 LinkedBlockingQueue ,TaskQueue

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

class TaskQueue extends LinkedBlockingQueue{

@Override

public boolean offer(Runnable o) {

//we can't do any checks

if (parent==null) {

return super.offer(o);

}

//we are maxed out on threads, simply queue the object

if (parent.getPoolSize() == parent.getMaximumPoolSize()) {

return super.offer(o);

}

//we have idle threads, just add it to the queue

if (parent.getSubmittedCount()<=(parent.getPoolSize())) {

return super.offer(o);

}

//if we have less threads than maximum force creation of a new thread

if (parent.getPoolSize()<parent.getMaximumPoolSize()) {

return false;

}

//if we reached here, we need to add it to the queue

return super.offer(o);

}

}

重写 offer 方法,没有达到线程池的最大线程数,则返回false ,继续创建线程,如果大于则加入阻塞队列,队列满了 则抛出 拒绝策略,如果 已提交的任务数小于当前线程池中的线程数 则加入队列(不创建新的线程)

Tomcat 线程池扩展了原生的 ThreadPoolExecutor,通过重写 execute 方法实现了自己的任务处理逻辑:

● 前 corePoolSize 个任务时,来一个任务就创建一个新线程。

● 还有任务提交,直接放到队列,队列满了,但是没有达到最大线程池数则创建临时线程救火。

● 线程总线数达到 maximumPoolSize ,继续尝试把任务放到队列中。如果队列也满了,插入任务失败,才执行拒绝策略。

最大的差别在于 Tomcat 在线程总数达到最大数时,不是立即执行拒绝策略,而是再尝试向任务队列添加任务,添加失败后再执行拒绝策略。

泛型类

在书写泛型类时,通常做以下的约定:

  • E表示Element,通常用在集合中;

  • ID用于表示对象的唯一标识符类型

  • T表示Type(类型),通常指代类;

  • K表示Key(键),通常用于Map中;

  • V表示Value(值),通常用于Map中,与K结对出现;

  • N表示Number,通常用于表示数值类型;

  • ?表示不确定的Java类型;

  • X用于表示异常;

  • U,S表示任意的类型。

Servlet容器的扩展机制

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

/**

生命周期由 Servlet 容器管理

Servlet 3.0 特性,SPI 机制提供 ServletContainerInitializer 扩展点



*/

SpringServletContainerInitializer implements ServletContainerInitializer

-> WebApplicationInitializer (用于自定义 提供扩展。SpringBoot web项目想要以 war 包方式部署需要用到这个机制、SpringBootServletInitializer)

/**

生命周期由Spring容器管理,Spring web容器(ServletWebServerApplicationContext)管理,

*/

ServletContextInitializer



image-20221211135720576

1
2
3
4
5
6
7
8
9
10
11
12
13

public class TomcatStartSpringBoot extends SpringBootServletInitializer {

@Override

protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {

return builder.sources(Application.class);

}

}

创建 ServletWebServerApplicationContext

  • AbstractApplicationContext.refresh() -> onRefresh() -> createWebServer() -> selfInitialize() 这里会读取 beanFactory 自动配置配类 DispatcherServletAutoConfiguration -> DispatcherServletRegistrationBean (底层实现接口ServletContextInitializer)-> 执行 onStartup() -> 导入DispacherServlet

  • 在第一次访问到 Servlet 时,调用 init() -> 加载HandlerMapping、HandlerAdpter

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

onRefresh() -> initStrategies()



initMultipartResolver(context);

initLocaleResolver(context);

initThemeResolver(context);

initHandlerMappings(context);

initHandlerAdapters(context);

initHandlerExceptionResolvers(context);

initRequestToViewNameTranslator(context);

initViewResolvers(context);

initFlashMapManager(context);



  • tip

  • Spring MVC 的配置(默认值):

  • 
    spring:
    
      mvc:
    
        servlet:
    
          load-on-startup: 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



    *



    * servlet init 方法的调用时机,依赖 load-on-startup的配置信息



    * init 方法是随 Servlet 实例化而被调用的,因为 load-on-startup 就是用来设置 Servlet 实例化时间的。



    * 因此,init 方法执行的时刻有两种:



    * (1) load-on-startup 的值大于等于0,则伴随 Servlet 实例化后执行。



    * (2) load-on-startup 的值小于0 或者 不配置, 则在第一次 Servlet 请求的时候执行。



    ### tips



    ###### WebMvcAutoConfiguration



    * 静态内部类 `EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration` 用来 注册 DispatcherServlet 各个handler (RequestMappingHandlerMapping)

    ```java

    @Bean

    @Primary

    @Override

    public RequestMappingHandlerMapping requestMappingHandlerMapping(

    @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,

    @Qualifier("mvcConversionService") FormattingConversionService conversionService,

    @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

    // Must be @Primary for MvcUriComponentsBuilder to work

    return super.requestMappingHandlerMapping(contentNegotiationManager, conversionService,

    resourceUrlProvider);

    }

  • Controller 在注入 Bean 以后,什么时候放入 RequestMappingHandlerMapping.mappingRegistry

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

    /**

    RequestMappingHandlerMapping extends AbstractHanlerMapping

    AbstractHandlerMethodMapping implements InitializingBean

    在 Bean 生命周期 afterPropertiesSet() 方法 注入 Controller 中的 Method

    重写 RequestMappingHandlerMapping 中的 isHandler 方法即可注入其他注解或者类型的Bean

    **/

    @Override

    protected boolean isHandler(Class<?> beanType) {

    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||

    AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));

    }

  • 创建 DispatcherServlet (DispatcherServletConfiguration)

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

@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)

public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {

DispatcherServlet dispatcherServlet = new DispatcherServlet();

dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());

dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());

dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());

dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());

dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());

return dispatcherServlet;

}



  • 通过 DispatcherServletRegistrationBean 注入 DispatcherServlet ,原理: RegistrationBean (注册 servlet 3.0+) 实现了 ServletContextInitializer 接口 servletContext 初始化生命周期方法

Service Mesh

  • 在 Service Mesh 架构出来之前,传统的微服务架构存在底层服务交互与业务逻辑耦合在一起,依赖冲突,考虑:透明升级,多语言服务通信等问题。

  • Service Mesh 通过部署独立的 Sidecar组件来拦截所有的出口与入口流量,集成丰富的负载均衡策略,开发本身只关注业务逻辑,开发更纯粹。

  • Service Mesh 解决了什么问题?

透明升级

多语言

依赖冲突

流量治理

  • 经典 Mesh 在实施层面也面临成本过高的问题

    1. 需要运维控制面(Control Panel)

    2. 需要运维 Sidecar

    3. 需要考虑如何从原有 SDK 迁移到 Sidecar

    4. 需要考虑引入 Sidecar 后整个链路的性能损耗

Proxyless Mesh

  • 为了解决 Service Mesh 的问题引入全新的 Proxyless Mesh 架构,Proxyless Mesh 指的是没有 Sidecar 部署,由 Dubbo SDK 直接与控制面交互。

SpringBoot扩展点 ImportBeanDefinitionRegistrar

主要用于框架与Spring 对接时注入核心类

一般配合 @Import 使用,用于在 Spring 启动时创建 BeanDefinition 并导入 BeanDefinition

案例:

  • Mybatis @MapperScan 导入 MapperScannerConfigurer 用于 (ClassPathMapperScanner) 扫描 @Mapper 利用 (BeanDefinitionRegistryPostProcesso) 生成 Mapper接口代理类 (MapperFactoryBean )]
1
2
3
4
5
6
7
有两个渠道会注入 MapperScannerConfigurer  
1. @MapperScan 注解中的 @Import
2. mybatis-spring-boot-autoconfigure 自动配置
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
MapperScannerRegistrarNotFoundConfiguration
注入条件是当前不存在 MapperScannerConfigurer,意思说 @MapperScan 配置优先级最高。也符合自动配置中约定大于配置的思想。

  • TkMybatis
  • Nacos
  • Apollo @EnableApolloConfig -> @Import(ApolloConfigRegistrar.class) -> ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar -> 注入核心控制类
  • OpenFegin

记录一些系统设计相关场景

系统设计先从四个方面考虑(4S):

  1. 场景功能(Scene)

  2. 定义服务(Service)

  3. 存储(Storage)

  4. 升级(Scale)