上一節中已經包DAO層編寫完成了,所謂的DAO層就是所有和數據訪問的部分都應該放在這個層里,它負責與數據庫打交道。對於一個web項目來說,大概由這幾部分組成:
1. 前台的顯示層。
2. 分發處理請求的web層,這一層來用一些MVC框架。
3. 負責業務邏輯處理的Service層。
4. 負責與數據庫交互的DAO層
這樣有利於代碼的分離,以前上課時各種聽不懂,但書上有句話記得很清楚,那就是代碼的設計原則應該是“低耦合,高內聚”,MVC框架的設計正好體現了這個原則。廢話不多說,開始編碼。
第一步:Service接口的設計與實現
在org.seckill目錄下新建service包用來存放我們要編寫的service接口和實現類,新建exception包用來存放這個項目中我們自己定義業務的異常,新建dto包來存放業務數據傳輸對象。雖然老師這么說,但是后兩個包的具體用處還不是很理解,先繼續往下做,做完后再來分析。目錄結構
做到這里我又卡殼了,不知道該怎么繼續,不對照視頻的話還是有點生疏。那么就靜下心好好思考下吧。對於一個項目來說,主要的是完成各種用戶所需的功能,這些功能對應到程序中的就是各種接口和函數的定義,有的方法要有數據交互即有的方法要去存取修改數據庫。對於這些功能來說,都是在處理用戶的各種請求,服務器收到請求后,所做的第一件事情便是判斷該怎么處理這個請求呢?該用什么樣的方法來實現用戶這個請求所需要的功能呢?那么這一步就是一個"判斷—分發"的過程,使請求轉向服務器后端相應的方法去處理,這個功能主要是有MVC框架來完成。那么請求被轉到后端相應的方法去處理,對吧?那么對於請求的處理,即是一個業務功能的實現,一個方法的實現的話,本質上就是給一個方法(函數)一些東西(參數),然后再返回執行后的結果。對於方法來說可以分為兩部分:1. 業務邏輯 2. 數據交換。 也就是說方法在細分成這兩部分的話 可以得到 “方法—業務邏輯 方法—數據交換” 這兩個,那么我們可以將其相分離,業務邏輯的歸業務層,數據交換的歸DAO層,這樣可以很好的分離代碼,簡而言之就是權責分明的部門體系,各管各的,需要用到別的部門提供的服務時把這個部門的幾個相關的人叫過來就行了。
以上是作為菜鳥的我的一些理解,那么現在再想想下部該怎么做呢?有上面思考可以知道,無論是DAO還是Service層的設計,圍繞的是方法(接口)的設計,因為這就是項目要實現的功能,當你不知道要怎么往下做的時候,可以回想下自己到底想要做什么?我想要實現什么樣的功能呢?
在service包下建SeckillService接口,用來定義和Seckill(即秒殺商品)有關的業務操作(方法)。
SeckillService接口
1 package org.seckill.service; 2 3 import org.seckill.dto.Exposer; 4 import org.seckill.dto.SeckillExecution; 5 import org.seckill.entity.Seckill; 6 import org.seckill.exception.RepeatKillException; 7 import org.seckill.exception.SeckillCloseException; 8 import org.seckill.exception.SeckillException; 9 10 import java.util.List; 11 12 /** 13 * 業務接口:站在"使用者"角度設計接口 14 * 三個方面:方法定一粒度,參數,返回類型/異常 15 * Created by yuxue on 2016/10/15. 16 */ 17 public interface SeckillService { 18 19 /** 20 * 查詢所有秒殺記錄 21 * @return 22 */ 23 List<Seckill> getSeckillList( ); 24 25 /** 26 * 查詢單個秒殺記錄 27 * @param seckillId 28 * @return 29 */ 30 Seckill getById(long seckillId); 31 32 /** 33 * 秒殺開啟時輸出秒殺接口地址 34 * 否則輸出系統時間和秒殺時間 35 * @param seckillId 36 */ 37 Exposer exportSeckillUrl(long seckillId); 38 39 /** 40 * 執行秒殺操作 41 * @param seckillId 42 * @param userPhone 43 * @param md5 44 */ 45 SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) 46 throws SeckillException,RepeatKillException,SeckillCloseException; 47 48 /** 49 * 執行秒殺操作by 存儲過程 50 * @param seckillId 51 * @param userPhone 52 * @param md5 53 */ 54 SeckillExecution executeSeckillProdure(long seckillId, long userPhone, String md5); 55 }
關於這段的代碼的說明:
1.關於exportSeckillUrl(long seckillId)這個方法,這個方法的功能是:當用戶點商品詳情頁面想要秒殺商品時,如果在秒殺時間范圍之內就顯示就輸出秒殺接口地址讓前端顯示,否則輸出系統時間和秒殺時間。這里中要的是它的返回值,是個Exposer類的實例,Exposer是定義在dto包下的:
業務數據傳輸對象Exposer
1 package org.seckill.dto; 2 3 /**暴露秒殺地址DTO 4 * Created by yuxue on 2016/10/15. 5 */ 6 public class Exposer { 7 8 //是否開啟秒殺 9 private boolean exposed; 10 11 //一種加密措施 12 private String md5; 13 14 //id 15 private long seckillId; 16 17 //系統當前時間(毫秒) 18 private long now; 19 20 //開啟時間 21 private long start; 22 23 //結束時間 24 private long end; 25 26 27 public Exposer(boolean exposed, String md5, long seckillId) { 28 this.exposed = exposed; 29 this.md5 = md5; 30 this.seckillId = seckillId; 31 } 32 33 public Exposer(boolean exposed, long seckillId,long now, long start, long end) { 34 this.exposed = exposed; 35 this.now = now; 36 this.seckillId=seckillId; 37 this.start = start; 38 this.end = end; 39 } 40 41 public Exposer(boolean exposed,long seckillId) { 42 this.exposed = exposed; 43 this.seckillId = seckillId; 44 } 45 46 public boolean isExposed() { 47 return exposed; 48 } 49 50 public void setExposed(boolean exposed) { 51 this.exposed = exposed; 52 } 53 54 public String getMd5() { 55 return md5; 56 } 57 58 public void setMd5(String md5) { 59 this.md5 = md5; 60 } 61 62 public long getSeckillId() { 63 return seckillId; 64 } 65 66 public void setSeckillId(long seckillId) { 67 this.seckillId = seckillId; 68 } 69 70 public long getNow() { 71 return now; 72 } 73 74 public void setNow(long now) { 75 this.now = now; 76 } 77 78 public long getStart() { 79 return start; 80 } 81 82 public void setStart(long start) { 83 this.start = start; 84 } 85 86 public long getEnd() { 87 return end; 88 } 89 90 public void setEnd(long end) { 91 this.end = end; 92 } 93 94 @Override 95 public String toString() { 96 return "Exposer{" + 97 "exposed=" + exposed + 98 ", md5='" + md5 + '\'' + 99 ", seckillId=" + seckillId + 100 ", now=" + now + 101 ", start=" + start + 102 ", end=" + end + 103 '}'; 104 } 105 }
前面說了,dto包是用來存放業務數據傳輸對象的,其中大部分與業務不相關,只是service返回的數據的封裝。對於exportSeckillUrl(long seckillId)這個方法,向前端輸出秒殺接口地址或者系統時間和這個商品的秒殺開始/結束時間,這里麻煩的一點便在於此,對於一個業務方法來說,他的返回可能比較復雜,會有多種數據成分,那么這時候需要將這些數據封裝成業務數據傳輸對象,這便是dto包里面的類的作用。對於這里的業務數據傳輸對象Exposer,封裝的信息有:1. 商品是否開啟標志位。2.加密字段MD5,這個字段由商品的id通過MD5加密算法生成,目的是防止數據被用戶使用的第三方工具篡改以及直接拼出秒殺地址。3. 商品的id,系統時間以及秒殺開始,結束時間。對應於這些字段定義了3個構造方法,對應不同的構造情況,因為可以秒殺以及不可以秒殺這兩種情況下需要向前端提供的數據不一樣。
接下來要分析的是executeSeckill(long seckillId, long userPhone, String md5)這個方法,這個方法負責具體的執行秒殺操作,返回的類型也是個業務數據傳輸對象SeckillExecution,這里為什么不直接返回個布爾型數據來表示執行秒殺操作成功與否呢?為什么要用個復雜的封裝類型呢?我自己理解是:對於一個方法的返回值的設計,你要看是誰調用了這個方法。在web項目中,調用Service層的業務的是web層,這層的具體任務就根據前端的請求調用相應的service來處理,處理完之后從service層中拿處理結果數據給前端顯示。對於執行秒殺操作這個業務方法,他的具體方法負責執行秒殺操作,對於調用它的web層,應給他提供執行秒殺操作后的相關信息,好讓它傳給前端顯示,所以這里如果只提供一個表示執行秒殺操作成功與否的布爾型值的話是肯定不夠的。
SeckillExecution
1 package org.seckill.dto; 2 3 import org.seckill.entity.SuccessKilled; 4 import org.seckill.enums.SeckillStatEnum; 5 6 /**封裝秒殺后執行的結果 7 * Created by yuxue on 2016/10/15. 8 */ 9 public class SeckillExecution { 10 11 private long seckillId; 12 13 //秒殺執行結果狀態 14 private int state; 15 16 //狀態展示 17 private String stateInfo; 18 19 //秒殺成功對象 20 private SuccessKilled successKilled; 21 22 public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) { 23 this.seckillId = seckillId; 24 this.state = statEnum.getState(); 25 this.stateInfo = statEnum.getStateInfo(); 26 this.successKilled = successKilled; 27 } 28 29 public SeckillExecution(long seckillId, SeckillStatEnum statEnum) { 30 this.seckillId = seckillId; 31 this.state = statEnum.getState(); 32 this.stateInfo = statEnum.getStateInfo(); 33 } 34 35 @Override 36 public String toString() { 37 return "SeckillExecution{" + 38 "seckillId=" + seckillId + 39 ", state=" + state + 40 ", stateInfo='" + stateInfo + '\'' + 41 ", successKilled=" + successKilled + 42 '}'; 43 } 44 45 public long getSeckillId() { 46 return seckillId; 47 } 48 49 public void setSeckillId(long seckillId) { 50 this.seckillId = seckillId; 51 } 52 53 public int getState() { 54 return state; 55 } 56 57 public void setState(int state) { 58 this.state = state; 59 } 60 61 public String getStateInfo() { 62 return stateInfo; 63 } 64 65 public void setStateInfo(String stateInfo) { 66 this.stateInfo = stateInfo; 67 } 68 69 public SuccessKilled getSuccessKilled() { 70 return successKilled; 71 } 72 73 public void setSuccessKilled(SuccessKilled successKilled) { 74 this.successKilled = successKilled; 75 } 76 }
還有一點是這里把執行秒殺操作后的相關信息單獨封裝成對象,里面有各個秒殺商品的秒殺執行結果狀態,狀態展示等信息,這樣前端在顯示商品秒殺信息的時候直接從這里去取就好了,不必再通過其他的方法去service層一個一個字段的取,使得數據之間耦合度低。
SeckillExecution里有不同的構造方法,這是為了對應不同的情況,比如說如果商品秒殺成功了,那么則需要秒殺商品的id,狀態展示,以及秒殺成功對象這三個字段,如果秒殺失敗,則只需要秒殺商品的id,狀態展示便可以了。
這里的秒殺執行結果狀態使用枚舉類演示的,開發過程有個原則就是如果程序中用到了一些常量,那么最好將這些常量放到一處,在程序中引用這些常量即可,這樣便於修改。
在在org.seckill目錄下新建enums包,存放我們建的枚舉類。
枚舉類SeckillStatEnum
1 package org.seckill.enums; 2 3 /** 4 * 使用枚舉表述常量數據 5 * Created by yuxue on 2016/10/15. 6 */ 7 public enum SeckillStatEnum { 8 //枚舉的使用 9 SUCCESS(1,"秒殺成功"), 10 END(0,"秒殺結束"), 11 REPEAT_KILL(-1,"重復秒殺"), 12 INNER_ERROR(-2,"系統異常"), 13 DATA_REWRITE(-3,"數據篡改"); 14 15 private int state; 16 17 private String stateInfo; 18 19 SeckillStatEnum(int state, String stateInfo) { 20 this.state = state; 21 this.stateInfo = stateInfo; 22 } 23 24 public int getState() { 25 return state; 26 } 27 28 public String getStateInfo() { 29 return stateInfo; 30 } 31 32 public static SeckillStatEnum stateOf(int index){ 33 for(SeckillStatEnum state:values()){ 34 if(state.getState()==index){ 35 return state; 36 } 37 } 38 return null; 39 } 40 }
接口和枚舉定義完了,現在到了接口的實現
SeckillServiceImpl
1 package org.seckill.service.impl; 2 3 import org.seckill.dao.SeckillDao; 4 import org.seckill.dao.SuccessKilledDao; 5 import org.seckill.dto.Exposer; 6 import org.seckill.dto.SeckillExecution; 7 import org.seckill.entity.Seckill; 8 import org.seckill.entity.SuccessKilled; 9 import org.seckill.enums.SeckillStateEnum; 10 import org.seckill.exception.RepeatKillException; 11 import org.seckill.exception.SeckillCloseException; 12 import org.seckill.exception.SeckillException; 13 import org.seckill.service.SeckillService; 14 import org.slf4j.Logger; 15 import org.slf4j.LoggerFactory; 16 import org.springframework.beans.factory.annotation.Autowired; 17 import org.springframework.stereotype.Service; 18 import org.springframework.transaction.annotation.Transactional; 19 import org.springframework.util.DigestUtils; 20 21 import java.util.Date; 22 import java.util.List; 23 24 /** 25 * Created by yuxue on 2016/11/7. 26 */ 27 @Service 28 public class SeckillServiceImpl implements SeckillService{ 29 private Logger logger = LoggerFactory.getLogger(this.getClass()); 30 31 @Autowired 32 SeckillDao seckillDao; 33 @Autowired 34 SuccessKilledDao successKilledDao; 35 36 //md5鹽值字符串,用於混淆MD5 37 private String salty="sfsafas((888__```"; 38 39 public List<Seckill> getSeckillList( ) { 40 return seckillDao.queryAllSeckill(0,4); 41 } 42 43 public Seckill querySeckill(int seckillId) { 44 return seckillDao.queryById(seckillId); 45 } 46 47 //暴露秒殺接口地址的實現 48 public Exposer exportSeckillUrl(long seckillId) { 49 Seckill seckill=seckillDao.queryById(seckillId); 50 if(seckill==null){ 51 return new Exposer(false,seckillId); 52 } 53 Date now=new Date(); 54 Date start=seckill.getStartTime(); 55 Date end=seckill.getEndTime(); 56 if(now.getTime()<start.getTime()||now.getTime()>end.getTime()){ 57 return new Exposer(false,seckillId,now,start,end); 58 } 59 String md5=getMD5(seckillId); 60 return new Exposer(true,md5,seckillId); 61 } 62 63 //根據秒殺商品id來生成MD5密鑰 64 private String getMD5(long seckillId){ 65 //拼接規則 66 String base=seckillId+"/"+salty; 67 String MD5= DigestUtils.md5DigestAsHex(base.getBytes()); 68 return MD5; 69 } 70 71 //執行秒殺方法的實現 72 @Transactional 73 public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) 74 throws SeckillException, RepeatKillException, SeckillCloseException { 75 if(md5==null||!md5.equals(getMD5(seckillId))){ 76 throw new SeckillException("seckill data rewrite"); 77 } 78 Date now=new Date(); 79 try { 80 int count = seckillDao.reduceNumber(seckillId, now); 81 if (count <= 0) { 82 throw new SeckillCloseException("秒殺關閉"); 83 } else { 84 int update = successKilledDao.insertSuccessSeckilled(seckillId, userPhone); 85 if (update <= 0) { 86 throw new RepeatKillException("重復秒殺"); 87 }else{ 88 SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 89 return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS,successKilled); 90 } 91 } 92 }catch (SeckillCloseException e){ 93 throw e; 94 }catch (RepeatKillException e){ 95 throw e; 96 }catch (Exception e){ 97 logger.error(e.getMessage(),e); 98 throw new SeckillException("Seckill inner error"+e.getMessage()); 99 } 100 } 101 }
分析:
1. private Logger logger = LoggerFactory.getLogger(this.getClass()); 使用日志。
2. String MD5= DigestUtils.md5DigestAsHex(base.getBytes());使用java提供的API來生成MD5密鑰
3. 定義了3種異常SeckillException, RepeatKillException, SeckillCloseException分別是秒殺異常,重復秒殺,秒殺關閉。注意這里並不是程序的運行異常,是我們自定義的業務異常,這是一種思路:將業務中可能出現的我們不允許的部分如重復秒殺
等作為異常拋出再對應與相對的異常捕捉,分類處理。這個項目里首先是將所有的業務異常定義為SeckillException,在以它為父類細化為RepeatKillException和SeckillCloseException這兩個異常。
4. @Transactional注解將執行秒殺這個方法聲明為一個事務。
在org.seckill目錄下新建exception包用來存放我們的自定義的異常
SeckillException
1 package org.seckill.exception; 2 3 /** 4 * 秒殺相關業務異常 5 * Created by yuxue on 2016/10/15. 6 */ 7 public class SeckillException extends RuntimeException{ 8 public SeckillException(String message) { 9 super(message); 10 } 11 12 public SeckillException(String message, Throwable cause) { 13 super(message, cause); 14 } 15 }
注意這里要繼承RuntimeException,因為對於java事務來說只有運行時異常時它才會回滾。
RepeatKillException
1 package org.seckill.exception; 2 3 /**重復秒殺異常(運行期異常) 4 * 5 * 6 * Spring事務只會接收運行期異常並回滾 7 * Created by yuxue on 2016/10/15. 8 */ 9 public class RepeatKillException extends SeckillException{ 10 11 public RepeatKillException(String message){ 12 super(message); 13 } 14 15 public RepeatKillException(String message, Throwable cause){ 16 super(message,cause); 17 } 18 }
SeckillCloseException
1 package org.seckill.exception; 2 3 /** 4 * Created by yuxue on 2016/10/15. 5 */ 6 public class SeckillCloseException extends SeckillException{ 7 public SeckillCloseException(String message) { 8 super(message); 9 } 10 11 public SeckillCloseException(String message, Throwable cause) { 12 super(message, cause); 13 } 14 }
第二步:基於Spring托管Service實現類,使用聲明式事務
讓Spring的IOC容器托管Service實現類,主要的是一些配置工作。通過對象工廠和依賴管理來達到一致性的訪問接口。
在resources目錄的spring文件目錄下新建spring-service.xml配置文件,區別於spring-dao.xml,表明這個配置文件是用來配置service層的。
spring-service.xml
1 <beans xmlns="http://www.springframework.org/schema/beans" 2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns:context="http://www.springframework.org/schema/context" 4 xmlns:tx="http://www.springframework.org/schema/tx" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans.xsd 7 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd 8 http://www.springframework.org/schema/tx 9 http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"> 10 <!--掃描service包下所有使用注解的類型--> 11 <context:component-scan base-package="org.seckill.service"/> 12 13 <!--配置事務管理--> 14 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 15 <property name="dataSource" ref="datasource"/> 16 </bean> 17 18 <!--配置基於注解的聲明式事務 19 默認使用注解來管理事務行為 20 --> 21 <tx:annotation-driven transaction-manager="transactionManager"/> 22 23 </beans>
使用注解控制事務方法的優點以及注意的事項:
1. 開發團隊一致的約定。
2. 保證事務方法的執行時間經可能的短,不要穿插其他網絡操作RPC/HTTP請求或者剝離到事務方法外部,使得這個事務方法是個比較干凈的對數據庫的操作。
3. 不是所有的方法都需要事務,如只有一條修改操作,只讀操作不需要事務控制。
第三步: Service層集成測試
下面是測試用例SeckillServiceTest
1 package org.seckill.service; 2 3 import org.junit.Test; 4 import org.junit.runner.RunWith; 5 import org.seckill.dto.Exposer; 6 import org.seckill.dto.SeckillExecution; 7 import org.seckill.entity.Seckill; 8 import org.seckill.exception.RepeatKillException; 9 import org.seckill.exception.SeckillCloseException; 10 import org.slf4j.Logger; 11 import org.slf4j.LoggerFactory; 12 import org.springframework.beans.factory.annotation.Autowired; 13 import org.springframework.test.context.ContextConfiguration; 14 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 15 16 import java.util.List; 17 18 import static org.junit.Assert.*; 19 20 /** 21 * Created by yuxue on 2016/10/15. 22 */ 23 @RunWith(SpringJUnit4ClassRunner.class) 24 @ContextConfiguration({ 25 "classpath:spring/spring-dao.xml", 26 "classpath:spring/spring-service.xml"}) 27 public class SeckillServiceTest { 28 private final Logger logger= LoggerFactory.getLogger(this.getClass()); 29 30 @Autowired 31 private SeckillService seckillService; 32 @Test 33 public void getSeckillList() throws Exception { 34 List<Seckill> list=seckillService.getSeckillList(); 35 logger.info("list={}",list);//這里的{}是個占位符 36 } 37 38 @Test 39 public void getById() throws Exception { 40 long id=1004; 41 Seckill seckill=seckillService.getById(id); 42 logger.info("seckill={}",seckill); 43 } 44 45 //集成測試代碼完整邏輯,注意可重復執行 46 @Test 47 public void exportSeckillLogic() throws Exception { 48 long id=1005; 49 Exposer exposer=seckillService.exportSeckillUrl(id); 50 if(exposer.isExposed()) { 51 logger.info("exposer={}", exposer); 52 long phone=243242343L; 53 String md5=exposer.getMd5(); 54 try{ 55 SeckillExecution seckillExecution=seckillService.executeSeckill(id,phone,md5); 56 logger.info("result={}",seckillExecution); 57 }catch (RepeatKillException e){ 58 logger.error(e.getMessage()); 59 }catch(SeckillCloseException e){ 60 logger.error(e.getMessage()); 61 } 62 }else{ 63 logger.warn("exposer={}",exposer); 64 } 65 } 79 }
關於遇到的問題:
在自動注入seckillService時我把接口寫成了實現類SeckillServiceImpl結果報錯了,即spring在這里要注入接口,而注入接口的實現類就會報錯,如果在SeckillServiceImpl中將implements SeckillService刪除的話便能執行通過。網上搜了下這其中的原因,總結如下:
1. Spring的依賴注入功能使用Spring的動態代理機制來實現,而spring動態代理功能的實現是基於Java的動態代理機制的,jdk規定動態代理必須用接口,反而類注入則要通過cglib進行動態代理。
2. 為什么在SeckillServiceImpl中將implements SeckillService刪除的話便能執行通過?這里或許要涉及到在有接口和無接口情況下,Spring動態代理機制執行的不同。於是自己嘗試了下,其結果:
(1)如果有接口,則注入后類型是
說明是由Java的動態代理機制Proxy.newProxyInstance()方法創建一個代理對象來代理指定的類,個方法有三個參數:newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h),第一個參數為要代理的類,第二 個參數為這個類的接口(。。),第三個暫且不管,說明java實現動態代理的時候要求必須有接口類。
(2)如果沒有用接口,即在SeckillServiceImpl中將implements SeckillService刪除然后用private SeckillServiceImpl這種方式注入的話,則注入后類型是
可見如果是實現類的方式的話,Spring的動態代理機制是使用cglib進行動態代理的,關於這其中具體的技術細節還要日后仔細分析才行。
Service層的學習總結:
這節真的學到了好多好多啊,照着視頻敲的確沒什么問題,自己手打就會各種出錯,再次強調:學編程一定要自己手敲代碼,這樣才能發現自己不懂的地方。這節中的一些分析是基於我個人的理解,因為我是個菜鳥所以可能有些錯誤,所以希望技術大神發現的話能指點指點,非常感謝。其中的一些技術細節如動態代理什么的以后還要寫寫博文來仔細分析分析下,多敲敲代碼,多思考,這樣才能提高自己的技術水平。下一節開始web層的設計與開發。