寫在前面
以前也一直不多有寫SpringBoot項目相關的文章,今天 準備整理一個我本身初始化SpringBoot項目時的一個腳手架,便於本身後面查閱。由於SpringBoot的約定大於配置,在整合各個組件的時候,咱們僅僅寫不多的代碼就能 整合 跑起來。css
本文,也僅僅是一個簡單的整合,更多個性化配置,更多調優,這個也是本身在工做中慢慢摸索的。若是你有什麼更多好的建議或者意見,也能夠留言交流。謝謝~html
咱們開始吧java
新建SpringBoot <version>2.0.3.RELEASE</version> web 項目mysql
標題1:AOP 切面統一打印請求日誌
意圖:能夠看到,每一個對於每一個請求,開始與結束一目瞭然,而且打印瞭如下參數:git
URL: 請求接口地址; HTTP Method: 請求的方法,是 POST, GET, 仍是 DELETE 等; Class Method: 對應 Controller 的全路徑以及調用的哪一個方法; IP: 請求 IP 地址; Request Args: 請求入參,以 JSON 格式輸出; Response Args: 響應出參,以 JSON 格式輸出; Time-Consuming: 請求耗時;
步驟一:添加依賴:github
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 用於日誌切面中,以 json 格式打印出入參 --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.5</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
步驟二:新建一個包aspect 自定義一個註解:web
import java.lang.annotation.*; /** * Description: TODO * * @Author: 留歌36 * @Date: 2019-11-27 15:43 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface WebLog { /** 日誌描述信息 */ String description() default ""; }
新建註解類:spring
import com.google.gson.Gson; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; /** * Description: 查看 https://www.cnblogs.com/quanxiaoha/p/10414681.html * * @Author: 留歌36 * @Date: 2019-11-08 11:00 */ @Aspect @Component @Slf4j public class WebLogAspect { /** 換行符 */ private static final String LINE_SEPARATOR = System.lineSeparator(); /** 以自定義 @WebLog 註解爲切點 */ @Pointcut("@annotation(com.csylh.boot2all.aspect.WebLog)") public void webLog() {} /** * 在切點以前織入 * @param joinPoint * @throws Throwable */ @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { // 開始打印請求日誌 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 獲取 @WebLog 註解的描述信息 String methodDescription = getAspectLogDescription(joinPoint); // 打印請求相關參數 log.info("========================================== Start =========================================="); // 打印請求 url log.info("URL : {}", request.getRequestURL().toString()); // 打印描述信息 log.info("Description : {}", methodDescription); // 打印 Http method log.info("HTTP Method : {}", request.getMethod()); // 打印調用 controller 的全路徑以及執行方法 log.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName()); // 打印請求的 IP log.info("IP : {}", request.getRemoteAddr()); // 打印請求入參 log.info("Request Args : {}", new Gson().toJson(joinPoint.getArgs())); } /** * 在切點以後織入 * @throws Throwable */ @After("webLog()") public void doAfter() throws Throwable { // 接口結束後換行,方便分割查看 log.info("=========================================== End ===========================================" + LINE_SEPARATOR); } /** * 環繞 * @param proceedingJoinPoint * @return * @throws Throwable */ @Around("webLog()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); // 打印出參 log.info("Response Args : {}", new Gson().toJson(result)); // 執行耗時 log.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime); return result; } /** * 獲取切面註解的描述 * * @param joinPoint 切點 * @return 描述信息 * @throws Exception */ public String getAspectLogDescription(JoinPoint joinPoint) throws Exception { String targetName = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] arguments = joinPoint.getArgs(); Class targetClass = Class.forName(targetName); Method[] methods = targetClass.getMethods(); StringBuilder description = new StringBuilder(""); for (Method method : methods) { if (method.getName().equals(methodName)) { Class[] clazzs = method.getParameterTypes(); if (clazzs.length == arguments.length) { description.append(method.getAnnotation(WebLog.class).description()); break; } } } return description.toString(); } }
就這樣就OK。測試:sql
標題2:Swagger 整合
意圖:生成文檔形式的API並提供給不一樣的團隊使用shell
便於本身單測
無需過多冗餘的word文檔,這一點很重要,由於我在工做中就遇到這麼一個狀況,因爲開發使用的文檔和最新文檔版本致使不一致,致使後期很煩人
步驟一:添加依賴
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.4.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.4.0</version> </dependency>
步驟2:新建swagger2配置類
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.ParameterBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.schema.ModelRef; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.service.Parameter; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList; import java.util.List; /** * Description: * * @author: 留歌36 * Date:2018/9/14 16:29 */ @Configuration @EnableSwagger2 public class Swagger2 { /** * @Description:swagger2的配置文件,這裏能夠配置swagger2的一些基本的內容,好比掃描的包等等 */ @Bean public Docket createRestApi() { // 爲swagger添加header參數可供輸入 // ParameterBuilder userTokenHeader = new ParameterBuilder(); // ParameterBuilder userIdHeader = new ParameterBuilder(); // List<Parameter> pars = new ArrayList<Parameter>(); // userTokenHeader.name("headerUserToken").description("userToken") // .modelRef(new ModelRef("string")).parameterType("header") // .required(false).build(); // userIdHeader.name("headerUserId").description("userId") // .modelRef(new ModelRef("string")).parameterType("header") // .required(false).build(); // pars.add(userTokenHeader.build()); // pars.add(userIdHeader.build()); return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select() // 注意修改這裏 .apis(RequestHandlerSelectors.basePackage("com.zd.tongnan.controller")) .paths(PathSelectors.any()).build() .globalOperationParameters(setHeaderToken()); // .globalOperationParameters(pars); } private List<Parameter> setHeaderToken() { ParameterBuilder tokenPar = new ParameterBuilder(); List<Parameter> pars = new ArrayList<>(); tokenPar.name("token").description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build(); pars.add(tokenPar.build()); return pars; } /** * @Description: 構建 api文檔的信息 */ private ApiInfo apiInfo() { return new ApiInfoBuilder() // 設置頁面標題 .title("xxx系統-接口數據文檔") // 描述 .description("xxx接口數據文檔") // 設置聯繫人 .contact(new Contact("留歌36","https://blog.csdn.net/liuge36","")) // .contact(new Contact("留歌36", "http://csylh.cn", "csylh36@163.com")) // 定義版本號 .version("V-1.0.0").build(); } }
步驟三:使用註解 ,主要是配置 在 controller類名,controller方法 和 實體類這三個地方 demo:
controller 類名上
@Api(value = 「用戶註冊登陸接口」,tags = {「登陸註冊註銷的controller」}) public class UserController{}
controller類 方法名上
@ApiOperation:用在請求的方法上,說明方法的用途、做用 - value=「說明方法的用途、做用」 - notes=「方法的備註說明」 案例: @ApiOperation(value = 「用戶註冊接口」, notes=「這是用戶註冊的接口,隨便寫均可以」) public ServerResponse register(@RequestBody Users user){ return iUserService.register(user); }
controller 類方法參數上
重點 兩大類:
1.@RequestParam ⇒ @ApiImplicitParams 使用@ApiImplicitParams來定義參數
@ApiImplicitParams({ @ApiImplicitParam(name="name",value="內存名",dataType="string", paramType = "query"), })
2.@RequestBody ⇒ @ApiModelProperty(value = "用戶名",name = "username",example = "admin",required = true) :注:這裏是在對應的實體類上的各個屬性上添加註解
區別:一個是在實體類上添加註解@ApiModelProperty 一個是在方法 參數上面添加註解@ApiImplicitParams
更多使用,參考 這裏
標題3:Mybatis 整合
意圖:這個是經常使用的持久層框架,雖然spring-data-jpa也是很優秀的。可是我本身在工做中這個用的比較多一點。
SpringBoot 整合 Mybatis 有兩種經常使用的方式,一種就是咱們常見的 xml 的方式 ,還有一種是全註解的方式。
如何選擇:在 SQL 語句不太長的狀況下,我以爲全註解的方式必定是比較清晰簡潔的。可是,複雜的 SQL 確實不太適合和代碼寫在一塊兒,那麼就使用xml文件的形式。其實這兩個方法也沒差。
步驟1:添加依賴
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency>
步驟2:配置 application.properties
server.port=9099 # 暫時使用SpringBoot2 自帶的 HikariCP 鏈接池,後面結合Druid spring.datasource.url=jdbc:mysql://192.168.1.200:3306/test2?useUnicode=true&characterEncoding=UTF-8&useSSL=false spring.datasource.username=db spring.datasource.password=xxx spring.datasource.driver-class-name=com.mysql.jdbc.Driver #Mybatis 配置 mybatis.config-location=classpath:mybatis-config.xml mybatis.mapper-locations=classpath*:/mappers/**.xml mybatis.type-aliases-package=com.liuge36.emr.entity
步驟3:resources 下新建mybatis-config.xml ,並創建本身的entity包
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 配置全局屬性 --> <settings> <!-- 使用jdbc的getGeneratedKeys獲取數據庫自增主鍵值 --> <setting name="useGeneratedKeys" value="true" /> <!-- 使用列標籤替換列別名 默認:true --> <setting name="useColumnLabel" value="true" /> <!-- 開啓駝峯命名轉換:Table{create_time} -> Entity{createTime} --> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> </configuration>
步驟4:測試 新建dao包,新建MemoryDao接口
import cn.com.zdmedical.emr.entity.Memory; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** * Description: TODO * * @Author: 留歌36 * @Date: 2019-11-28 09:10 */ @Mapper public interface MemoryDao { /** 根據名字查找內存信息 */ Memory findMemoryByName(@Param("name") String name); }
xml 實現:
<?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="com.liuge36.emr.dao.MemoryDao"> <select id="findMemoryByName" parameterType="String" resultType="com.liuge36.emr.entity.Memory"> SELECT * FROM memory WHERE name = #{name} </select> </mapper>
其他的就是基本的常規業務操做了。 註解的方式:
@Mapper public interface UserDao { /** * 經過名字查詢用戶信息 */ @Select("SELECT * FROM user WHERE name = #{name}") User findUserByName(@Param("name") String name); /** * 查詢全部用戶信息 */ @Select("SELECT * FROM user") List<User> findAllUser(); /** * 插入用戶信息 */ @Insert("INSERT INTO user(name, age,money) VALUES(#{name}, #{age}, #{money})") void insertUser(@Param("name") String name, @Param("age") Integer age, @Param("money") Double money); /** * 根據 id 更新用戶信息 */ @Update("UPDATE user SET name = #{name},age = #{age},money= #{money} WHERE id = #{id}") void updateUser(@Param("name") String name, @Param("age") Integer age, @Param("money") Double money, @Param("id") int id); /** * 根據 id 刪除用戶信息 */ @Delete("DELETE from user WHERE id = #{id}") void deleteUser(@Param("id") int id); }
因此,其實SpringBoot整合這些框架的 基本 使用仍是很簡單的。
標題4:Druid 數據庫鏈接池 整合
https://github.com/alibaba/druid
阿里巴巴數據庫事業部出品,爲監控而生的數據庫鏈接池
Druid是Java語言中最好的數據庫鏈接池。Druid可以提供強大的監控和擴展功能。
步驟1:添加依賴
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency>
步驟2:配置 application.properties
#spring.datasource.url=jdbc:mysql://192.168.1.200:3306/test2?useUnicode=true&characterEncoding=UTF-8&useSSL=false #spring.datasource.username=root #spring.datasource.password=xx #spring.datasource.driver-class-name=com.mysql.jdbc.Driver # 這4個參數key裏不帶druid也能夠,便可以還用上面的這個4個參數 spring.datasource.druid.url=jdbc:mysql://192.168.1.200:3306/test2?useUnicode=true&characterEncoding=UTF-8&useSSL=false spring.datasource.druid.username=root spring.datasource.druid.password=xx spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver # 初始化時創建物理鏈接的個數 spring.datasource.druid.initial-size=5 # 最大鏈接池數量 spring.datasource.druid.max-active=30 # 最小鏈接池數量 spring.datasource.druid.min-idle=5 # 獲取鏈接時最大等待時間,單位毫秒 spring.datasource.druid.max-wait=60000 # 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 spring.datasource.druid.time-between-eviction-runs-millis=60000 # 鏈接保持空閒而不被驅逐的最小時間 spring.datasource.druid.min-evictable-idle-time-millis=300000 # 用來檢測鏈接是否有效的sql,要求是一個查詢語句 spring.datasource.druid.validation-query=SELECT 1 FROM DUAL # 建議配置爲true,不影響性能,而且保證安全性。申請鏈接的時候檢測,若是空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測鏈接是否有效。 spring.datasource.druid.test-while-idle=true # 申請鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。 spring.datasource.druid.test-on-borrow=false # 歸還鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。 spring.datasource.druid.test-on-return=false # 是否緩存preparedStatement,也就是PSCache。PSCache對支持遊標的數據庫性能提高巨大,好比說oracle。在mysql下建議關閉。 spring.datasource.druid.pool-prepared-statements=true # 要啓用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改成true。 spring.datasource.druid.max-pool-prepared-statement-per-connection-size=50 # 配置監控統計攔截的filters,去掉後監控界面sql沒法統計 spring.datasource.druid.filters=stat,wall # 經過connectProperties屬性來打開mergeSql功能;慢SQL記錄 spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 # 合併多個DruidDataSource的監控數據 spring.datasource.druid.use-global-data-source-stat=true
步驟3:訪問 http://127.0.0.1:9099/druid/index.html 打開mysql客戶端navicat的sql窗口,執行show full processlist,顯示以下內容:
能夠看到,啓動項目後,直接建立5個數據鏈接,這是由application.properties配置文件中spring.datasource.druid.initial-size=5控制的。
步驟4:druid監控 在步驟3咱們能夠看到,瀏覽器輸入http://127.0.0.1:9099/druid/index.html直接就能看到druid控制檯界面,在這裏面能夠看到不少項目信息,若是任憑用戶隨意訪問,很是危險。咱們能夠經過配置,設置只有經過登陸認證才能夠訪問。
在application.properties配置文件中增長:
# druid鏈接池監控 spring.datasource.druid.stat-view-servlet.login-username=admin spring.datasource.druid.stat-view-servlet.login-password=admin # 排除一些靜態資源,以提升效率 spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
再次訪問:http://127.0.0.1:9099/druid/login.html 輸入 admin /admin 進去
標題5:通用工具類+通用返回
4個經常使用JSON類庫分別爲:Gson,FastJson,Jackson,Json-lib
步驟1:添加依賴
<dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.9.13</version> </dependency>
步驟2:修改配置文件
# 屬性爲 空(」」) 或者爲 NULL 都不序列化 spring.jackson.default-property-inclusion=non_empty
步驟3:新建 common 包 在包下新建:ResponseCode
/** * Description: * * @author: 留歌36 * Date:2018/11/4 16:04 */ public enum ResponseCode { SUCCESS(200,"成功"), ERROR(1,"錯誤"), NEED_REGISTER(10,"須要註冊,請受權登陸!"), NEED_LOGIN(12,"須要登陸,請登陸!"), TOMANYLOGIN(11,"帳號被擠出."), ILLEGAL_ARGUMENT(2,"ILLEGAL_ARGUMENT"); private final int code; private final String desc; ResponseCode(int code, String desc){ this.code=code; this.desc=desc; } public int getCode(){ return code; } public String getDesc(){ return desc; } }
新建通用返回對象:
import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.map.annotate.JsonSerialize; import java.io.Serializable; /** * Description: * * @author: 留歌36 * Date:2018/11/4 16:03 */ @JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) //保證序列化json的時候,若是是null的對象,key也會消失 public class ServerResponse<T> implements Serializable{ private int status; private String msg; private T data;//能夠指定泛型裏面的內容,也能夠不指定,並且裏面的類型能夠是多種,map,list,string //編寫外部訪問的Public方法,以前須要寫一個枚舉類 //這樣外部的顯示的就是這幾個值啦 public int getStatus(){ return status; } public String getMsg(){ return msg; } public T getData(){ return data; } //判斷是否登錄成功 @JsonIgnore public boolean isSuccess(){ return this.status == ResponseCode.SUCCESS.getCode(); } //編寫 私有 的構造方法,外部是不能new的 // 開放供外部使用的Public方法 private ServerResponse(int status){ this.status=status; } private ServerResponse(int status, T data){ this.status=status; this.data=data; } private ServerResponse(int status, String msg){ this.status=status; this.msg=msg; } private ServerResponse(int status, String msg, T data){ this.status=status; this.msg=msg; this.data=data; } //編寫成功靜態的方法供外部的調用 public static <T> ServerResponse<T> createBySuccess(){ return new ServerResponse<T>(ResponseCode.SUCCESS.getCode()); } public static <T> ServerResponse<T> createBySuccess(T data){ return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),data); } public static <T> ServerResponse<T> createBySuccess(String msg,T data){ return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),msg,data); } public static <T> ServerResponse<T> createBySuccessMessage(String msg){ return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(),msg); } //編寫失敗的方法 public static <T> ServerResponse<T> createByError(){ return new ServerResponse<T>(ResponseCode.ERROR.getCode(),ResponseCode.ERROR.getDesc()); } public static <T> ServerResponse<T> createByErrorMessage(String errorMessage) { return new ServerResponse<T>(ResponseCode.ERROR.getCode(),errorMessage); } public static <T> ServerResponse<T> createByErrorCodeMessage(int errorcode,String erroeMessage){ return new ServerResponse<T>(errorcode,erroeMessage); } public static <T> ServerResponse<T> createByErrorNeeDLogin(String erroeMessage){ return new ServerResponse<T>(ResponseCode.NEED_REGISTER.getCode(),erroeMessage); } }
容許全局跨域:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; /** * Description: 配置全局跨域 * * @Author: 留歌36 * @Date: 2019-11-28 11:45 */ @Configuration public class GlobalCorsConfig { private CorsConfiguration buildConfig() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedOrigin("*"); corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); corsConfiguration.setAllowCredentials(true); return corsConfiguration; } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", buildConfig()); return new CorsFilter(source); } }
SQL樣例:
create database imooc_homepage_sc; -- 用戶信息表 create table if not exists `imooc_homepage_sc`.`homepage_user` ( `id` bigint(20) not null auto_increment comment '自增ID', `username` varchar(128) not null default '' comment '用戶名', `email` varchar(128) not null default '' comment '用戶郵箱', `create_time` datetime not null default '1970-01-01 08:00:00' comment '建立時間', `update_time` datetime not null default '1970-01-01 08:00:00' comment '更新時間', primary key(`id`), unique key `key_username` (`username`) )engine=InnoDB auto_increment=1 default charset=utf8 row_format=compact comment='用戶信息表'; -- 用戶課程表 create table if not exists `imooc_homepage_sc`.`homepage_user_course` ( `id` bigint(20) not null auto_increment comment '自增ID', `user_id` bigint(20) not null default 0 comment '用戶 ID', `course_id` bigint(20) not null default 0 comment '課程 ID', `create_time` datetime not null default '1970-01-01 08:00:00' comment '建立時間', `update_time` datetime not null default '1970-01-01 08:00:00' comment '更新時間', primary key(`id`), unique key `key_user_course` (`user_id`, `course_id`) )engine=InnoDB auto_increment=1 default charset=utf8 row_format=compact comment='用戶課程表'; -- 課程表 create table if not exists `imooc_homepage_sc`.`homepage_course` ( `id` bigint(20) not null auto_increment comment '自增ID', `course_name` varchar(128) not null default '' comment '課程名稱', `course_type` varchar(128) not null default '' comment '課程類型', `course_icon` varchar(128) not null default '' comment '課程圖標', `course_intro` varchar(128) not null default '' comment '課程介紹', `create_time` datetime not null default '1970-01-01 08:00:00' comment '建立時間', `update_time` datetime not null default '1970-01-01 08:00:00' comment '更新時間', primary key(`id`), unique key `key_course_name` (`course_name`) )engine=InnoDB auto_increment=1 default charset=utf8 row_format=compact comment='課程表';
未完待續~
更多好文:https://blog.csdn.net/liuge36
原文出處:https://www.cnblogs.com/liuge36/p/11950446.html