Java 9 新特性:模块化系统

Java 9 引入的模块系统(Module System)是 Jigsaw 项目的一部分,旨在将模块化开发实践引入 Java 平台。这一特性带来了更好的代码重用性和维护性。本文将通过源码解读和示例讲解模块系统的核心概念、设计目的以及在动态模块加载和热部署中的应用。

什么是模块系统?

模块系统是一个唯一命名的、可重用的包集合,以及资源和模块描述文件(module-info.java)。任何一个 JAR 文件,只要加上一个模块描述文件(module-info.java),就可以升级为一个模块。

模块描述文件 module-info.java

模块描述文件 module-info.java 是定义模块的核心。它允许我们定义模块的名称、依赖项、导出的包等。下面是一个简单的模块描述文件示例:

java

module my.module {
    exports com.my.package.name;
}

在上面的代码中,exports 关键字用于公开指定包的所有公共成员。

模块化的设计目的

  1. 提高封装性:通过模块系统,可以控制哪些类可以对外开放使用,哪些类只能内部使用。
  2. 减少模块之间的耦合:通过模块化,可以减少系统中模块之间的依赖和耦合。
  3. 定制运行时环境:通过 jlink 工具,可以创建只包含所依赖的 JDK 模块的自定义运行时镜像,从而减少 Java 运行时环境的大小。

JDK 模块化

在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大地减少 Java 运行时环境的大小。

exports 关键词

exports 关键字用于控制哪些包可以对外开放使用。我们还可以使用 exports ... to 语法来限制包的访问范围,例如:

java

module my.module {
    exports com.my.package.name to com.specific.package;
}

在上面的代码中,exports ... to 语法用于限制访问的成员范围,只有 com.specific.package 可以访问 com.my.package.name 包中的类。

示例:创建和使用模块

下面我们通过一个具体的示例来说明如何创建和使用模块。

创建模块 com.example.module

首先,我们创建一个模块 com.example.module,并在该模块中定义一个简单的类 HelloWorld

java

// 文件路径:src/com.example.module/module-info.java
module com.example.module {
    exports com.example.module;
}

java

// 文件路径:src/com.example.module/com/example/module/HelloWorld.java
package com.example.module;

public class HelloWorld {
    public void sayHello() {
        System.out.println("Hello, Module System!");
    }
}

使用模块

接下来,我们创建一个新的模块 com.example.main 来使用 com.example.module 模块:

java

// 文件路径:src/com.example.main/module-info.java
module com.example.main {
    requires com.example.module;
}

java

// 文件路径:src/com.example.main/com/example/main/Main.java
package com.example.main;

import com.example.module.HelloWorld;

public class Main {
    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.sayHello();
    }
}

编译和运行

编译和运行上述模块化应用程序:

  1. 编译模块 com.example.module

    shell

    javac -d out/module --module-source-path src $(find src -name "*.java")
  2. 编译模块 com.example.main

    shell

    javac -d out/main --module-path out/module --module-source-path src $(find src -name "*.java")
  3. 运行应用程序:

    shell

    java --module-path out -m com.example.main/com.example.main.Main

模块系统与传统的 JAR 包管理有什么本质区别?

模块系统与传统的 JAR 包管理有着显著的区别,它们在设计理念、依赖管理、封装性和运行时环境等方面存在本质上的不同。下面我们详细解析这些区别,并举例说明每种方法的具体应用场景。

1. 设计理念

传统的 JAR 包管理

传统的 JAR 包管理主要依赖于类路径(classpath)。所有的 JAR 文件都被放置在类路径中,JVM 会根据类路径加载相应的类和资源。

