Java 版本:11

参考:

实验代码:https://github.com/rectcircle/explore-spring-boot-with-jshell

Jshell 简介

自 Java 9 起,JDK 包含了一个 REPL 工具 jshell,功能类似 pythonscala 命令

因此,就可以通过 Jshell 做一些 Java 的 实验了。

Jshell 结合 Spring Boot

在 Python Django 中,Django 提供了 python manage.py shell 命令用于加载 Django 框架,并启动 Python shell,可以很方便的调试 Django 程序。

理论上 Jshell 也可以实现类似的特性

思路

  • 将 Spring Boot 项目编译为 fat jar
  • 解压下来
  • 启动 Jshell 时,将 CLASSPATH 环境变量指定为 target/"BOOT-INF/classes:BOOT-INF/lib/*"
  • 引入 常用 组件包 和 Application 类
  • 调用 main 方法启动
  • 通过 我们实现的 SpringBeanUtils 类拿到 ApplicationContext,这样就可以调试 Spring 管理 的 bean 了

实现

创建一个测试的 Spring Boot Web 项目。

添加 测试的 Controller Service 等

src
└── main
    └── java
        └── cn
            └── rectcircle
                └── explore
                    └── springbootwithjshell
                        ├── ExploreSpringBootWithJshellApplication.java
                        ├── controller
                        │   └── HelloController.java
                        ├── dto
                        │   └── RespDTO.java
                        ├── service
                        │   ├── HelloService.java
                        │   └── HelloServiceImpl.java
                        └── util
                            └── SpringBeanUtils.java

添加 SpringBeanUtils

package cn.rectcircle.explore.springbootwithjshell.util;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 用于向不受 Spring 管理的 Bean 访问 Bean 工厂(谨慎使用)
 * @author rectcircle
 */
@Component
public class SpringBeanUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringBeanUtils.applicationContext = applicationContext;
    }

    public static ApplicationContext getInstance() {
        return applicationContext;
    }

}

maven 编译 和 解压jar包

./mvnw clean package -DskipTests
cd target
jar xf $MODULE_NAME-*.jar

指定 CLASSPATH 并执行 Jshell

CLASSPATH="BOOT-INF/classes:BOOT-INF/lib/*" jshell

测试Jshell

// import main
import cn.rectcircle.explore.springbootwithjshell.*

// import 组件
import cn.rectcircle.explore.springbootwithjshell.controller.*
import cn.rectcircle.explore.springbootwithjshell.dto.*
import cn.rectcircle.explore.springbootwithjshell.service.*
import cn.rectcircle.explore.springbootwithjshell.util.*

// 启动 Spring App
ExploreSpringBootWithJshellApplication.main(new String[]{})

// 注入 spring 上下文
var springContext = SpringBeanUtils.getInstance()

// 调试 bean
var hs = springContext.getBean(HelloService.class);
hs.sayHello("test")

curl 测试

curl "http://127.0.0.1:8080/hello?word=world"

脚本化

  • 将上边大段 import 写入 *.jsh 文件。
  • 通过 *.sh 文件,通过控制编译解压,导入环境变量的配置,实现一键进入。

cli/jsh/base.jsh

// import main
import cn.rectcircle.explore.springbootwithjshell.*

// import 组件
import cn.rectcircle.explore.springbootwithjshell.controller.*
import cn.rectcircle.explore.springbootwithjshell.dto.*
import cn.rectcircle.explore.springbootwithjshell.service.*
import cn.rectcircle.explore.springbootwithjshell.util.*

void help() {
    System.out.println();
    System.out.println("该脚本预执行了大量代码,导入了很多类,参见:");
    System.out.println("        cli/jsh/base.jsh");
    System.out.println("        cli/jsh/explore-spring-boot-with-jshell.jsh");
    System.out.println();
    System.out.println("同时,上下文中存在注入了 springContext: ApplicationContext 等变量");
    System.out.println();
    System.out.println("常见命令如下:");
    System.out.println("        /?  查看命令帮助");
    System.out.println("        /vars  查看定义变量");
    System.out.println("        /list  查看历史命令");
    System.out.println();
}

cli/jsh/explore-spring-boot-with-jshell.jsh

// 启动 Spring App
ExploreSpringBootWithJshellApplication.main(new String[]{})

// 注入 spring 上下文
var springContext = SpringBeanUtils.getInstance()

// 打印帮助提示
help()

cli/spring-jshell.sh

#!/usr/bin/env bash

set -e

NEED_COMPILE="0"
SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-"default"}
MODULE_NAME="explore-spring-boot-with-jshell"

while getopts ":cp:h" opt; do #不打印错误信息, -a -c需要参数 -b 不需要传参  
  case $opt in
    c)
      NEED_COMPILE="1"
      ;;
    p)
      SPRING_PROFILES_ACTIVE=$OPTARG
      ;;
    h)
      echo "Usage: ./cli/spring-jshell -c -p <spring.profiles.active>"
      echo "    Flag:"
      echo "          -c 是否重新编译"
      echo "          -h 显示帮助"
      echo "    Option:"
      echo "          -p 指定 Spring 激活的配置名,例如 dev"
      exit 0
      ;;
    :)
      exit 1
      ;;
    ?)
      echo "Invalid option: -$OPTARG"
      exit 1
      ;;
  esac
done

echo "Parameter: "
echo NEED_COMPILE=$NEED_COMPILE
echo SPRING_PROFILES_ACTIVE=$SPRING_PROFILES_ACTIVE
echo

if [ z"$NEED_COMPILE" = z"1" ]; then
    ./mvnw clean package -DskipTests
fi

BASE_JSH_PATH=$(pwd)/cli/jsh/base.jsh
MODULE_JSH_PATH=$(pwd)/cli/jsh/$MODULE_NAME.jsh

cd target

if [ ! -d "BOOT-INF" ]; then
    jar xf $MODULE_NAME-*.jar
fi

SPRING_PROFILES_ACTIVE=$SPRING_PROFILES_ACTIVE CLASSPATH="BOOT-INF/classes:BOOT-INF/lib/*" jshell $BASE_JSH_PATH $MODULE_JSH_PATH

使用上

# 查看帮助
./cli/spring-jshell.sh -h
# 编译并进入jshell
./cli/spring-jshell.sh -c
# 不编译直接进入jshell
./cli/spring-jshell.sh