1.簡介
首先聲明這是基於Java後端開發的SSM框架,在開發過程中,編碼階段第一步就是實現對各表的增刪改查操作,也就是俗稱的CURD,一大半的時間都是把以前的代碼拿來抄抄改改,即使是使用了Mybatis的逆向工程,service
、controller
的代碼量依然很大,而且還要實現的諸如分頁查詢、多參數查詢等功能,Mybatis逆向工程提供的模板並不能完全適用。
在這裏,我依據我的編碼習慣,基於EasyCode插件,修改完成了我的一套完全生成dao
、service
、controller
代碼的宏定義模板,我想重點不是我的模板適不適你的場景,而是知道如何利用這個宏定義模板讓我們告別繁雜的CURD重複代碼編寫。
idea插件市場的下載不好用的話可以到gitee上去下載:https://gitee.com/makejava/EasyCode
2.使用EasyCode
下載好之後進行離線安裝:
然後創建一個數據庫,添加一個user表:
CREATE TABLE `user` (
`id` int(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(16) DEFAULT NULL COMMENT '姓名',
`pwd` varchar(16) DEFAULT NULL COMMENT '密碼',
`type` int(1) DEFAULT NULL COMMENT '0-管理員 1-普通用戶',
PRIMARY KEY (`id`)
)
構建一個SpringBoot項目,然後在idea中打開database添加mysql並連接上創建的數據庫:
在表上右鍵選擇EasyCode->General code,然後選擇包和生成的文件即可生成代碼:
3.自定義模板
3.1 如何自定義
使用作者預先設定的模板可以完成dao和service的大部分代碼,由於適用的普遍性,初始模板沒有包含過多具體的代碼,不過我們可以自定義:
File -> Settings 搜索 Easy Code
,選擇Template即可看到宏定義模板,我們可以在此處進行修改或者替換:
3.2 實例
接下來介紹針對CURD,我自己修改實現的包含分頁查詢、多條件查詢等的RestFul風格、前後端Json數據交互的CURD接口服務。
mapper.xml:
##引入mybatis支持
$!mybatisSupport
##設置保存名稱與保存位置
$!callback.setFileName($tool.append($!{tableInfo.name}, "Dao.xml"))
$!callback.setSavePath($tool.append($modulePath, "/src/main/resources/mapper"))
##拿到主鍵
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="$!{tableInfo.savePackageName}.dao.$!{tableInfo.name}Dao">
<resultMap type="$!{tableInfo.savePackageName}.entity.$!{tableInfo.name}" id="$!{tableInfo.name}Map">
#foreach($column in $tableInfo.fullColumn)
<result property="$!column.name" column="$!column.obj.name" jdbcType="$!column.ext.jdbcType"/>
#end
</resultMap>
<!--查詢單個-->
<select id="queryById" resultMap="$!{tableInfo.name}Map">
select
#allSqlColumn()
from $!{tableInfo.obj.parent.name}.$!tableInfo.obj.name
where $!pk.obj.name = #{$!pk.name}
</select>
<!--條件查詢(帶分頁)-->
<select id="selectList" parameterType="map" resultMap="$!{tableInfo.name}Map">
select
#allSqlColumn()
from $!{tableInfo.obj.parent.name}.$!tableInfo.obj.name
<where>
#foreach($column in $tableInfo.fullColumn)
<if test="$!column.name != null#if($column.type.equals("java.lang.String")) and $!column.name != ''#end">
and $!column.obj.name = #{$!column.name}
</if>
#end
</where>
<if test="offset != null and limit !=null">
limit ${offset},${limit}
</if>
</select>
<!--符合條件總數查詢-->
<select id="selectListSize" parameterType="map" resultType="java.lang.Integer">
select
count(*)
from $!{tableInfo.obj.parent.name}.$!tableInfo.obj.name
<where>
#foreach($column in $tableInfo.fullColumn)
<if test="$!column.name != null#if($column.type.equals("java.lang.String")) and $!column.name != ''#end">
and $!column.obj.name = #{$!column.name}
</if>
#end
</where>
</select>
<!--新增所有列-->
<insert id="insert" keyProperty="$!pk.name" useGeneratedKeys="true">
insert into $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name}(#foreach($column in $tableInfo.otherColumn)$!column.obj.name#if($velocityHasNext), #end#end)
values (#foreach($column in $tableInfo.otherColumn)#{$!{column.name}}#if($velocityHasNext), #end#end)
</insert>
<!--通過主鍵修改數據-->
<update id="update">
update $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name}
<set>
#foreach($column in $tableInfo.otherColumn)
<if test="$!column.name != null#if($column.type.equals("java.lang.String")) and $!column.name != ''#end">
$!column.obj.name = #{$!column.name},
</if>
#end
</set>
where $!pk.obj.name = #{$!pk.name}
</update>
<!--通過主鍵刪除-->
<delete id="deleteById">
delete from $!{tableInfo.obj.parent.name}.$!{tableInfo.obj.name} where $!pk.obj.name = #{$!pk.name}
</delete>
</mapper>
dao.java:
##定義初始變量
#set($tableName = $tool.append($tableInfo.name, "Dao"))
##設置回調
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/dao"))
##拿到主鍵
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}dao;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表數據庫訪問層
*
* @author $!author
* @since $!time.currTime()
*/
public interface $!{tableName} {
/**
* 通過ID查詢單條數據
*
* @param $!pk.name 主鍵
* @return 實例對象
*/
$!{tableInfo.name} queryById($!pk.shortType $!pk.name);
/**
* 條件查詢(帶分頁)
*
* @param params 實例對象
* @return 對象列表
*/
List<$!{tableInfo.name}> selectList(Map<String,String> params);
/**
* 符合條件總數查詢
*
* @param params 實例對象
* @return 對象列表
*/
int selectListSize(Map<String,String> params);
/**
* 新增數據
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 實例對象
* @return 影響行數
*/
int insert($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}));
/**
* 修改數據
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 實例對象
* @return 影響行數
*/
int update($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}));
/**
* 通過主鍵刪除數據
*
* @param $!pk.name 主鍵
* @return 影響行數
*/
int deleteById($!pk.shortType $!pk.name);
}
entity.java:
##引入宏定義
$!define
##使用宏定義設置回調(保存位置與文件後綴)
#save("/entity", ".java")
##使用宏定義設置包後綴
#setPackageSuffix("entity")
##使用全局變量實現默認包導入
$!autoImport
import java.io.Serializable;
##使用宏定義實現類註釋信息
#tableComment("實體類")
public class $!{tableInfo.name} implements Serializable {
private static final long serialVersionUID = $!tool.serial();
#foreach($column in $tableInfo.fullColumn)
#if(${column.comment})/**
* ${column.comment}
*/#end
private $!{tool.getClsNameByFullName($column.type)} $!{column.name};
#end
#foreach($column in $tableInfo.fullColumn)
##使用宏定義實現get,set方法
#getSetMethod($column)
#end
}
serviceImpl.java
##定義初始變量
#set($tableName = $tool.append($tableInfo.name, "ServiceImpl"))
##設置回調
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/service/impl"))
##拿到主鍵
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}service.impl;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import $!{tableInfo.savePackageName}.dao.$!{tableInfo.name}Dao;
import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.security.InvalidParameterException;
import java.util.List;
import java.util.Map;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表服務實現類
*
* @author $!author
* @since $!time.currTime()
*/
@Service("$!tool.firstLowerCase($!{tableInfo.name})Service")
public class $!{tableName} implements $!{tableInfo.name}Service {
@Resource
private $!{tableInfo.name}Dao $!tool.firstLowerCase($!{tableInfo.name})Dao;
/**
* 通過ID查詢單條數據
*
* @param $!pk.name 主鍵
* @return 實例對象
*/
@Override
public $!{tableInfo.name} queryById($!pk.shortType $!pk.name) {
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Dao.queryById($!pk.name);
}
/**
* 條件查詢(帶分頁)
*
* @param params 傳入參數
* @return 對象列表
*/
@Override
public List<$!{tableInfo.name}> selectList(Map<String,String> params) {
try {
if (params.containsKey("page") && params.containsKey("limit")) {
int offset = (Integer.parseInt(params.get("page")) - 1) * Integer.parseInt(params.get("limit"));
if (offset < 0) throw new InvalidParameterException("請傳入正確的分頁參數");
params.put("offset", offset + "");
}
}catch (Exception e){
throw new InvalidParameterException("分頁參數非法:"+e.getMessage());
}
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Dao.selectList(params);
}
/**
* 符合條件總數查詢
*
* @param params 傳入參數
* @return 對象列表
*/
public int selectListSize(Map<String, String> params) {
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Dao.selectListSize(params);
}
/**
* 新增數據
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 實例對象
* @return 是否成功
*/
@Override
public boolean insert($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})) {
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Dao.insert($!tool.firstLowerCase($!{tableInfo.name})) > 0;
}
/**
* 修改數據
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 實例對象
* @return 是否成功
*/
@Override
public boolean update($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})) {
if(StringUtils.isEmpty($!tool.append($!tool.firstLowerCase($!{tableInfo.name}),".get","$!tool.firstUpperCase($!pk.name)()")))
throw new InvalidParameterException("請傳入更新數據的主鍵");
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Dao.update($!tool.firstLowerCase($!{tableInfo.name})) > 0;
}
/**
* 通過主鍵刪除數據
*
* @param $!pk.name 主鍵
* @return 是否成功
*/
@Override
public boolean deleteById($!pk.shortType $!pk.name) {
return this.$!{tool.firstLowerCase($!{tableInfo.name})}Dao.deleteById($!pk.name) > 0;
}
}
service.java
##定義初始變量
#set($tableName = $tool.append($tableInfo.name, "Service"))
##設置回調
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/service"))
##拿到主鍵
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}service;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import java.util.List;
import java.util.Map;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表服務接口
*
* @author $!author
* @since $!time.currTime()
*/
public interface $!{tableName} {
/**
* 通過ID查詢單條數據
*
* @param $!pk.name 主鍵
* @return 實例對象
*/
$!{tableInfo.name} queryById($!pk.shortType $!pk.name);
/**
* 條件查詢(帶分頁)
*
* @param params 傳入參數
* @return 對象列表
*/
List<$!{tableInfo.name}> selectList(Map<String,String> params);
/**
* 符合條件總數查詢
*
* @param params 傳入參數
* @return 對象列表
*/
int selectListSize(Map<String,String> params);
/**
* 新增數據
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 實例對象
* @return 是否成功
*/
boolean insert($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}));
/**
* 修改數據
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 實例對象
* @return 是否成功
*/
boolean update($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}));
/**
* 通過主鍵刪除數據
*
* @param $!pk.name 主鍵
* @return 是否成功
*/
boolean deleteById($!pk.shortType $!pk.name);
}
controller.java:
##定義初始變量
#set($tableName = $tool.append($tableInfo.name, "Controller"))
##設置回調
$!callback.setFileName($tool.append($tableName, ".java"))
$!callback.setSavePath($tool.append($tableInfo.savePath, "/controller"))
##拿到主鍵
#if(!$tableInfo.pkColumn.isEmpty())
#set($pk = $tableInfo.pkColumn.get(0))
#end
#if($tableInfo.savePackageName)package $!{tableInfo.savePackageName}.#{end}controller;
import $!{tableInfo.savePackageName}.entity.$!{tableInfo.name};
import $!{tableInfo.savePackageName}.service.$!{tableInfo.name}Service;
import $!{tableInfo.savePackageName}.util.JsonResp;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
/**
* $!{tableInfo.comment}($!{tableInfo.name})表控制層
*
* @author $!author
* @since $!time.currTime()
*/
@RestController
@RequestMapping("$!tool.firstLowerCase($tableInfo.name)")
public class $!{tableName} {
/**
* 服務對象
*/
@Resource
private $!{tableInfo.name}Service $!tool.firstLowerCase($tableInfo.name)Service;
/**
* 通過主鍵查詢單條數據
*
* @param id 主鍵
* @return resp
* @throws Exception
*/
@GetMapping("/{id}")
public JsonResp queryById(@PathVariable $!pk.shortType id) throws Exception{
$!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name}) = this.$!{tool.firstLowerCase($tableInfo.name)}Service.queryById(id);
JsonResp resp = new JsonResp(JsonResp.STATE_OK,JsonResp.CODE_OK);
resp.setObj($!tool.firstLowerCase($!{tableInfo.name}));
return resp;
}
/**
* 條件查詢(帶分頁),參數名和實體類一致,另規定:
* page 表示頁數
* limit 表示每頁顯示數據條數
*
* @param params 傳入參數,K-V
* @return resp
* @throws Exception
*/
@GetMapping("/list")
public JsonResp list(@RequestParam Map<String,String> params) throws Exception{
JsonResp resp = new JsonResp(JsonResp.STATE_OK, JsonResp.CODE_OK);
List<$!{tableInfo.name}> list = this.$!{tool.firstLowerCase($tableInfo.name)}Service.selectList(params);
resp.setData(list);
resp.setCount(this.$!{tool.firstLowerCase($tableInfo.name)}Service.selectListSize(params));
return resp;
}
/**
* 新增數據
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 實例對象
* @return resp
* @throws Exception
*/
@PostMapping
public JsonResp insert($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})) throws Exception {
boolean flag = this.$!{tool.firstLowerCase($tableInfo.name)}Service.insert($!tool.firstLowerCase($!{tableInfo.name}));
JsonResp resp = null;
if(flag){
resp = new JsonResp(JsonResp.STATE_OK,JsonResp.CODE_OK);
}else{
resp = new JsonResp(JsonResp.STATE_ERR,JsonResp.CODE_ERR);
}
return resp;
}
/**
* 修改數據
*
* @param $!tool.firstLowerCase($!{tableInfo.name}) 實例對象
* @return resp
* @throws Exception
*/
@PutMapping
public JsonResp update($!{tableInfo.name} $!tool.firstLowerCase($!{tableInfo.name})) throws Exception {
boolean flag = this.$!{tool.firstLowerCase($tableInfo.name)}Service.update($!tool.firstLowerCase($!{tableInfo.name}));
JsonResp resp = null;
if(flag){
resp = new JsonResp(JsonResp.STATE_OK,JsonResp.CODE_OK);
}else{
resp = new JsonResp(JsonResp.STATE_ERR,JsonResp.CODE_ERR);
}
return resp;
}
/**
* 通過主鍵刪除數據
*
* @param $!pk.name 主鍵
* @return resp
* @throws Exception
*/
@DeleteMapping("/{id}")
public JsonResp deleteById(@PathVariable $!pk.shortType id) throws Exception {
boolean flag = this.$!{tool.firstLowerCase($tableInfo.name)}Service.deleteById(id);
JsonResp resp = null;
if(flag){
resp = new JsonResp(JsonResp.STATE_OK,JsonResp.CODE_OK);
}else{
resp = new JsonResp(JsonResp.STATE_ERR,JsonResp.CODE_ERR);
}
return resp;
}
}
模板中的註釋寫得比較詳細,將這些模板代碼替換進去之後,直接生成代碼:
然後加上JsonResp返回值封裝類,放在util包裏面:
package com.dsy.codetst.util;
import org.springframework.util.StringUtils;
import java.util.List;
public class JsonResp {
// 成功
public final static String STATE_OK = "ok";
public final static Integer CODE_OK = 0;
// 系統錯誤
public final static String STATE_ERR = "error";
public final static Integer CODE_ERR = 1;
// 業務錯誤
public final static String STATE_WARN = "warn";
/**
* @Fields state: 請求結果狀態
*/
private String state;
/**
* @Fields errMsg: 錯誤信息
*/
private String msg;
/**
* @Fields exceptionDetails: 異常細節,針對非自定義類異常中信息
*/
private String exceptionDetails;
/**
* @Fields exceptionStackTrace: 異常堆棧,針對非自定義類異常中信息
*/
private String exceptionStackTrace;
/**
* @Fields data: 結果集合
*/
private List data;
/**
* 結果集條數
*/
private Integer count;
/**
* 單個對象
*/
private Object obj;
/**
* 內部定義標識:0成功,其他錯誤
*/
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public List getData() {
return data;
}
public void setData(List data) {
this.data = data;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public JsonResp() {
}
public JsonResp(String state) throws Exception {
this.state = state;
}
public JsonResp(String state, Integer code) throws Exception {
this.state = state;
this.code = code;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getExceptionDetails() {
return exceptionDetails;
}
public void setExceptionDetails(String exceptionDetails) {
this.exceptionDetails = exceptionDetails;
}
public String getExceptionStackTrace() {
return exceptionStackTrace;
}
public void setExceptionStackTrace(String exceptionStackTrace) {
this.exceptionStackTrace = exceptionStackTrace;
}
}
在啓動類上添加註解 @MapperScan("com.dsy.codetst.dao")
掃描dao接口
配置文件中添加數據庫信息、mapper映射文件路徑:
application.properties
server.port=8080
spring.datasource.name=codetest
spring.datasource.url=jdbc:mysql://localhost:3333/codetest?characterEncoding=UTF-8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#mybatis.type-aliases-package=com.dag.dao.entity
mybatis.mapperLocations=classpath:mapper/*Dao.xml
到這裏,基於user的CURD就完成了,直接啓動項目就可以使用。
3.3 接口測試
新增數據
POST http://localhost:8080/user?name=測試&password=123456&type=1
返回:
{
"state": "ok",
"msg": null,
"exceptionDetails": null,
"exceptionStackTrace": null,
"data": null,
"count": null,
"obj": null,
"code": 0
}
再插入一條
POST http://localhost:8080/user?name=測試2&password=123456&type=1
主鍵查詢
GET http://localhost:8080/user/1
返回:
{
"state": "ok",
"msg": null,
"exceptionDetails": null,
"exceptionStackTrace": null,
"data": null,
"count": null,
"obj": {
"id": "1",
"name": "測試",
"pwd": null,
"type": 1
},
"code": 0
}
條件+分頁查詢(page表示頁數,limit表示每頁的數據條數)
GET http://localhost:8080/user/list?page=1&limit=3
返回:
{
"state": "ok",
"msg": null,
"exceptionDetails": null,
"exceptionStackTrace": null,
"data": [{
"id": "1",
"name": "測試",
"pwd": null,
"type": 1
},
{
"id": "2",
"name": "測試2",
"pwd": null,
"type": 1
}
],
"count": 2,
"obj": null,
"code": 0
}
修改數據
PUT http://localhost:8080/user?id=1&name=測試修改&pwd=123456
返回:
......(和查詢一樣,回調ok)
刪除數據
DELETE http://localhost:8080/user/2
返回:
......(同上,回調ok)
再次查詢驗證數據
GET http://localhost:8080/user/list?page=1&limit=3
{
"state": "ok",
"msg": null,
"exceptionDetails": null,
"exceptionStackTrace": null,
"data": [{
"id": "1",
"name": "測試修改",
"pwd": "123456",
"type": 1
}],
"count": 1,
"obj": null,
"code": 0
}
最後,結合SpringMVC的異常處理機制,會更好用,可以捕獲到拋出的異常,並仍然以JsonResp封裝返回錯誤信息:
package com.dsy.codetst.handler;
import com.dsy.codetst.util.JsonResp;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import java.security.InvalidParameterException;
/**
* 統一異常處理類
*
*/
@ControllerAdvice
@RestController
public class GlobalExceptionHandler {
/**
* 捕獲InvalidParameterException異常(參數只能是Throwable及其子類)
* @return
*/
@ExceptionHandler({InvalidParameterException.class})
public JsonResp handlerArithmeticException(InvalidParameterException e) throws Exception {
JsonResp resp = new JsonResp(JsonResp.STATE_ERR);
e.printStackTrace();
resp.setMsg("參數錯誤: "+e.getMessage());
return resp;
}
@ExceptionHandler({Exception.class})
public JsonResp handlerArithmeticException(Exception e) throws Exception {
JsonResp resp = new JsonResp(JsonResp.STATE_ERR);
e.printStackTrace();
resp.setMsg("發生錯誤: "+e.getMessage());
return resp;
}
}
一些說明:
1.在條件查詢的設計當中,爲了結合分頁查詢和數據封裝的簡便,使用了HashMap作爲參數,使用了page和limit分別做頁數和條數,如果你的數據表中要使用page、limit做字段,應當修改此處的參數名。2.分頁的limit子句中,使用了${param}取值,但不必擔心sql注入風險,service層對參數做了校驗,如果非數字,會直接拋出異常。