全栈教程:Spring Boot 和 Vue.js 入门

在本教程中,你将创建一个 CoffeeBot 应用程序。该应用程序就像机器人咖啡机的控制器。遗憾的是,它实际上不会为你提供咖啡,但它将演示大量有用的编程技术。该应用程序将有一个 Vue.js 客户端和一个 Spring Boot 资源服务器。它将使用 JHipster 进行引导,节省大量时间并演示一些最佳实践,包括在客户端和服务器中自动配置端到端 JWT 身份验证。你还将使用 Split 来展示如何分别使用 Split 的 Javascript SDK 和 Java SDK 在运行时动态地使用功能标志来管理前端和后端的功能集。
该应用程序本身提供饮料。服务器维护着一份饮料清单,主要是咖啡,附有尺寸、价格和名称。为简单起见,饮料列表将使用内存中的开发数据库进行保存,但如果需要,可以轻松地重新配置以实现持久性。客户端从服务器请求饮料列表,如果用户通过身份验证,则传递任何身份验证数据。

客户端接收此列表并将饮料列表显示为按钮列表。第二次分割纯粹与客户有关。添加了一项新功能:能够在饮料中添加奶油。正如你可以想象的那样,考虑到人们对咖啡中奶油的喜爱程度,虚拟骚乱已经开始要求此功能。人们在叫嚷。但经理们希望在广泛发布之前确保奶油功能正常工作(CoffeeBot 有时会失灵)。因此,根据经过身份验证的用户,添加奶油的能力被切换。你可以在这个链接:

看到如何使用 Split 的 Javascript SDK 来控制客户端的奶油功能,以及如何使用 Split 的 Java SDK 来控制服务器的饮料列表

全栈教程:Spring Boot 和 Vue.js 入门_第1张图片

Java + Vue.js 教程依赖项

Java:我在本教程中使用了 Java 12。你可以访问AdaptOpenJdk 网站下载并安装 Java 。或者你可以使用SDKMAN甚至Homebrew等版本管理器。

Node:按照 Node网站上的说明安装 Node 。本教程是使用 Node 12.14.0 编写的。

JHipster:一旦安装了 Java 和 Node,就可以安装 JHipster。按照其网站上的说明进行操作(如果出现问题,有助于排除故障)或只需运行此命令:npm install -g [email protected]使用npm.

拆分:如果你还没有免费的拆分帐户,请注册一个。这就是实现功能标志的方式。

使用 JHipster 引导你的 Spring Boot 应用程序

要创建示例 CoffeBot 应用程序,你将使用 JHipster。正如其网站上所述,“JHipster 是一个快速生成、开发和部署现代 Web 应用程序和微服务架构的开发平台。” 它允许你快速启动具有各种前端和服务器配置的基于 Java 的项目。本教程将使用 Vue.js。

JHipster 的优点之一是它为你创建了一个包含 Java 服务器应用程序和 Vue.js 前端应用程序的组合项目。它还包括将创建数据模型实体和 Web 服务控制器的生成器。它做了很多事情并创建了很多文件。如果你对这些平台相对陌生,那么项目结构可能看起来有点令人难以承受,但他们在网站上记录了所有内容,做得很好。他们布置的项目遵循当前的最佳实践,因此它是一个很好的学习工具。

打开 shell 并为你的项目创建一个根目录,例如 CoffeeBotApp. 导航到该目录。你将在此处生成项目文件。

通常,当你运行 JHipster 时,它会询问你许多有关你正在引导的项目的问题。但是,为了简化事情,你将使用此.yo-rc.json文件来预先配置项目,从而绕过询问。
在根项目目录中,创建一个.yo-rc.json包含以下内容的文件。此配置的一些亮点是:

applicationType:整体应用程序:典型的标准应用程序(本质上不是微服务)

baseName : coffeebot– 应用程序的名称

packageName : com.split.coffeebot– 基础 Java 包

authenticationType : jwt– JSON Web 令牌身份验证

devDatabaseType : h2Memory– 开发数据库使用内存中的 H2 数据库,该数据库不会跨会话持久化

clientFramework : vue– 使用Vue.js作为前端客户端框架

SkipFakeData : true– JHipster 默认情况下会为数据模型生成一组随机的假数据,我们希望在本教程中跳过这些数据

