OpenZeppelin 7个最常使用的合约

栏目: IT技术 · 发布时间: 4年前

内容简介:使用 OpenZeppelin 来帮助进行合约开发,即可以提高代码的安全性,又可以提高开发效率。本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

使用 OpenZeppelin 来帮助进行合约开发,即可以提高代码的安全性,又可以提高开发效率。

OpenZeppelin的智能合约代码库 是以太坊开发者的宝库,OpenZeppelin代码库包含了经过社区审查的ERC代币标准、安全协议以及很多的辅助 工具 库,这些代码可以帮助开发者专注业务逻辑的,而无需重新发明轮子。

基于OpenZeppelin开发合约,即可以提高代码的安全性,又可以提高开发效率,文本列举了最应该添加到我们项目的 7个OpenZeppelin合约。

注意:在本文中我们使用的OpenZeppelin版本为2.5.x,使用 solidity 0.5.x编译器编译。

访问控制合约

1. 使用 Ownable 进行所有者限制

OpenZeppelin 的 Ownable 合约提供的 onlyOwner 修饰器是用来限制某些特定合约函数的访问权限。

我们很多时候需要这样做,因此这个模式在以太坊智能合约开发中非常流行。

Ownable合约的部署账号会被当做合约的拥有者(owner),某些合约函数,例如转移所有权,就限制在只允许拥有者(owner)调用。

下面是Ownable合约的源代码:

pragma solidity ^0.5.0;

import "../GSN/Context.sol";

contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor () internal {
        address msgSender = _msgSender();
        _owner = msgSender;
        emit OwnershipTransferred(address(0), msgSender);
    }

    function owner() public view returns (address) {
        return _owner;
    }

    modifier onlyOwner() {
        require(isOwner(), "Ownable: caller is not the owner");
        _;
    }

    function isOwner() public view returns (bool) {
        return _msgSender() == _owner;
    }

    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }

    function transferOwnership(address newOwner) public onlyOwner {
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}

注意在构造函数中如何设置合约的owner账号。当Ownable的子合约(即继承Ownable的合约)初始化时,部署的账号就会设置为 _owner

下面是一个简单的、继承自Ownable的合约:

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/ownership/Ownable.sol";

contract OwnableContract is Ownable {

  function restrictedFunction() public onlyOwner returns (uint) {
    return 99;
  }

  function openFunction() public returns (uint) {
    return 1;
  }

}

通过添加 onlyOwner 修饰器 来限制 restrictedFunction 函数合约的owner账号可以成功调用:

2. 使用 Roles 进行角色控制

进行访问控制另一个相对于 Ownable 合约 更高级一些的是使用 Roles 库, 它可以定义多个角色,对于需要多个访问层次的控制时,应当考虑使用Roles库。

OpenZeppelinRoles 库的源代码如下:

pragma solidity ^0.5.0;

library Roles {
    struct Role {
        mapping (address => bool) bearer;
    }

    function add(Role storage role, address account) internal {
        require(!has(role, account), "Roles: account already has role");
        role.bearer[account] = true;
    }

    function remove(Role storage role, address account) internal {
        require(has(role, account), "Roles: account does not have role");
        role.bearer[account] = false;
    }

    function has(Role storage role, address account) internal view returns (bool) {
        require(account != address(0), "Roles: account is the zero address");
        return role.bearer[account];
    }
}

由于 Roles 是一个Solidity库而非合约,因此不能通过继承的方式来使用,需要使用solidity的using语句来将库中定义的函数附加到指定的数据类型上。

下面的代码使用 Roles 库用 _minters_burners 两种角色去限制函数:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/access/Roles.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";

