0%

说明

双冒号( :: )运算符在 Java 8 中被用作方法引用(method reference),方法引用是与 lambda 表达式相关的一个重要特性。

语法

Kind Example
对静态方法的引用 ContainingClass::staticMethodName
对一个特定对象的实例方法的引用 containingObject::instanceMethodName
对一个任意对象特定类型的实例方法的引用 ContainingType::methodName
对构造函数的引用 ClassName::new

测试

public class MethodReferencesTest {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("java", "python", "go");
        // 静态方法引用
        list.stream().forEach(MethodReferencesTest::staticMethod);

        // 对象方法引用
        list.stream().forEach(new MethodReferencesTest()::method);

        List<String> list1 = Arrays.asList("2", "3", "4");
        list1.stream().map(Integer::valueOf).forEach(MethodReferencesTest::staticMethod);

    }

    private void method(String a) {
        System.out.println(a);
    }

    public static void staticMethod(String a) {
        System.out.println(a);
    }

    public static void staticMethod(int a) {
        System.out.println("int " + a);
    }
}

说明

springboot默认的打包插件

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

及对应的Dockerfile文件

FROM openjdk:8-jdk-alpine
MAINTAINER happywzy
WORKDIR application
EXPOSE 8080
ADD ./target/*.jar ./app.jar
CMD java  -jar app.jar

打包命令

mvn clean package
docker build -t test:1.0.0 .

这种方式打包 springboot 缺点是每次打包 docker 镜像都是全量上传 ja r包,如果 jar 很大会严重影响拉取镜像的速度.

springboot 分层打包

springboot 分层打包需要 springboot 版本大于 2.3.0.RELEASE.

开启

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <layers>
                    <enabled>true</enabled>
                </layers>
            </configuration>
        </plugin>
    </plugins>
</build>

再次使用 mvn clean package 命令打包,可以使用 java 包分析工具查看

java -Djarmode=layertools -jar target/test-0.0.1-SNAPSHOT.jar list

改造原有的 Dockerfile

FROM openjdk:8-jdk-alpine as builder
WORKDIR application
ADD ./target/*.jar ./app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM openjdk:8-jdk-alpine
MAINTAINER happywzy

WORKDIR application

COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./

EXPOSE 8080

ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

这个 dockerfile 表示先进行一次临时镜像构建标记为 builder,并加载一次全量 jar 包,然后执行 java -Djarmode=layertools -jar app.jar extract 命令将 jar 包分解为分层打包目录,再次构建一个新镜像,按照list 的目录顺序分批将分层目录加载到 docker 镜像中. 再次执行构建命令

docker build -t test:1.0.0 .

docker镜像分析

可以使用 docker inspectdocker historydive 等工具对比全量打包和分层打包的区别.

说明

docker 镜像分析工具有 docker 自带的 docker inspectdocker history,对于具体每一层(layers)组成可以使用 dive 工具. 地址:https://github.com/wagoodman/dive,该工具主要用于探索 docker 镜像层内容以及发现减小 docker 镜像大小的方法.

docker安装

docker pull wagoodman/dive

使用

# windows 下 docker.sock 路径可以使用 -v //var/run/docker.sock:/var/run/docker.sock
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest <dive arguments...>
# 示例
docker run --rm -it -v //var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest test:v1.2

二进制安装

# centos
curl -OL https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.rpm
rpm -i dive_0.9.2_linux_amd64.rpm
# windows
go get github.com/wagoodman/dive

使用

dive <your-image> --source <source>
# or 
dive <source>://<your-image>

配置

在 resources 目录下 (application.yml 同目录)创建 banner.txt 文件, springboot 启动时会自动加载这个文件替换原有的 spring boot banner.

生成banner

其他配置

  • banner.txt 内配置
${AnsiColor.BRIGHT_RED}: 设置控制台中输出内容的颜色
${application.version}: 用来获取MANIFEST.MF文件中的版本号
${application.formatted-version}: 格式化后的${application.version}版本信息
${spring-boot.version}: Spring Boot的版本号
${spring-boot.formatted-version}: 格式化后的${spring-boot.version}版本信息
  • application.yml 配置
spring:
  banner:
    charset: UTF-8
    location: classpath:banner.txt
    image:
      location: classpath:banner.gif
      width: 76
      height:
      margin: 2
      invert: false
  • 关闭 banner
@SpringBootApplication
public class SpringbootBannerApplication {

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(SpringbootBannerApplication.class);
        //Banner.Mode.OFF 关闭
        springApplication.setBannerMode(Banner.Mode.OFF);
        springApplication.run(args);
    }
}

内网穿透工具frp

通过在公网服务器部署 frp 服务端( frps ),内网部署 frp 客户端( frpc ),实现通过公网访问内网的服务.

frp目录说明

├── frpc //客户端
├── frpc_full.ini
├── frpc.ini//客户端配置
├── frps//服务端
├── frps_full.ini
├── frps.ini//服务端配置
└── LICENSE

配置

  • frps.ini
[common]
#服务端口
bind_port = 7000
#监听地址
#bind_addr = 0.0.0.0

#认证token
#token = big_cat
#http服务端口
vhost_http_port = 7080
#https服务端口
vhost_https_port = 70443
  • frpc.ini
[common]
#frps的ip地址
server_addr = 114.114.114.114
#frps的端口
server_port = 7000
#认证token
#token = big_cat

[web-01]
# 远端服务器定义好了 http 服务的端口 这里不需要指定
# 只需要指定 custom/sub_domains 用来做路由即可 
# 如果只是代理一台 则指定服务端ip 使用ip访问即可
type = http
#本地http服务端口
local_port = 8081
#本地http服务地址
local_ip = 127.0.0.1
# 直接使用服务端的公网ip绑定(这样一个frps只能代理一个http客户端)
#custom_domains = 118.118.118.118
# 或者指定域名 可以使用其他域名继续绑定
custom_domains = frp1.com

[web-02]
type = http
local_port = 8082
#本地http服务地址
local_ip = 127.0.0.1
# 直接使用服务端的公网ip绑定(这样一个frps只能代理一个http客户端)
#custom_domains = 118.118.118.118
# 或者指定域名 可以使用其他域名继续绑定
custom_domains = frp2.com

[ssh]
type = tcp
local_port = 22
local_ip = 127.0.0.1
# 在服务端注册端口 服务端将监听 7022 ssh root@118.118.118.118 -p 7022 即可代理到本机 ssh 登录
remote_port = 7022

启动

# 启动服务端服务
nohup ./frps -c ./frps.ini &
# 启动客户端服务
nohup ./frpc -c ./frpc.ini &

注意: http/https 服务是在服务端配置中定义的端口,客户端指定协议后会自动关联,ssh 等使用 tcp 的则是在客户端定义好 remote_port ,服务端开放此端口即可。

说明

在 springboot 中一个接口有多个实现,我们希望通过配置来控制运行时实例化哪个对象,springboot 中 @Conditional 注解可以帮助我们细粒度控制 bean 的实例化。

Spring Bootorg.springframework.boot.autoconfigure.condition 包下定义了以下注解:

注解名 作用
@ConditionalOnJava 基于JVM版本作为判断条件.
@ConditionalOnBean 当容器中有指定的Bean的条件下.
@ConditionalOnClass 当类路径下游指定的类的条件下.
@ConditionalOnExpression 基于SpEL表达式作为判断条件.
@ConditionalOnJndi 在JNDI存在的条件下查找指定的位置.
@ConditionalOnMissingBean 当容器中没有指定Bean的情况下.
@ConditionalOnMissingClass 当类路径下没有指定的类的情况下.
@ConditionalOnNotWebApplication 当前项目不是web项目的条件下.
@ConditionalOnProperty 指定的属性是否有指定的值.
@ConditionalOnResource 类路径是否有指定的值.
@ConditionalOnSingleCandidate 当指定Bean在容器中只有一个,或者虽然有多个但是指定首选的Bean.
@ConditionalOnWebApplication 当前项目是web项目的条件下.

@ConditionalOnExpression

@Component
// user.label 属性等于 user2 创建TestBean
// @ConditionalOnExpression("'${user.label}'.equals('user2')")

// 可以使用 && || 等运算符
// @ConditionalOnExpression("'${user.label}'.equals('user3') && '${xxx}'.equals('xx')")
// @ConditionalOnExpression("'${user.label}'.equals('${xxx}')")

// 当环境变量 user.label=user2 创建TestBean, 注意: 这个环境变量可以通过 application.yml 传入
@ConditionalOnExpression("#{'user2'.equals(environment['user.label'])}")
public class TestBean {
    @PostConstruct
    public void init() {
        System.out.println(111);
    }
}

@ConditionalOnProperty

@Component
// 当有 user.label 这个属性时创建 TestBean
// @ConditionalOnProperty(name = "user.label")
// 当有 user.label 和 xxx 两个属性时创建 TestBean
// @ConditionalOnProperty(name = {"user.label", "xxx"})
// 当有 user.label 这个属性且值为 user2 时创建 TestBean
@ConditionalOnProperty(name = "user.label", havingValue = "user2")
public class TestBean {
    @PostConstruct
    public void init(){
        System.out.println(111);
    }
}

自定义 Conditional

Bean对象

  • 两个 User Bean
@Data
@NoArgsConstructor
public class User1 {
    private String name;
    private int age;
}

@Data
@NoArgsConstructor
public class User2 {
    private String name;
    private int age;
}

方法一

  • 为每类对象创建一个 Conditional,如下为 User1和User2分别创建一个
public class MyConditional1 implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String v = context.getEnvironment().getProperty("user.label");
        if (v.equals("user1")) return true;
        return false;
    }
}

public class MyConditional2 implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String v = context.getEnvironment().getProperty("user.label");
        if (v.equals("user2")) return true;
        return false;
    }
}
  • springboot 实例化 bean
@Bean
@Conditional(MyConditional1.class)
public User init(){
    System.out.println(111);
    return new User();
}

@Bean
@Conditional(MyConditional2.class)
public User2 init2(){
    System.out.println(222);
    return new User2();
}
  • application.propeties
# 如果 label 匹配 bean 的label,该 bean 就会被实例化
user.label=user1

方法二

  • 新增注解给要实例化的对象打上标签
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(MyConditional.class)
public @interface MyConditionalAnnotation {
    String label();
}
  • MyConditional.java
public class MyConditional extends SpringBootCondition {
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(MyConditionalAnnotation.class.getName());
        // 获取注解所传入的 label
        Object value = annotationAttributes.get("label");
        if (value == null) {
//            return new ConditionOutcome(false, "ERROR");
            return ConditionOutcome.noMatch("ERROR");
        }
        // user.label 的值是通过application.propeties传入
        String v = context.getEnvironment().getProperty("user.label");
        if (v.equals(value)){
            // 如果匹配就实例化该 bean
            return ConditionOutcome.match("OK");
        }
        return ConditionOutcome.noMatch("ERROR");
    }
}

注意:代码中通过 application.propeties 传入配置优先级比较高,所以通过context可以获取到,如果通过别的配置文件可能无法获取则需要手动加载。

Properties properties = new Properties();
try {
    properties.load(conditionContext.getResourceLoader().getResource("test.properties").getInputStream());
} catch (IOException ex) {
    ex.printStackTrace();
}
String v = properties.getProperty("user.label");
  • springboot 创建bean
// 通过自定义注解设置 label
@MyConditionalAnnotation(label = "MyUserLabel1")
@Bean
public User1 init(){
    System.out.println(111);
    return new User1();
}

@MyConditionalAnnotation(label = "MyUserLabel2")
@Bean
public User2 init2(){
    System.out.println(222);
    return new User2();
}
  • application.propeties
# 如果 label 匹配 bean 的label,该 bean 就会被实例化
user.label=MyUserLabel2