1. 程式人生 > >“南航順風車”

“南航順風車”

第一週(2018/7/16 - 2018/7/23) 

明確介面定義,和前端溝通好了URL格式(比如:http://localhost:8080/login?name= ) ,請求體(position、seats、id、name之類的)和返回型別(比如我的登入介面返回的是String)。然後開始各自開發,前後端的分離。

作為一枚後臺小白,java以及框架都成為我要學習的模組。導師列出了一系列的技術難點,比如springboot、springMVC、restful API、JSON語言等等。由於對介面是什麼都不清楚,我前期花了大量時間去了解springboot和介面。包括,一個完整的springboot專案目錄應該如下:

com
  +- example
    +- myproject
      +- Application.java
      |
      +- domain
      |  +- Customer.java
      |  +- CustomerRepository.java
      |
      +- service
      |  +- CustomerService.java
      |
      +- controller
      |  +- CustomerController.java
      |

1、Application.java 建議放到跟目錄下面,主要用於

做一些框架配置

2、domain目錄主要用於實體(Entity)與資料訪問層(Repository)

3、service 層主要是業務類程式碼

4、controller 負責頁面訪問控制

此外,Spring Boot的基礎結構共三個檔案:

l. src/main/java  程式開發以及主程式入口

ll. src/main/resources 配置檔案

lll. src/test/java  測試程式

第一週之後,我才認識到:“URL”格式和請求體都是後端定義的,前端按照這個格式傳引數即可。如(http://localhost:8080/login?name=)前端可以在後面寫“張三”那麼就把對應的字串POST到了後端。而URL的格式在後端controller是通過註解@RequestMapping(value = "/login")來自主定義的。

第二週(2018/7/24 - 2018/7/31)

經過第一週的配置環境和軟體下載 ,並且在看了一大堆案例的情況下,對springboot框架有了較深的理解。接著我下了一個sqlyog圖形介面方便建表和查看錶內欄位值,下載了postman作http操作(Get\Post\Update\Delete)。對了順便列一下software:

開發語言:JAVA 、 Spring Data JPA、少量原生SQL語句

輕量級框架:springboot

編輯器:ecplise(STS)外掛 -->Spring Starrer Project        

PS:下次要試下主流的Intellj IDEA,xml檔案還是比application.properties要便捷很多,可以合併某些欄位頭

HTTP操作模擬器:Postman

附一下專案的配置(application.properties):

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/csu?useUnicode=true&characterEncoding=UTF8
spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.driverClassName = com.mysql.jdbc.Driver
 
 
# \u914D\u7F6E\u6570\u636E\u5E93
spring.jpa.database = MYSQL
# \u67E5\u8BE2\u65F6\u662F\u5426\u663E\u793A\u65E5\u5FD7
spring.jpa.show-sql = true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = update
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect

第二週遇到的比較困難的點在於JPA,專案前半段三張表“司機表--使用者表--乘客表”。JPA語言雖然不需要寫任何一句原生sql語句,並且通過一些註解可以實現自增和外來鍵約束。但是JPA的關聯查詢及其複雜。難點如下:

要訪問某個表,JPA需要三個類:(1)實體類 (2)資料介面repository (一般要繼承JPARepository或者CURDRepository) (3)Controller類。裡面的方法與一般的java方法區別僅在於頂部有路由路徑,並且是由@requestmapping註解宣告的。還有一個很致命的問題在此提一下:不同的Controller類的方法不可以相互呼叫!對於普通的java類,可以new一個物件來繼承別的類就可以使用它的成員及其方法。但在springboot中你是不可以的,那怎麼解決呢?

比如A controller類的B方法要呼叫 C controller類的D方法。你只能在A類下宣告C類controller對應的資料介面物件c1,然後把D方法寫在A類中(此時B、D方法都在一個controller裡,但同時D方法可以在A類裡使用到資料介面c1)。示例如下:

       public class DistanceGetController {

	  	@Autowired
	    private  RestTemplate restTemplate;
	  	@Autowired
		private PassengerInfoRepository passengerRepositoy;
	  	@Autowired
		private DriverInfoRepository driverRepositoy;

在整個專案完成過程中,給我很大困擾的,是JPA外來鍵的約束問題。先來解答幾個問題:

1.什麼是外來鍵,外來鍵有什麼要求,多對一情況下外來鍵設定在哪?

外來鍵是不同表之間建立關聯的重要手段,外來鍵欄位可以不是主鍵但一定要建立索引,但被外來鍵引用的欄位一定要是該表的主鍵。多對一、一對多情況下外來鍵都是設定在“多”的一方。

2.JPA外來鍵怎麼設定?

    @Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private int id;
	@NotNull
	private String name;
	@OneToMany(cascade={CascadeType.ALL},fetch = FetchType.LAZY,mappedBy = "user")
	private List<DriverInfo> driver;
    @JsonManagedReference
	public List<DriverInfo> getDriver() {
		return driver;
	}

	public void setDriver(List<DriverInfo> driver) {
        this.driver = driver;
    }
public class DriverInfo {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private int id;
	@NotNull
	private String startPlace;
    @ManyToOne
	@JoinColumn(name = "user_id")

	private LoginInfo user;

	@JsonBackReference
	public LoginInfo getUser() {
		return user;
	}

	public void setUser(LoginInfo user) {
		this.user = user;
	}

由一些註解組成,一對多、多對一的時候,@OneToMany 和@ManyToOne 不要混淆,此外一定要在自己的實體類裡寫上對方的

欄位。在ORM模型是通過JPA的外來鍵欄位對映到另一張表的,這裡特別神奇。比如有司機表實體類driverInfo。先new一個物件:driverInfo aa = new driverInfo(); 然後通過aa.getUser().getName() 可以從司機表對映查詢到另外一種表使用者表的name欄位。但是

JPA的關聯查詢 存在很大的問題 :json序列化及反序列化。在“順風車”專案中,司機表和乘客表都雙向關聯著使用者表。

A<---> B <---> C 意味著:在查詢A表的時候會帶出B表,同時B表還可以帶出A表(雙向)這時候便產生了死迴圈。花了差不多一週的時間 都沒能找到支援雙向關聯查詢的解決辦法。最後為了防止序列化,只能加上上圖的註解,抑制雙向關聯,不再死鎖。

第三週(2018/8/1 - 2018/8/8  )

經過兩週的時間 ,把司機、乘客釋出行程的介面寫好了。在和導師溝通過後,決定要做兩件事:(1)統一異常處理 (2)登入資訊存放在session 裡面。在session這一部分,也遇到了一些困難。session的儲存其實很簡單,寥寥幾行程式碼,用到httpsession的reponse和request即可,這裡也可以記錄下:

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
		HttpSession session = (HttpSession) request.getSession();
        //下面是存進session
        session.setAttribute("userId", user.getId());//前面是鍵,後面是值

        //從session取
        int userId = (int) session.getAttribute("userId");//取出session值
        system.out.println(userId);//輸出來看看~

一般作登入介面的時候,都會用到session或者cookie。我在專案中用session是想把使用者表對應id對映到外來鍵欄位裡。十分的方便。

開始 著手寫匹配度介面,經過2天的討論,我們把演算法確定了一下:先呼叫高德開發平臺/路徑規劃/駕駛路徑API,算出 順風車司機行程的距離distance,接著將乘客起點終點設定為途經點,計算出新距離distance 1。然後自定義匹配度規範:多走的距離distance-distance1 每 0.5km 減掉1%的匹配度,並且只對前端返回附近的匹配度 >50% 的司機/乘客。

記錄下這部分演算法的難點吧:

(1)第一次接觸到後臺呼叫第三方API。原本一直以為前端js才可以呼叫api,後來瞭解到當後臺需要第三方資料時,後臺可以當做前端,給第三方發請求,這是一樣的道理。這裡使用的是導師介紹的Resttemplate:

//主要程式碼就兩句
//lat 、lng是經緯度變數 
String url="https://restapi.amap.com/v3/direction/driving?origin="+lat+","+lng+"&destination="+lat1+","+lng1+"&extensions=all&output=json&key=924c44ade82fbc30ab8e19ffba3c67e6";//呼叫第三方API的url
JSONObject json = restTemplate.getForEntity(url, JSONObject.class).getBody();//接收返回的JSON資料

(2)JSON解析

拿到第三方返回的JSON資料時,第一反應是發了條朋友圈,但是冷靜下來後發現解析真的繁瑣。我們知道,JSON資料流是由JSONArray以及JSONObject兩種形成,對應表現形式是{ }和[ ] 。那麼對應的解析方法可以詳見https://www.cnblogs.com/xudong-bupt/archive/2013/05/06/3060745.html(轉載“旭東的部落格“)。簡而言之,需要從上往下一層層對陣列及object物件進行解析。提一下object解析吧,由於是由“鍵-值“組成,所以只能通過鍵來提取出值。

(3)當前司機,遍歷乘客表乘客,計算對應的匹配度,並返回>50%的 乘客

首先,解決下當前司機的問題。在這裡我用的是全域性變數 。在司機提交行程介面取出司機的經緯度,儲存在全域性變數m1、n1、m2、n2

其次,是遍歷的問題,如雨後春筍搬冒出許多問題:

  • 由於設定表關聯,單表的id不是連續的。如A表插入id=110的資料,B表插入的就會是id=111,下次再往A表新增資料時,插入的是id=112,因此僅僅寫一個for迴圈是不夠的,因為id++不成立(id不連續)

       解決辦法:找到第一個元素輸出其物件 ,用getId()獲取乘客表第一行的id;用count方法獲取總行數。

  • 雖然解決了for迴圈的起始問題,但是便利的時候還是無法確定id。經過一天的時候(那天有點笨maybe...)決定用findALL 和get(i)方法分別獲取i次的id,再進行取經緯度 操作。
        Long b1 = driverRepositoy.count();//獲取總行數
		String[] dis1 = new String[(int) (b1+1-1)];
		
		for(int i=0; i < b1 ;i++){
			
			List<DriverInfo> abc = driverRepositoy.findAll();
			DriverInfo def= abc.get(i);//獲取乘客表第i個元素
			int a1 = def.getId(); //id
			
		DriverInfo driverEntity = driverRepositoy.findDriverInfoById(a1);
		double lat2 = driverEntity.getStartPosition2();
		double lng2 = driverEntity.getStartPosition1();
		double lat3 = driverEntity.getEndPosition2();
		double lng3 = driverEntity.getEndPosition1();
  • 一開始打算構建一張新表,關於“乘客資訊以及匹配度”,再寫一個介面讓前端GET。但是當一切都寫好了之後,包括我把匹配度從單個數組取出,將乘客資訊取出,設定斷點發現都能存進實體類。但是最後的最後無法執行save操作,即將實體類資料儲存到資料庫。檢視save官方文件發現,save(entity)的實體只能是前端接收的形參組成的。也就是說該方法引數形式必須是(@requestbody 資料型別 形參)。而前端傳過來的資料是json資料,我嘗試將資料用json封裝,但是依舊無法save。

        解決辦法:改為自行封裝json,並直接返回前端。使用JPA的save方法優點在於:把資料儲存在資料庫後,再用JPA語句返回前端是會自行封裝好的。而手動封裝容易出錯,而且繁瑣。我最後封裝的格式是:[{object1} , {object2},{object3} ]

  • 對於匹配度50%以上的篩選,是用一個if else 語句及continue來完成的。如果大於50%則執行jsonobject.put()操作,否則continue使id++
  • 優化演算法的時候,發現全域性變數的設定對於使用者量大的情況,可能會出現因大資料產生訂單覆蓋的問題。最後總結出三種改進思路:(1)訊息佇列  (2)執行緒池  (3)預先設定約束條件,篩減符合的乘客數量,減少遍歷次數

        解決辦法:最後我並沒有選擇以上幾種,我還是選擇最熟悉的Session機制。Sesion優點在於使用者唯一性,客戶端(伺服器)會對每一個訪問的使用者分配一個新的session物件,這個物件有著唯一的session_id。因此,我把對應司機/乘客的訂單資訊(演算法只用到經緯度資訊)儲存在Session裡面再對應取出來。這樣就不會在A司機提交行程的時候,出現被B司機行程覆蓋的問題。

第四周(2018/8/9 - 2018/8/16)

繼續往下走,完善智慧匹配路線模組。這部分想讓前端響應使用者操作,跳轉新介面。

實現功能如下:前端點選選中乘客,則把該司機和該乘客的經緯度POST後臺,後臺再按照前端需要的格式封裝返回給前端。前端呼叫高德路線顯示API,展示接單後路線。這部分要注意把乘客起點終點當成途經點。

ps:忍不住想吐槽一下前端partner,我感覺應該小資料流的儲存是沒有問題的。發後臺轉前端實在增加時間複雜度。

暫時寫到這啦,第一篇技術部落格。後臺小白,衝鴨!