有很多选择。请参阅文档以深入了解这一点。

{
  "generator-jhipster": {
    "promptValues": {
      "packageName": "com.split.coffeebot"
    },
    "jhipsterVersion": "6.9.0",
    "applicationType": "monolith",
    "baseName": "coffeebot",
    "packageName": "com.split.coffeebot",
    "packageFolder": "com/split/coffeebot",
    "serverPort": "8080",
    "authenticationType": "jwt",
    "cacheProvider": "ehcache",
    "enableHibernateCache": true,
    "websocket": false,
    "databaseType": "sql",
    "devDatabaseType": "h2Memory",
    "prodDatabaseType": "mysql",
    "searchEngine": false,
    "messageBroker": false,
    "serviceDiscoveryType": false,
    "buildTool": "gradle",
    "enableSwaggerCodegen": false,
    "jwtSecretKey": "ZDg4ZjkzMDJkNWQ4YWJlMjUxOTY3YjE1MDNjY2ZkMzJjYWQwYjJiOTkyMWQ3YTE5ZTgwNWY3Y2E1ZDg0OWViZjM0Nzg1NDE3MjNlMGY1MDBkNTg4YWU1MmZmNTU1ZGEzOTJiMTVlMWZjZDc5NDUyMTlmZmRmYTU0NDJjMDdiODA=",
    "embeddableLaunchScript": false,
    "useSass": true,
    "clientPackageManager": "npm",
    "clientFramework": "vue",
    "clientTheme": "none",
    "clientThemeVariant": "",
    "creationTimestamp": 1601147759112,
    "testFrameworks": [],
    "jhiPrefix": "jhi",
    "entitySuffix": "",
    "dtoSuffix": "DTO",
    "otherModules": [
      {
        "name": "generator-jhipster-vuejs",
        "version": "1.8.0"
      }
    ],
    "enableTranslation": false,
    "blueprints": [
      {
        "name": "generator-jhipster-vuejs",
        "version": "1.8.0"
      }
    ],
    "skipFakeData": true
  }
}

通过运行以下命令(在包含该文件的根项目目录中.yo-rc.json)创建入门应用程序。

jhipster

当 JHipster 创建项目时,你将看到大量控制台输出。它应该以以下行结束。

INFO! Congratulations, JHipster execution is complete!

代码语言: Swift (斯威夫特)
JHipster 已经创建了一个 Git 存储库并进行了初始提交。此时,你可以通过打开两个 shell(一个用于客户端,一个用于服务器)并运行以下命令来运行入门应用程序。

Spring Boot 服务器:

./gradlew

Vue.js 客户端:

npm start

生成 Spring Boot 数据模型

现在你想要使用 JHipster 生成数据模型或实体。它们定义了将存储在数据库中并由 REST 服务提供服务的数据结构。当你使用 JHipster 的生成器创建实体时,JHipster 和 Spring 会为你完成许多出色的幕后工作。它创建表示数据结构的 Java 类,并使用允许将数据保存到数据库的 JPA 注释进行注释。它还创建一个实现创建、读取、更新和删除 (CRUD) 功能的资源文件,该文件会自动受到所选身份验证方案(在我们的示例中为 JWT)的保护。

在前端,生成器创建必要的文件,允许你与实体的资源服务器进行交互,以及前端文件来创建、更新和检查持久数据实体(要访问它,你必须以管理员用户身份登录)。

在项目根目录中,创建一个新文件:entities.jdl. 该文件定义了一种具有四个属性的实体类型,以及该实体中使用的枚举类型。

enum DrinkSize {
    Small,
    Medium,
    Large,
    XLarge,
    XXLarge
}

entity Drink {
    name String required,
    size DrinkSize required,
    caffeineMilligrams Integer required,
    priceDollars Integer required,
}

通过运行生成实体文件:

jhipster import-jdl entities.jdl

当它询问你是否覆盖文件时,只需键入ato overwrite this and all others。

现在是运行入门应用程序并探索引导功能的好时机。请记住,你需要运行两个不同的进程。