contract MyToken is ERC20, ERC20Detailed {
    using Roles for Roles.Role;

    Roles.Role private _minters;
    Roles.Role private _burners;

    constructor(address[] memory minters, address[] memory burners)
        ERC20Detailed("MyToken", "MTKN", 18)
        public
    {
        for (uint256 i = 0; i < minters.length; ++i) {
            _minters.add(minters[i]);
        }

        for (uint256 i = 0; i < burners.length; ++i) {
            _burners.add(burners[i]);
        }
    }

    function mint(address to, uint256 amount) public {
        // Only minters can mint
        require(_minters.has(msg.sender), "DOES_NOT_HAVE_MINTER_ROLE");

        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public {
        // Only burners can burn
        require(_burners.has(msg.sender), "DOES_NOT_HAVE_BURNER_ROLE");

       _burn(from, amount);
    }
}

第8行的作用是将 Roles 库中的函数附加到 Roles.Role 类型上。第18行就是在 Roles.Role 类型上直接使用这些库函数的方法: _minters.add() ,其中 add() 就是 Roles 库提供的实现。

算术运算

3. 安全的算术运算库:SafeMath

永远不要直接使用算术运算符例如:+、-、*、/ 进行数学计算,除非你了解如何检查溢出漏洞,否则就没法保证这些算术计算的安全性。

SafeMath库的作用是帮我们进行算术运中进行必要的检查,避免代码中因算术运算(如溢出)而引入漏洞。

下面是SafeMath的源代码:

pragma solidity ^0.5.0;

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        // Solidity only automatically asserts when dividing by 0
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

和Roles库的用法类似,你需要使用using语句将SafeMath库中的函数附加到uint256类型上,例如:

using SafeMath for uint256;

4. 安全类型转换库:SafeCast

作为一个智能合约开发者,我们常常会思考如何减少合约的执行时间以及空间,节约代码空间的一个办法就是使用更少位数的整数类型。 但不幸的是,如果你使用 uint8 作为变量类型,那么在调用 SafeMath 库函数之前,就必须先将其转换为 uint256 类型,然后在调用 SafeMath 库函数之后,还需要再转换回 uint8 类型。 SafeCast 库的作用就在于可以帮你完成这些转换而无需担心溢出问题。

SafeCast的源代码如下:

pragma solidity ^0.5.0;

library SafeCast {

    function toUint128(uint256 value) internal pure returns (uint128) {
        require(value < 2**128, "SafeCast: value doesn\'t fit in 128 bits");
        return uint128(value);
    }

    function toUint64(uint256 value) internal pure returns (uint64) {
        require(value < 2**64, "SafeCast: value doesn\'t fit in 64 bits");
        return uint64(value);
    }

    function toUint32(uint256 value) internal pure returns (uint32) {
        require(value < 2**32, "SafeCast: value doesn\'t fit in 32 bits");
        return uint32(value);
    }

    function toUint16(uint256 value) internal pure returns (uint16) {
        require(value < 2**16, "SafeCast: value doesn\'t fit in 16 bits");
        return uint16(value);
    }

    function toUint8(uint256 value) internal pure returns (uint8) {
        require(value < 2**8, "SafeCast: value doesn\'t fit in 8 bits");
        return uint8(value);
    }
}

下面的示例代码是如何使用 SafeCastuint 转换为 uint8

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/math/SafeCast.sol";

contract BasicSafeCast {

  using SafeCast for uint;
  
  function castToUint8(uint _a) public returns (uint8) {
    return _a.toUint8();
  }
}

Tokens (代币或通证)

ERC20Detailed

不需要自己实现完整的ERC20代币合约 ,OpenZeppelin已经帮我们实现好了, 我们只需要继承和初始化就好了。

OpenZeppelin 的ERC20进行了标准的基础实现,ERC20Detailed 合约包含了额外的选项:例如代币名称、代币代号以及小数点位数。

下面是一个利用 OpenZeppelinERC20ERC20Detailed 合约实现定制代币的例子:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";

contract GLDToken is ERC20, ERC20Detailed {
    constructor(uint256 initialSupply) ERC20Detailed("Gold", "GLD", 18) public {
        _mint(msg.sender, initialSupply);
    }
}

6. 非同质化代币:ERC721Enumerable / ERC721Full

OpenZeppelin也提供了非同质化代币的实现,我们同样不需要把完整的把标准实现一次。

如果需要枚举一个账号的所持有的ERC721资产,需要使用 ERC721Enumerable 合约而不是基础的 ERC721

ERC721Enumerable 提供了 _tokensOfOwner() 方法 直接支持枚举特定账号的所有资产。如果你希望有所有的扩展功能合约,那么可以直接选择 ERC721Full 。下面的代码展示了基于 ERC721Full 定制非同质化代币:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
import "@openzeppelin/contracts/drafts/Counters.sol";

contract GameItem is ERC721Full {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721Full("GameItem", "ITM") public {
    }

    function awardItem(address player, string memory tokenURI) public returns (uint256) {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(player, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

辅助工具库

7. 用 Address库识别地址

有时候在Solidity合约中需要了解一个地址是普通钱包地址还是合约地址。 OpenZeppelin的 Address 库提供了一个方法 isContract() 可以帮我们解决这个问题。

下面的代码展示了如何使用 isContract() 函数:

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/utils/Address.sol";

contract BasicUtils {
    using Address for address;

    function checkIfContract(address _addr) public {
        return _addr.isContract();
    }
}

原文链接: 7 OpenZeppelin Contracts You Should Always Use

作者: Alex Roan

OpenZeppelin的智能合约代码库 是以太坊开发者的宝库,OpenZeppelin代码库包含了经过社区审查的ERC代币标准、安全协议以及很多的辅助工具库,这些代码可以帮助开发者专注业务逻辑的,而无需重新发明轮子。

基于OpenZeppelin开发合约,即可以提高代码的安全性,又可以提高开发效率,文本列举了最应该添加到我们项目的 7个OpenZeppelin合约。

注意:在本文中我们使用的OpenZeppelin版本为2.5.x,使用 solidity 0.5.x编译器编译。

访问控制合约

1. 使用 Ownable 进行所有者限制

OpenZeppelin 的 Ownable 合约提供的 onlyOwner 修饰器是用来限制某些特定合约函数的访问权限。

我们很多时候需要这样做,因此这个模式在以太坊智能合约开发中非常流行。

Ownable合约的部署账号会被当做合约的拥有者(owner),某些合约函数,例如转移所有权,就限制在只允许拥有者(owner)调用。

下面是Ownable合约的源代码:

pragma solidity ^0.5.0;

import "../GSN/Context.sol";

contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor () internal {
        address msgSender = _msgSender();
        _owner = msgSender;
        emit OwnershipTransferred(address(0), msgSender);
    }

    function owner() public view returns (address) {
        return _owner;
    }

    modifier onlyOwner() {
        require(isOwner(), "Ownable: caller is not the owner");
        _;
    }

    function isOwner() public view returns (bool) {
        return _msgSender() == _owner;
    }

    function renounceOwnership() public onlyOwner {
        emit OwnershipTransferred(_owner, address(0));
        _owner = address(0);
    }

    function transferOwnership(address newOwner) public onlyOwner {
        _transferOwnership(newOwner);
    }

    function _transferOwnership(address newOwner) internal {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }
}

注意在构造函数中如何设置合约的owner账号。当Ownable的子合约(即继承Ownable的合约)初始化时,部署的账号就会设置为 _owner

下面是一个简单的、继承自Ownable的合约:

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/ownership/Ownable.sol";

contract OwnableContract is Ownable {

  function restrictedFunction() public onlyOwner returns (uint) {
    return 99;
  }

  function openFunction() public returns (uint) {
    return 1;
  }

}

通过添加 onlyOwner 修饰器 来限制 restrictedFunction 函数合约的owner账号可以成功调用:

2. 使用 Roles 进行角色控制

进行访问控制另一个相对于 Ownable 合约 更高级一些的是使用 Roles 库, 它可以定义多个角色,对于需要多个访问层次的控制时,应当考虑使用Roles库。

OpenZeppelinRoles 库的源代码如下:

pragma solidity ^0.5.0;

library Roles {
    struct Role {
        mapping (address => bool) bearer;
    }

    function add(Role storage role, address account) internal {
        require(!has(role, account), "Roles: account already has role");
        role.bearer[account] = true;
    }

    function remove(Role storage role, address account) internal {
        require(has(role, account), "Roles: account does not have role");
        role.bearer[account] = false;
    }

    function has(Role storage role, address account) internal view returns (bool) {
        require(account != address(0), "Roles: account is the zero address");
        return role.bearer[account];
    }
}

由于 Roles 是一个Solidity库而非合约,因此不能通过继承的方式来使用,需要使用solidity的using语句来将库中定义的函数附加到指定的数据类型上。

下面的代码使用 Roles 库用 _minters_burners 两种角色去限制函数:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/access/Roles.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";

contract MyToken is ERC20, ERC20Detailed {
    using Roles for Roles.Role;

    Roles.Role private _minters;
    Roles.Role private _burners;

    constructor(address[] memory minters, address[] memory burners)
        ERC20Detailed("MyToken", "MTKN", 18)
        public
    {
        for (uint256 i = 0; i < minters.length; ++i) {
            _minters.add(minters[i]);
        }

        for (uint256 i = 0; i < burners.length; ++i) {
            _burners.add(burners[i]);
        }
    }

    function mint(address to, uint256 amount) public {
        // Only minters can mint
        require(_minters.has(msg.sender), "DOES_NOT_HAVE_MINTER_ROLE");

        _mint(to, amount);
    }

    function burn(address from, uint256 amount) public {
        // Only burners can burn
        require(_burners.has(msg.sender), "DOES_NOT_HAVE_BURNER_ROLE");

       _burn(from, amount);
    }
}

第8行的作用是将 Roles 库中的函数附加到 Roles.Role 类型上。第18行就是在 Roles.Role 类型上直接使用这些库函数的方法: _minters.add() ,其中 add() 就是 Roles 库提供的实现。

算术运算

3. 安全的算术运算库:SafeMath

永远不要直接使用算术运算符例如:+、-、*、/ 进行数学计算,除非你了解如何检查溢出漏洞,否则就没法保证这些算术计算的安全性。

SafeMath库的作用是帮我们进行算术运中进行必要的检查,避免代码中因算术运算(如溢出)而引入漏洞。

下面是SafeMath的源代码:

pragma solidity ^0.5.0;

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        // Solidity only automatically asserts when dividing by 0
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

和Roles库的用法类似,你需要使用using语句将SafeMath库中的函数附加到uint256类型上,例如:

using SafeMath for uint256;

4. 安全类型转换库:SafeCast

作为一个智能合约开发者,我们常常会思考如何减少合约的执行时间以及空间,节约代码空间的一个办法就是使用更少位数的整数类型。 但不幸的是,如果你使用 uint8 作为变量类型,那么在调用 SafeMath 库函数之前,就必须先将其转换为 uint256 类型,然后在调用 SafeMath 库函数之后,还需要再转换回 uint8 类型。 SafeCast 库的作用就在于可以帮你完成这些转换而无需担心溢出问题。

SafeCast的源代码如下:

pragma solidity ^0.5.0;

library SafeCast {

    function toUint128(uint256 value) internal pure returns (uint128) {
        require(value < 2**128, "SafeCast: value doesn\'t fit in 128 bits");
        return uint128(value);
    }

    function toUint64(uint256 value) internal pure returns (uint64) {
        require(value < 2**64, "SafeCast: value doesn\'t fit in 64 bits");
        return uint64(value);
    }

    function toUint32(uint256 value) internal pure returns (uint32) {
        require(value < 2**32, "SafeCast: value doesn\'t fit in 32 bits");
        return uint32(value);
    }

    function toUint16(uint256 value) internal pure returns (uint16) {
        require(value < 2**16, "SafeCast: value doesn\'t fit in 16 bits");
        return uint16(value);
    }

    function toUint8(uint256 value) internal pure returns (uint8) {
        require(value < 2**8, "SafeCast: value doesn\'t fit in 8 bits");
        return uint8(value);
    }
}

下面的示例代码是如何使用 SafeCastuint 转换为 uint8

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/math/SafeCast.sol";

contract BasicSafeCast {

  using SafeCast for uint;

  function castToUint8(uint _a) public returns (uint8) {
    return _a.toUint8();
  }
}

Tokens (代币或通证)

ERC20Detailed

不需要自己实现完整的ERC20代币合约 ,OpenZeppelin已经帮我们实现好了, 我们只需要继承和初始化就好了。

OpenZeppelin 的ERC20进行了标准的基础实现,ERC20Detailed 合约包含了额外的选项:例如代币名称、代币代号以及小数点位数。

下面是一个利用 OpenZeppelinERC20ERC20Detailed 合约实现定制代币的例子:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";

contract GLDToken is ERC20, ERC20Detailed {
    constructor(uint256 initialSupply) ERC20Detailed("Gold", "GLD", 18) public {
        _mint(msg.sender, initialSupply);
    }
}

6. 非同质化代币:ERC721Enumerable / ERC721Full

OpenZeppelin也提供了非同质化代币的实现,我们同样不需要把完整的把标准实现一次。

如果需要枚举一个账号的所持有的ERC721资产,需要使用 ERC721Enumerable 合约而不是基础的 ERC721

ERC721Enumerable 提供了 _tokensOfOwner() 方法 直接支持枚举特定账号的所有资产。如果你希望有所有的扩展功能合约,那么可以直接选择 ERC721Full 。下面的代码展示了基于 ERC721Full 定制非同质化代币:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
import "@openzeppelin/contracts/drafts/Counters.sol";

contract GameItem is ERC721Full {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721Full("GameItem", "ITM") public {
    }

    function awardItem(address player, string memory tokenURI) public returns (uint256) {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(player, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

辅助工具库

7. 用 Address库识别地址

有时候在Solidity合约中需要了解一个地址是普通钱包地址还是合约地址。 OpenZeppelin的 Address 库提供了一个方法 isContract() 可以帮我们解决这个问题。

下面的代码展示了如何使用 isContract() 函数:

pragma solidity ^0.5.5;

import "@openzeppelin/contracts/utils/Address.sol";

contract BasicUtils {
    using Address for address;

    function checkIfContract(address _addr) public {
        return _addr.isContract();
    }
}

原文链接: 7 OpenZeppelin Contracts You Should Always Use

作者: Alex Roan

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 22分钟前
  • 阅读 ( 45 )
  • 学分 ( 0 )
  • 分类:智能合约

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java夜未眠

Java夜未眠

蔡学镛 / 电子工业出版社 / 2003-4 / 20.00元

本书是一本散文集。作为一名资深程序设计师,作者走笔清新面独特,简练俏皮的文字下,是作者对工作,对人生的理性思考。书中收录的文章内容贴近程序员的生活,能令读者产生强烈共鸣。此外,书中的部分文章也以轻松的风格剖析了学习Java技术时的常见问题,并以专家眼光和经验推荐介绍了一批优秀的技术书籍,旨在帮助读者兴趣盎然地学习Java。一起来看看 《Java夜未眠》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试