java -cp lib/* com.example.Main
模块系统

模块系统引入了模块描述文件 module-info.java,每个模块都有明确的边界和依赖关系。模块系统通过模块路径(module path)管理模块,取代了传统的类路径。

java

module com.example.module {
    exports com.example.module;
}

2. 封装性

传统的 JAR 包管理

在传统的 JAR 包管理中,所有类都是公开的,只要类路径中有该 JAR 文件,任何其他类都可以访问其中的公共类和方法。这种方式缺乏对内部实现细节的封装控制。

模块系统

模块系统通过 exports 关键字控制包的可见性,只有显式导出的包中的公共类和方法才能被外部访问。其他包中的类和方法默认是隐藏的。

java

module com.example.module {
    exports com.example.module;
}

3. 依赖管理

传统的 JAR 包管理

在传统的 JAR 包管理中,依赖关系是通过手动管理的,所有依赖的 JAR 文件都需要被添加到类路径中。这种方式容易导致类路径的冲突和依赖地狱(Jar Hell)。

shell

java -cp lib/dependency1.jar:lib/dependency2.jar com.example.Main
模块系统

模块系统通过 requires 关键字明确声明模块依赖关系,JVM 在启动时会根据模块描述文件自动解析和加载依赖模块。

java

module com.example.main {
    requires com.example.module;
}

4. 运行时环境

传统的 JAR 包管理

在传统的 JAR 包管理中,JVM 会加载所有类路径中的 JAR 文件,这可能会导致冗余和不必要的资源占用。

模块系统

模块系统通过 jlink 工具可以创建自定义的运行时环境,只包含所需的模块。这不仅减少了运行时环境的大小,还提升了性能和安全性。

shell

jlink --module-path $JAVA_HOME/jmods --add-modules com.example.module --output custom-runtime

实例对比

传统的 JAR 包管理实例

假设我们有一个简单的项目 example.jar,并且它依赖于 commons-lang3-3.12.0.jar

  1. 创建项目结构:

txt

example/
├── lib/
│   └── commons-lang3-3.12.0.jar
├── src/
│   └── com/example/Main.java
└── example.jar
  1. 编写 Main.java

java

package com.example;

import org.apache.commons.lang3.StringUtils;

public class Main {
    public static void main(String[] args) {
        System.out.println(StringUtils.capitalize("hello world"));
    }
}
  1. 编译和运行:

shell

javac -d out -cp lib/commons-lang3-3.12.0.jar src/com/example/Main.java
jar --create --file example.jar --main-class com.example.Main -C out/ .
java -cp lib/commons-lang3-3.12.0.jar:example.jar com.example.Main
模块系统实例

假设我们有相同的项目,但我们将其模块化。

  1. 创建模块结构:

txt

example/
├── lib/
│   └── commons-lang3-3.12.0.jar
├── src/
│   ├── com.example.main/
│   │   └── module-info.java
│   └── com.example.main/com/example/Main.java
└── example.jar
  1. 编写 module-info.java

java

module com.example.main {
    requires org.apache.commons.lang3;
}
  1. 编写 Main.java

java

package com.example;

import org.apache.commons.lang3.StringUtils;

public class Main {
    public static void main(String[] args) {
        System.out.println(StringUtils.capitalize("hello world"));
    }
}
  1. 编译和运行:

shell

javac -d out --module-path lib --module-source-path src $(find src -name "*.java")
jar --create --file example.jar --main-class com.example.Main -C out/ .
java --module-path lib:example.jar --module com.example.main/com.example.Main

在动态模块加载和热部署场景中,模块化系统有哪些注意事项和最佳实践?

在现代软件开发中,动态模块加载和热部署是非常重要的需求,特别是在需要高可用性和快速迭代的应用中。Java 9 引入的模块系统带来了一些新的挑战和机会,使得动态模块加载和热部署变得更加复杂但也更加可控。下面我们详细讨论在动态模块加载和热部署场景中,模块化系统的注意事项和最佳实践。

1. 动态模块加载的基础

传统的类加载机制

在传统的 Java 应用中,动态加载类通常通过 ClassLoader 来实现。例如,可以使用 URLClassLoader 动态加载 JAR 文件:

java

URL[] urls = {new URL("file:///path/to/your.jar")};
URLClassLoader classLoader = new URLClassLoader(urls);
Class<?> clazz = classLoader.loadClass("com.example.YourClass");
Object instance = clazz.newInstance();
模块化系统中的动态加载

在模块化系统中,动态加载模块变得更加复杂。Java 9 引入了 Layer 和 ModuleFinder,可以用于动态加载模块。以下是一个基本的示例:

java

// 找到模块路径
Path modulePath = Paths.get("/path/to/your/modules");

// 创建模块查找器
ModuleFinder finder = ModuleFinder.of(modulePath);

// 创建模块图
Configuration configuration = ModuleLayer.boot().configuration()
    .resolve(finder, ModuleFinder.of(), Set.of("your.module"));

// 创建新的模块层
ModuleLayer layer = ModuleLayer.boot().defineModulesWithOneLoader(configuration, ClassLoader.getSystemClassLoader());

// 加载模块中的类
Class<?> clazz = layer.findLoader("your.module").loadClass("com.example.YourClass");
Object instance = clazz.newInstance();

2. 热部署的基础

传统的热部署

传统的热部署通常依赖于应用服务器(如 Tomcat、Jetty)或第三方工具(如 JRebel)来实现。这些工具通过监控文件系统的变化,重新加载修改过的类和资源。

模块化系统中的热部署

在模块化系统中,实现热部署需要更多的控制和管理。以下是一些常见的策略:

  1. 使用 OSGi:OSGi 是一种动态模块系统,可以与 Java 模块系统结合使用,提供动态加载、卸载和更新模块的功能。
  2. 自定义类加载器:通过自定义类加载器,管理模块的加载和卸载。
  3. 服务提供者接口(SPI):通过 SPI 机制,可以动态发现和加载模块中的服务。

3. 注意事项和最佳实践

注意事项
  1. 模块版本控制:确保模块的版本控制和依赖管理清晰,避免版本冲突。
  2. 模块依赖:在动态加载模块时,确保所有依赖模块都已加载。
  3. 资源管理:注意模块的资源管理,特别是文件句柄和网络连接等资源的释放。
最佳实践
  1. 使用服务提供者接口(SPI)
    通过 SPI,可以在模块间定义和发现服务,便于动态加载和替换模块中的实现。

    java

    // 定义服务接口
    public interface GreetingService {
        void greet(String name);
    }
    
    // 实现服务接口
    public class GreetingServiceImpl implements GreetingService {
        @Override
        public void greet(String name) {
            System.out.println("Hello, " + name);
        }
    }
    
    // 使用服务加载器加载服务
    ServiceLoader<GreetingService> loader = ServiceLoader.load(GreetingService.class);
    for (GreetingService service : loader) {
        service.greet("World");
    }
  2. 模块化设计
    在设计模块时,尽量保持模块的独立性和低耦合,便于动态加载和卸载。

  3. 使用 OSGi
    OSGi 提供了丰富的动态模块管理功能,可以与 Java 模块系统结合使用,实现更灵活的动态加载和热部署。

  4. 监控和日志
    实施详细的监控和日志记录,及时发现和处理动态加载和热部署中的问题。

  5. 测试自动化
    编写自动化测试,确保动态加载和热部署功能的稳定性和可靠性。

示例:使用 OSGi 实现动态模块管理

以下是一个使用 OSGi 动态加载和卸载模块的完整示例。

步骤 1:设置 OSGi 环境

首先,需要添加 OSGi 依赖。可以使用 Maven 或 Gradle 来管理这些依赖。

xml

<!-- Maven 依赖 -->
<dependency>
    <groupId>org.osgi</groupId>
    <artifactId>org.osgi.core</artifactId>
    <version>6.0.0</version>
</dependency>
<dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.framework</artifactId>
    <version>6.0.3</version>
</dependency>
<dependency>
    <groupId>org.apache.felix</groupId>
    <artifactId>org.apache.felix.main</artifactId>
    <version>6.0.3</version>
</dependency>
步骤 2:创建 OSGi 模块(Bundle)

创建一个简单的 OSGi 模块,包含一个服务。

com.example.hello 模块
META-INF/MANIFEST.MF

plaintext

Bundle-ManifestVersion: 2
Bundle-Name: Hello Bundle
Bundle-SymbolicName: com.example.hello
Bundle-Version: 1.0.0
Export-Package: com.example.hello
src/main/java/com/example/hello/HelloService.java

java

package com.example.hello;

public class HelloService {
    public void sayHello() {
        System.out.println("Hello, OSGi!");
    }
}
步骤 3:创建 OSGi 框架启动器

编写一个 OSGi 框架启动器,用于加载和卸载模块。

java

import org.apache.felix.framework.Felix;
import org.apache.felix.main.AutoProcessor;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.launch.Framework;
import org.osgi.framework.launch.FrameworkFactory;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;

public class OSGiLauncher {
    private Framework framework;

    public void startFramework() throws BundleException {
        Map<String, String> config = new HashMap<>();
        config.put("org.osgi.framework.storage.clean", "onFirstInit");

        FrameworkFactory factory = ServiceLoader.load(FrameworkFactory.class).iterator().next();
        framework = factory.newFramework(config);
        framework.init();

        AutoProcessor.process(config, framework.getBundleContext());
        framework.start();
    }

    public void stopFramework() throws BundleException {
        if (framework != null) {
            framework.stop();
            framework.waitForStop(0);
        }
    }

    public Bundle installBundle(String bundlePath) throws BundleException {
        BundleContext context = framework.getBundleContext();
        return context.installBundle(new File(bundlePath).toURI().toString());
    }

    public void uninstallBundle(Bundle bundle) throws BundleException {
        if (bundle != null) {
            bundle.uninstall();
        }
    }

    public static void main(String[] args) throws Exception {
        OSGiLauncher launcher = new OSGiLauncher();
        launcher.startFramework();

        // 安装并启动 bundle
        Bundle bundle = launcher.installBundle("path/to/hello-bundle.jar");
        bundle.start();

        // 使用 bundle 中的服务
        Class<?> helloServiceClass = bundle.loadClass("com.example.hello.HelloService");
        Object helloService = helloServiceClass.getDeclaredConstructor().newInstance();
        helloServiceClass.getMethod("sayHello").invoke(helloService);

        // 卸载 bundle
        launcher.uninstallBundle(bundle);

        launcher.stopFramework();
    }
}

4. 自定义类加载器实现动态卸载

如果不使用 OSGi,我们还可以通过自定义类加载器实现动态卸载和资源释放。这种方法虽然没有 OSGi 那么强大,但在一些简单的场景中也能奏效。

示例:自定义类加载器
步骤 1:创建自定义类加载器

java

import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;

public class DynamicClassLoader extends URLClassLoader {
    private Map<String, Class<?>> loadedClasses = new HashMap<>();

    public DynamicClassLoader(URL[] urls) {
        super(urls);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (loadedClasses.containsKey(name)) {
            return loadedClasses.get(name);
        }
        Class<?> clazz = super.findClass(name);
        loadedClasses.put(name, clazz);
        return clazz;
    }

    public void unloadClass(String name) {
        loadedClasses.remove(name);
    }
}
步骤 2:加载和卸载类

java

import java.net.URL;

public class DynamicModuleManager {
    private DynamicClassLoader classLoader;

    public DynamicModuleManager(String modulePath) throws Exception {
        URL[] urls = {new URL("file://" + modulePath)};
        classLoader = new DynamicClassLoader(urls);
    }

    public Class<?> loadClass(String className) throws ClassNotFoundException {
        return classLoader.loadClass(className);
    }

    public void unloadClass(String className) {
        classLoader.unloadClass(className);
    }

    public void close() throws Exception {
        classLoader.close();
    }

    public static void main(String[] args) throws Exception {
        DynamicModuleManager manager = new DynamicModuleManager("/path/to/your/module");

        // 加载类
        Class<?> clazz = manager.loadClass("com.example.YourClass");
        Object instance = clazz.getDeclaredConstructor().newInstance();
        clazz.getMethod("yourMethod").invoke(instance);

        // 卸载类
        manager.unloadClass("com.example.YourClass");
        manager.close();
    }
}

5. 资源管理和释放

无论使用哪种方法,资源管理和释放都是动态卸载模块中的重要部分。确保在卸载模块时释放所有占用的资源(如文件句柄、网络连接等)是至关重要的。

示例:资源释放
步骤 1:创建 ResourceHandler 类

java

import java.io.Closeable;
import java.io.IOException;

public class ResourceHandler implements Closeable {
    private boolean isResourceOpen;

    public void openResource() {
        isResourceOpen = true;
        System.out.println("Resource opened.");
    }

    public void useResource() {
        if (isResourceOpen) {
            System.out.println("Using resource.");
        } else {
            System.out.println("Resource is not open.");
        }
    }

    @Override
    public void close() throws IOException {
        if (isResourceOpen) {
            isResourceOpen = false;
            System.out.println("Resource closed.");
        }
    }
}
步骤 2:在模块中使用 ResourceHandler

java

package com.example.resource;

import java.io.IOException;

public class ResourceModule {
    private ResourceHandler resourceHandler;

    public ResourceModule() {
        resourceHandler = new ResourceHandler();
        resourceHandler.openResource();
    }

    public void performTask() {
        resourceHandler.useResource();
    }

    public void releaseResources() throws IOException {
        resourceHandler.close();
    }
}
步骤 3:动态加载和卸载模块时释放资源

在动态加载和卸载模块时,我们需要确保正确调用资源释放方法。

java

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;

public class ResourceModuleManager {
    private URLClassLoader classLoader;
    private Object resourceModuleInstance;

    public void loadModule(String modulePath) throws Exception {
        URL[] urls = {new URL("file://" + modulePath)};
        classLoader = new URLClassLoader(urls);

        Class<?> moduleClass = classLoader.loadClass("com.example.resource.ResourceModule");
        resourceModuleInstance = moduleClass.getDeclaredConstructor().newInstance();
        moduleClass.getMethod("performTask").invoke(resourceModuleInstance);
    }

    public void unloadModule() throws Exception {
        if (resourceModuleInstance != null) {
            resourceModuleInstance.getClass().getMethod("releaseResources").invoke(resourceModuleInstance);
            resourceModuleInstance = null;
        }
        if (classLoader != null) {
            classLoader.close();
            classLoader = null;
        }
    }

    public static void main(String[] args) throws Exception {
        ResourceModuleManager manager = new ResourceModuleManager();
        manager.loadModule("/path/to/your/module");

        // Perform some operations
        // ...

        // Unload module and release resources
        manager.unloadModule();
    }
}

在上述示例中,我们定义了一个 ResourceHandler 类来管理资源。在 ResourceModuleManager 类中,我们动态加载 ResourceModule 模块,并在卸载模块时调用 releaseResources 方法以释放资源。

6. 使用 ServiceLoader 动态发现和加载服务

Java 平台提供了 ServiceLoader 机制,可以在运行时动态发现和加载实现了特定接口的服务。这种方式可以用于动态加载和替换模块中的实现。

示例:使用 ServiceLoader 动态加载服务
步骤 1:定义服务接口

java

package com.example.service;

public interface GreetingService {
    void greet(String name);
}
步骤 2:实现服务接口

java

package com.example.service.impl;

import com.example.service.GreetingService;

public class GreetingServiceImpl implements GreetingService {
    @Override
    public void greet(String name) {
        System.out.println("Hello, " + name);
    }
}
步骤 3:创建服务提供者配置文件

在模块的资源目录中创建 META-INF/services/com.example.service.GreetingService 文件,内容为实现类的全限定名:

txt

com.example.service.impl.GreetingServiceImpl
步骤 4:使用 ServiceLoader 加载服务

java

import com.example.service.GreetingService;

import java.util.ServiceLoader;

public class ServiceLoaderExample {
    public static void main(String[] args) {
        ServiceLoader<GreetingService> loader = ServiceLoader.load(GreetingService.class);
        for (GreetingService service : loader) {
            service.greet("World");
        }
    }
}

 

在上述示例中,我们定义了 GreetingService 接口,并实现了该接口。通过 ServiceLoader,我们可以在运行时动态加载和使用服务实现。这种机制特别适用于需要动态发现和加载插件的场景。

实现动态模块卸载和资源释放的最佳实践

1. 确保资源的正确释放

在卸载模块时,确保所有占用的资源(如文件句柄、网络连接、线程等)都被正确释放,避免资源泄漏。可以通过实现 AutoCloseable 或 Closeable 接口来确保资源释放的逻辑清晰和统一。

2. 使用弱引用防止内存泄漏

在动态加载和卸载模块时,使用弱引用(WeakReference)可以帮助防止内存泄漏。弱引用允许垃圾回收器回收不再使用的对象,从而避免内存泄漏。

3. 模块间的解耦

设计模块时,尽量使模块之间的依赖关系松散,避免强耦合。可以使用依赖注入(DI)或服务提供者接口(SPI)来实现模块间的解耦。

4. 使用 OSGi 框架

OSGi 框架提供了强大的动态模块管理功能,可以轻松实现模块的动态加载、卸载和更新。OSGi 框架还提供了模块生命周期管理、版本控制和依赖管理等功能,适合复杂的动态模块管理需求。

5. 详细的监控和日志记录

在动态模块加载和卸载过程中,详细的监控和日志记录可以帮助及时发现和解决问题。可以记录模块的加载、卸载、资源使用情况等信息,以便在出现问题时进行快速诊断。

实例:综合示例

以下是一个综合示例,展示了如何实现动态模块加载、卸载和资源释放。

步骤 1:创建模块 com.example.resource

java

// 文件路径:src/com.example.resource/module-info.java
module com.example.resource {
    exports com.example.resource;
}

java

// 文件路径:src/com.example.resource/com/example/resource/ResourceHandler.java
package com.example.resource;

import java.io.Closeable;
import java.io.IOException;

public class ResourceHandler implements Closeable {
    private boolean isResourceOpen;

    public void openResource() {
        isResourceOpen = true;
        System.out.println("Resource opened.");
    }

    public void useResource() {
        if (isResourceOpen) {
            System.out.println("Using resource.");
        } else {
            System.out.println("Resource is not open.");
        }
    }

    @Override
    public void close() throws IOException {
        if (isResourceOpen) {
            isResourceOpen = false;
            System.out.println("Resource closed.");
        }
    }
}
步骤 2:创建模块 com.example.main

java

// 文件路径:src/com.example.main/module-info.java
module com.example.main {
    requires com.example.resource;
}

java

// 文件路径:src/com.example.main/com/example/main/Main.java
package com.example.main;

import com.example.resource.ResourceHandler;

import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        ResourceHandler resourceHandler = new ResourceHandler();
        resourceHandler.openResource();
        resourceHandler.useResource();
        
        try {
            resourceHandler.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
步骤 3:编译和运行

编译和运行上述模块化应用程序:

  1. 编译模块 com.example.resource 和 com.example.main

    shell

    javac -d out --module-source-path src $(find src -name "*.java")
  2. 运行应用程序:

    shell

    java --module-path out -m com.example.main/com.example.main.Main

在上述示例中,我们定义了 ResourceHandler 类来管理资源,并在 Main 类中动态加载和使用该资源。在卸载模块时,通过调用 close 方法来释放资源。

总结

模块化系统为 Java 带来了更高的封装性和依赖管理能力,但也引入了一些新的复杂性。在动态模块加载和热部署场景中,实现模块的动态卸载和资源释放需要仔细设计和管理。通过使用 OSGi 框架、自定义类加载器和 ServiceLoader 机制,可以有效地实现模块的动态加载、卸载和资源释放。确保在卸载模块时正确释放所有占用的资源是至关重要的,以避免资源泄漏和潜在的问题。

  • 40
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值