Spring Boot Java 服务器`

./gradlew

Vue.js 客户端(你可能需要等待一分钟左右服务器才能完成运行):

npm start

客户端应用程序应自动打开。如果没有,请打开 http://localhost:9000

使用默认凭据以管理员用户身份登录admin:admin。查看“管理”菜单下的所有功能。另请查看“实体”菜单。你可以在此处查看、添加和更新你创建的实体。在我们的例子中,这是Drink实体,它有四个属性:name、size、caffeine mgs和Price Dollars。
Vue.js 客户端使用 TypeScript 和类组件,并将模板和组件声明拆分为两个单独的文件。如果你习惯于更传统的.vue单文件结构,一开始这可能看起来有点奇怪,但大多数差异都是不言自明的。如果你需要帮助,请查看文档中的官方页面以获取更多信息。

将功能标志添加到你的 Spring Boot Java 服务器

你将使用 Split 在客户端和服务器上实现功能标志。你应该已经注册了一个免费帐户(如果没有,请立即注册)。目前,你要将 Java Split SDK 集成到 Spring Boot 应用程序中。我将引导你完成此过程,但如果你需要更多信息或遇到麻烦,请查看他们的 Java SDK 文档。

首先,将Split依赖添加到build.gradle文件中(在项目根目录中)。该build.gradle文件包含大量内容。只需在末尾附近添加以下行,就在开始的注释行上方//jhipster-needle-gradle-dependency。

dependencies {
    ....
    compile 'io.split.client:java-client:4.0.1'
    //jhipster-needle-gradle-dependency - JHipster will add additional dependencies here
}

你将需要你的 Split API 密钥。打开你的拆分仪表板。通过转到仪表板左上角的方形工作区图标(可能显示默认为DE)找到 API 密钥,单击它,然后单击Admin Settings。单击左侧面板中“工作区设置”下的“API 密钥”。
你将看到已创建四个 API 密钥,其中两个用于生产,两个用于暂存。服务器端 SDK 使用和客户端 Javascript 使用有不同的密钥。SDK和staging-default密钥是你稍后需要的。

全栈教程:Spring Boot 和 Vue.js 入门_第2张图片

将 API 密钥添加到配置文件的末尾application.yml。
src/main/resources/application.yml

#application:
split:
    api-key: 代码语言: 小黄瓜 (gherkin )

我只是指出,在这里你将其添加到全局配置文件中,但在更实际的用例中,你可能会使用两个不同的 API 密钥,一个用于暂存和生产,分别将它们添加到 和application-dev.yml文件application-prod.yml中。

创建一个名为 的 Java 文件,SplitConfig.java该文件将在 Spring Boot 应用程序中配置 Split 客户端。它创建了一个可用于依赖注入的 Spring Bean,并且由于 Bean 的默认行为是创建一个单例实例,因此这与 Split 自己的指导一致,建议只创建一个客户端实例。

src/main/java/com/split/coffeebot/config/SplitConfig.java

@Configurationpublic class SplitConfig {
    @Value("#{ @environment['split.api-key'] }")
    private String splitApiKey;
    @Bean
    public SplitClient splitClient() throws Exception {
        SplitClientConfig config = SplitClientConfig.builder()
                .setBlockUntilReadyTimeout(1000)
                .enableDebug()
                .build();
        SplitFactory splitFactory = SplitFactoryBuilder.build(splitApiKey, config);
        SplitClient client = splitFactory.client();
        client.blockUntilReady();
        return client;
    }
}

你还需要向DrinkRepository. 它JPARepository为你提供了相当多的功能,无需任何自定义,但在这个应用程序中,你将需要一个自定义方法,该方法允许你从标准方法中排除一些饮料findAll()。该方法findByNameNotIn()是一个JPA查询方法,其语法和实现由Spring Boot提供。你所要做的就是定义方法以使其可供使用。有关更多信息,请参阅Spring Data JPA 查询方法的文档。

src/main/java/com/split/coffeebot/repository/DrinkRepository.java

@SuppressWarnings("unused")  @Repository  public interface DrinkRepository extends JpaRepository {  
    List findByNameNotIn(Collection names);  
}

现在创建一个CoffeeBotResource.java文件,其中包含 CoffeeBot 应用程序的业务逻辑和 REST 端点。

src/main/java/com/split/coffeebot/web/rest/CoffeeBotResource.java

package com.split.coffeebot.web.rest;
…
@RestController
@RequestMapping("/api/coffee-bot")public class CoffeeBotResource {
    private final Logger log = LoggerFactory.getLogger(CoffeeBotResource.class);
    SplitClient splitClient;
    DrinkRepository drinkRepository;
    public CoffeeBotResource(SplitClient splitClient, DrinkRepository drinkRepository) {
        this.splitClient = splitClient;
        this.drinkRepository = drinkRepository;
    }
    private Drink makeDrink(String name, DrinkSize size, Integer caffeineMg, Integer price) {
        Drink drink = new Drink();
        drink.setCaffeineMilligrams(caffeineMg);
        drink.setName(name);
        drink.setSize(size);
        drink.setPriceDollars(price);
        return drink;
    }
    @EventListener
    public void onApplicationEvent(ContextRefreshedEvent event) {
        drinkRepository.save(makeDrink("Water", DrinkSize.Small, 0, 1));
        drinkRepository.save(makeDrink("Soda", DrinkSize.Medium, 30, 3));
        drinkRepository.save(makeDrink("Coffee", DrinkSize.XLarge, 50, 5));
        drinkRepository.save(makeDrink("Coffee", DrinkSize.Small, 30, 3));
        drinkRepository.save(makeDrink("Coffee", DrinkSize.Medium, 40, 3));
        drinkRepository.save(makeDrink("Latte", DrinkSize.Large, 100, 8));
        drinkRepository.save(makeDrink("Latte", DrinkSize.Small, 80, 6));
        drinkRepository.save(makeDrink("Latte", DrinkSize.Medium, 60, 5));
    }
    @GetMapping("/list-drinks")
    public List listDrinks() {
        Optional userName = SecurityUtils.getCurrentUserLogin();
        String treatment = splitClient.getTreatment(userName.get(),"drink-types");
        if (treatment.equals("on")) {
            return drinkRepository.findAll();
        }
        else {
            return drinkRepository.findByNameNotIn(Arrays.asList("Latte", "Soda"));
        }
    }
}

和方法作为辅助方法,用于在应用程序启动时创建一些示例makeDrink()数据onApplicationEvent()(请记住,它使用的是内存数据库,不会在会话之间保留任何数据)。

该类使用 Spring 的依赖注入来使两个对象可用:DrinkRepository,这是自动创建的接口,定义应用程序如何操作实体(饮料);,SplitClient它是负责与 Split 通信并获取给定密钥和治疗名称的治疗的客户端。

你很快就会创建这种治疗方法。现在,请注意该getTreatment()方法至少需要两个参数。一种是文本键,它是任意字符串值,通常是用户名、帐户 ID 或用于区分用户的另一个唯一键。另一个是分割名称,它指定使用哪种处理来进行分割。

专业提示:可选的第三个getTreatment参数(我们在这里不会使用)是一个属性 映射对象,包含名称-值对中的用户属性。即使是敏感的用户数据也可以在此映射中传递,因为这些数据都不会发送到斯普利特的云。相反,属性映射会在本地内存中与你在拆分 UI 中输入的定位规则进行比较。更多内容请参见 Split SDK 文档:使用属性映射进行自定义定位。

如果治疗是on,它会返回所有可用的饮料。如果处理不是on(off或control或任何其他值),则返回除Latte和之外的所有饮料Soda。这演示了一种基于拆分来分叉代码的简单方法。更复杂的分割和治疗用例是可能的。
在你搬家之前,最后一项改变。打开SecurityConfiguration文件并允许api/coffee-bot资源路径上的所有流量。这将允许匿名用户获得饮料清单。

你要添加这一行:

.antMatchers("/api/coffee-bot/**").permitAll()  代码语言: Bash  (bash )

至configure(HttpSecurity http)方法。立场很重要。该行需要添加到该.antMatchers("/api/**").authenticated()行之前。

src/main/java/com/split/coffeebot/config/SecurityConfiguration.java

@Overridepublic void configure(HttpSecurity http) throws Exception {
    // @formatter:off
    http
    ...
    .and()
        .authorizeRequests()
        .antMatchers("/api/authenticate").permitAll()
        .antMatchers("/api/register").permitAll()
        .antMatchers("/api/activate").permitAll()
        .antMatchers("/api/account/reset-password/init").permitAll()
        .antMatchers("/api/account/reset-password/finish").permitAll()
        .antMatchers("/api/coffee-bot/**").permitAll()
        .antMatchers("/api/**").authenticated()
        .antMatchers("/management/health").permitAll()
        .antMatchers("/management/info").permitAll()
        .antMatchers("/management/prometheus").permitAll()
        .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
    ...
    // @formatter:on
}

创建特征标志处理

如果你对治疗和 Split 不熟悉,你可能需要阅读Split 网站上的入门信息。简而言之,分割定义了一个决策点,一个标志,可以在代码中使用它来修改呈现给一组用户的功能。键(以及可选的属性映射)是根据拆分中定义的规则确定标志状态的值。这个决定是在运行时调用该方法时做出的。SplitClientgetTreatment()

在我们非常简单的示例中,你将创建一个名为Drink-types的拆分。这个分割将是一个简单的开/关分割,就像一个布尔标志(除了开和关之外,还可以有多个值)。分割将默认为off,但on如果admin用户在场,则分割将变为 。这是一个非常幼稚的例子。举个例子,在生产中,你可以根据用户群的一部分来定义这种划分,以在细分中推出新功能;或者,你可以先将功能仅提供给公共 Beta 测试人员,然后再将其发布给整个用户群。
打开拆分仪表板。你应该位于默认工作区中。

单击左侧的“拆分” 。

单击蓝色的“创建拆分”按钮。

全栈教程:Spring Boot 和 Vue.js 入门_第3张图片

为分割命名:drink-types。你可以将其余部分留空。

单击创建。

全栈教程:Spring Boot 和 Vue.js 入门_第4张图片

从环境下拉列表中选择暂存-默认。

单击添加规则按钮。

请注意,在定义处理部分下,定义了两个值:on和off。对于我们的用例来说,这非常棒。

在“创建单个目标”下,单击“添加目标”按钮。添加名为 的用户admin。这意味着用户admin将受到治疗on。

单击面板右上角的保存更改。

单击下一个面板上的“确认”以确认更改。

更新 Vue.js 客户端应用程序

客户端代码使用axios向资源服务器发出请求。你还需要安装 Split 模块依赖项。从项目根目录添加依赖项。

npm install --save axios @splitsoftware/[email protected]

更新home.component.ts文件以创建 CoffeeBot 应用程序。你需要为下面代码中的位置添加Javascript – staging-default键。const SPLIT_AUTH_KEY
src/main/webapp/app/core/home/home.component.ts

import Component from 'vue-class-component';
import { Inject, Vue, Watch } from 'vue-property-decorator';
import LoginService from '@/account/login.service';
import { SplitFactory } from '@splitsoftware/splitio';
import { IClient } from '@splitsoftware/splitio/types/splitio';
import axios from 'axios';const SPLIT_AUTH_KEY = ;
@Component
export default class Home extends Vue {
  @Inject('loginService')
  private loginService: () => LoginService;
  private splitClient: IClient = null;
  // our list of drinks
  private drinks = [];
  // holds the drink that is the current order
  private currentOrder = null;
  // cream or no cream?
  private withCream = false;
  // the current Split.io treatment
  private treatment = null;
  public openLogin(): void {
    this.loginService().openLogin((this).$root);
  }
  public get authenticated(): boolean {
    return this.$store.getters.authenticated;
  }
  public get username(): string {
    return this.$store.getters.account ? this.$store.getters.account.login : '';
  }
  async getTreatment() {
    // create a configured SplitFactory
    const splitFactory = SplitFactory({
      core: {
        authorizationKey: SPLIT_AUTH_KEY,  // your Split.io auth key
        key: this.username,  // identifier for this treatment (username in this case)
        trafficType: 'user'
      },
      startup: {
        readyTimeout: 1.5 // 1.5 sec
      }
    });
    // create the split client (NOT READY TO USE YET)
    this.splitClient = splitFactory.client();
    // block untli the client is ready
    this.splitClient.on(this.splitClient.Event.SDK_READY, function() {
      // client is ready, get the treatment
      this.treatment = this.splitClient.getTreatment('drink-types');
    }.bind(this));
  }
  // triggered when username changes to update list
  // of drinks and Split.io treatment
  @Watch('username')
  async usernameChanged(newVal: string, oldVal: String) {
    // get treatment from split.io
    await this.getTreatment();
    // call the REST service to load drinks
    await this.loadDrinks();
        // clear the current order
        this.currentOrder = null;
  }
  async loadDrinks() {
    const response = await axios.get('http://localhost:8080/api/coffee-bot/list-drinks');
    console.log(response);
    if (response && response.status === 200) {
      this.drinks = response.data;
    }
    else {
      this.drinks = [];
    }
  }
  async mounted() {
    await this.getTreatment();
    await this.loadDrinks();
  }
  beforeDestroy() {
    this.splitClient.destroy();
  }
}

该组件的身份验证部分是通过 JHipster 引导程序免费提供的。通过该方法从 Spring Boot 资源服务器加载数据loadDrinks(),该方法只是将饮料存储在本地数组中,巧妙地称为drinks. 当安装组件和用户更改时(因为可用的饮料取决于治疗,而治疗由用户决定),则会调用此方法。你可能会注意到此方法没有传递用户名。这是因为用户名会自动通过 JWT(JSON Web 令牌)传递到服务器,由 Spring Security 处理,身份验证代码由 JHipster 引导。

另一个重要的函数是usernameChanged()方法,当属性更改时调用该方法username。每次有新用户时,都需要创建新的 Split 客户端并重新加载处理。你还需要从服务器重新加载饮料。这个方法处理所有这些。

请注意此处的一般流程。首先,SplitFactory使用分割身份验证密钥和新用户名配置 a。该SplitFactory实例用于创建SplitClient实例。然而,客户此时不一定准备好。该代码会阻塞,直到SDK_READY事件被触发(参见下面的代码),然后才尝试从 获取处理SplitClient,否则它将仅返回control处理。

// block until SDK is readythis.splitClient.on(this.splitClient.Event.SDK_READY, function () {
  // ready now, so get treatment
  this.treatment = this.splitClient.getTreatment('drink-types');
}.bind(this))

home.vue现在更新与该组件对应的模板文件。

src/main/webapp/app/core/home/home.vue



请注意,该文件使用 Vue 的条件语法根据处理状态有条件地渲染添加和删除奶油按钮。在这种情况下,即使组件中有一些与添加和删除奶油功能相关的代码未切换,这就是我们管理功能状态所需要做的全部工作。

Add Cream - FREE Remove Cream

尝试完成的 Spring Boot + Vue.js 教程应用程序

你现在可以尝试完成的应用程序。启动或重新启动服务器和客户端。你可能希望在启动客户端之前让服务器完成启动。

Spring Boot Java 服务器:

./gradlew

Vue.js 客户端:

npm start

客户端应用程序应自动打开。如果没有,请打开 http://localhost:9000

当你第一次加载应用程序时,你会看到用户是anonymous并且治疗是对照治疗。

全栈教程:Spring Boot 和 Vue.js 入门_第5张图片

使用默认管理员凭据 ( ) 登录admin:admin,你将看到扩展的饮料列表。当你添加一个饮料时,你会看到一个名为"Add Cream"的按钮。该按钮的可见性或功能可能受到名为"split treatment"的处理方式的控制。换句话说,根据某种分割处理的规则或逻辑,决定了是否显示或启用这个"Add Cream"按钮。

全栈教程:Spring Boot 和 Vue.js 入门_第6张图片

注销并以默认用户 ( user:user) 身份登录,你将看到处理方式,并且off你将获得与该用户相同的饮料列表anonymous。此外,你无法选择添加奶油。

了解有关 Spring Boot、功能标志和生产中测试的更多信息

在本教程中,你创建了一个全栈应用程序,其中包括 Vue.js 客户端和 Spring Boot 资源服务器。Vue.js 客户端使用 TypeScript 进行更现代、无错误的开发(因为如果使用得当,类型检查可以大大减少运行时错误)。Spring Boot 服务器使用 Spring Security 和 Spring JPA 等技术来快速轻松地定义数据模型(或实体),将该实体的实例保存到数据库,并在 REST 接口中提供该数据。

客户端和服务器都使用 JWT 身份验证来保护安全。Split用于实现功能标志,在服务器端使用Java SDK实现拆分,在客户端使用Javascript SDK实现拆分。

所有这一切都是使用 JHipster 引导的,这使得使用现代最佳实践和技术启动新的全栈项目变得非常容易。

你可以在Split 的示例 GitHub上找到所有设置的 JHipster 的完整源代码。

你可能感兴趣的:(java)