GPS定位系統(三)——Java後端 前言 正題 總結 關於作者

前言

GPS系列——Java端,github項目地址

前面已經瞭解或者實現了Android端的gps上傳定位信息,現在就差後臺的接口支持了。

我們需要數據庫來儲存上傳的定位信息,並且還要滿足不同的人的數據隔離,也就是用戶系統。

下面就給大家介紹Java端的主要實現和代碼,更多更詳細的內容,還是得看源碼。

對大家有用的知識,大家自行拷貝使用。

GPS定位系統系列

GPS定位系統(一)——介紹

GPS定位系統(二)——Android端

GPS定位系統(三)——Java後端

GPS定位系統(四)——Vue前端

GPS定位系統(五)——Docker

目錄

[TOC]

收穫

學習完這篇文章你將收穫:

  • springboot+mybatis的主流應用
  • jwt的token全局驗證
  • 上傳文件及其文件映射
  • 跨域問題處理
  • 全局異常處理
  • gps定位、用戶表設計
  • mybatis-generator配置

正題

一、Java技術框架

java8

springboot + mybatis

jwt

mysql

mybatis-generator

運用比較主流的springboot框架,數據庫框架mybatis,token驗證jwt,mysql5.7.0,mabatis-generator自動生成bean\dao\mapper

二、環境準備

java環境、mysql

建議使用docker來管理java的發佈、mysql、nginx,後面會有專門GPS定位系統(五)——Docker,來介紹容器下web、java、mysql等環境搭建。

三、開發步驟

1、創建數據庫,設計創建數據庫表

2、配置好datasource的連接信息,application.yml裏面datasource

3、配置mybatis-generator的config配置文件,並生成bean、Mapper的java文件和xml文件

4、編寫controller所需接口

5、全局異常處理

6、全局token驗證處理

7、測試、發佈

四、數據庫表

user:

CREATE TABLE `user` (
  `uid` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(40) NOT NULL,
  `password` varchar(40) NOT NULL,
  `token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
  `name` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
  `create_time` bigint NOT NULL,
  `update_time` bigint DEFAULT NULL,
  `mobile` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
  `gender` tinyint(1) DEFAULT '0' COMMENT '0:男 1:女',
  `user_role` tinyint(3) unsigned zerofill DEFAULT '000',
  `avatar` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
  `last_login_time` bigint DEFAULT NULL,
  PRIMARY KEY (`uid`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8;

gps:

CREATE TABLE `location` (
  `id` int NOT NULL AUTO_INCREMENT,
  `lat` double(20,10) NOT NULL,
  `lng` double(20,10) NOT NULL,
  `time` bigint DEFAULT NULL,
  `uid` bigint DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=340 DEFAULT CHARSET=utf8;

mybatis-generator配置

1、在根目錄下創建generatorConfig.xml文件

2、配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

    <context id="test" targetRuntime="MyBatis3">
        <plugin type="org.mybatis.generator.plugins.EqualsHashCodePlugin"></plugin>
        <plugin type="org.mybatis.generator.plugins.SerializablePlugin"></plugin>
        <plugin type="org.mybatis.generator.plugins.ToStringPlugin"></plugin>
        <commentGenerator>
            <!-- 這個元素用來去除指定生成的註釋中是否包含生成的日期 false:表示保護 -->
            <!-- 如果生成日期,會造成即使修改一個字段,整個實體類所有屬性都會發生變化,不利於版本控制,所以設置爲true -->
            <property name="suppressDate" value="true" />
            <!-- 是否去除自動生成的註釋 true:是 : false:否 -->
            <property name="suppressAllComments" value="true" />
        </commentGenerator>
        <!--數據庫鏈接URL,用戶名、密碼 -->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/gps?useSSL=false" userId="root" password="7632785">
            <!-- 這裏面可以設置property屬性,每一個property屬性都設置到配置的Driver上 -->
            <!--mysql 8會生成其他一些類 加上這個就不生成-->
            <property name="nullCatalogMeansCurrent" value="true"/>
        </jdbcConnection>
        <javaTypeResolver>
            <!-- This property is used to specify whether MyBatis Generator should
                force the use of java.math.BigDecimal for DECIMAL and NUMERIC fields, -->
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>
        <!-- 生成模型的包名和位置 -->
        <javaModelGenerator targetPackage="com.jafir.springboot.service.model" targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>
        <!-- 生成映射文件的包名和位置 -->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>
        <!-- 生成DAO的包名和位置 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.jafir.springboot.service.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <!-- 要生成哪些表 -->
        <!--<table tableName="user" domainObjectName="User" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" />-->
        <!--<table tableName="location" domainObjectName="Location" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" />-->
        <table tableName="setting" domainObjectName="Setting" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" />

    </context>
</generatorConfiguration>

pom.xml

<plugin>
  <groupId>org.mybatis.generator</groupId>
  <artifactId>mybatis-generator-maven-plugin</artifactId>
  <version>1.3.7</version>
  <executions>
    <execution>
      <id>Generate MyBatis Artifacts</id>
      <phase>deploy</phase>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
  </executions>
  <!-- 配置數據庫鏈接及mybatis generator core依賴 生成mapper時使用 -->
  <dependencies>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.11</version>
    </dependency>
  </dependencies>
</plugin>   

注意:如果在maven打包的時候,會再次調用mybatis-generator一次,會重複生成文件。這時候,需要配置execution的goals,這樣就不會再次生成一份文件。

 <executions>
   <execution>
     <id>Generate MyBatis Artifacts</id>
     <phase>deploy</phase>
     <goals>
       <goal>generate</goal>
     </goals>
   </execution>
</executions>

3、雙擊【maven】-【plugins】-【mybatis-generator】-【mybatis-generator:generate】使用

注意:如果已經生成了,再次會重複生成文件,不會覆蓋。java文件是會多生成一份,到時候可以拷貝即可使用;xml mapper文件會內容疊加一部分,到時候可以刪除原來的,即爲最新的。

五、接口功能

userController

  • 獲取所有註冊controller的url

  • 創建用戶

  • 更新用戶

  • 刪除用戶

  • 登錄

  • 獲取所有用戶

  • 上傳頭像文件

特殊接口的mapper:

<select id="getUsers" resultType="com.jafir.springboot.service.model.result.AllUserResult">
    SELECT * FROM user
  </select>

  <select id="getUserByName" resultType="com.jafir.springboot.service.model.User">
    SELECT * FROM user where username = #{0}
  </select>

gpsController

  • 上傳gps信息
  • 獲取某用戶的gps定位
  • 獲取所有用戶的gps定位
  • 獲取某用戶的軌跡數據

特殊接口所需的mapper:

<select id="getAllNowGps" resultType="com.jafir.springboot.service.model.result.NowGpsResult">
    SELECT
        l.uid,
        lat,
        lng,
        avatar,
        name,
        time
    FROM (SELECT DISTINCT *
          FROM location
          ORDER BY uid, time
              DESC) AS l LEFT JOIN user ON l.uid = user.uid
    GROUP BY l.uid;

</select>

此sql較爲複雜一些,意爲獲取所有用戶的最後一次上傳的位置信息,即爲獲取所有用戶的實時定位

接口功能都比較簡單,主要就是提供這些接口供移動端、web端使用

注意:

  • 例如登錄、獲取用戶信息等接口,一般數據庫查出來都是user類型的數據,但是敏感字段password等,是不能返回的,可以直接把數據庫查出的User對象,setPassword(null)
  • 登錄成功後,使用jwt用username\userId生成token,返回
  • 做了token驗證之後,很多接口,如果是請求自身數據,不需要再傳userId,header獲取token,使用jwt可以獲取userId、userName等

例如:

   @RequestMapping(value = "/get_info", method = {RequestMethod.POST, RequestMethod.GET})
    @ResponseBody
    public ResponseResult<User> getUserInfo(@RequestHeader(value = "token") String token) {
        LogUtil.info("token:" + token);
        String userId = JwtUtil.getUserId(token);
        LogUtil.info("userId:" + userId);
        User user = userService.getUserById(Long.valueOf(userId));
        //去掉密碼
        user.setPassword("");
        if (user != null) {
            return ResponseUtil.makeOK(user);
        }
        return ResponseUtil.makeErr();
    }

六、上傳頭像文件

@RequestMapping(value = "/upload", method = RequestMethod.POST)
    @ResponseBody
    public String upload(MultipartFile file) throws Exception {
        System.out.print(file.getOriginalFilename());
        System.out.print(file.getSize());

        File localFile = new File("/Users/jafir/Downloads/upload", file.getOriginalFilename());
        if (!localFile.getParentFile().exists()) {
            localFile.getParentFile().mkdirs();
        }
        if (!localFile.exists()) {
            localFile.createNewFile();
        }
        file.transferTo(localFile);

        String returnUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath() + "/res/" + localFile.getName();

        System.out.print("return url:" + returnUrl);
        return returnUrl;
    }

一般上傳文件後都要返回其url,這裏需要在application.yaml配置靜態資源映射。

spring:
  mvc:
    static-path-pattern: /res/**
  resources:
    static-locations: classpath:/static/ , file:/Users/jafir/Downloads/upload/ #靜態資源配置

映射 : classpath:/static/ xxx => /res/ xxx ; /Users/jafir/Downloads/upload/ xxx => /res/ xxx

classpath路徑爲相對路徑,相對於jar包中class存放的目錄(可以把打的jar包解壓,然後就可以看到class和satic目錄)

這樣的話上傳就會傳到/Users/jafir/Downloads/upload/下,訪問的話直接localhost:9090/res/xxxx.png就能訪問

這裏沒有使用oss等雲存儲,一般情況下,最好是雲存儲。這裏存在本地磁盤上面,是在系統文件的根目錄爲起點的文件目錄/User/jafir/Downloads/upload下面(當前是我mac的目錄,大家可以自行配置)。

這樣做的好處是:開發環境下,文件上傳到固定的地方,並且不會隨着項目的clean而丟失;線上環境下,文件也會存在於相應服務器的根目錄爲起點的文件目錄下。

七、token驗證

public class TokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        LogUtil.info("url:" + request.getRequestURI());

                //解決axios ajax跨域問題
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            LogUtil.info("OPTIONS:" + request.getRequestURI());
            return true;
        }

        String token = request.getHeader("token");
        LogUtil.info("token:" + token);
        if (null != token) {
            boolean result = JwtUtil.verify(token);
            if (result) {
                //存在且正確 不攔截
                return true;
            }
        }
        //不存在或者錯誤拋異常
        throw new TokenException();
    }

}

注意:關於axios或者ajax請求接口的時候,一般情況會首先發出一個OPTIONS的請求來刺探是否能夠請求成功,如果自身沒有OPTIONS這類型的接口的話,我們TokenInterceptor應該放行此請求行爲,不然的話會驗證不通過

WebAppConfig.java文件

@Configuration
public class WebAppConfig implements WebMvcConfigurer{
//    這裏不推薦用 support,因爲它會是spring本身的自動配置失效 影響較大
//public class WebAppConfig extends WebMvcConfigurationSupport{
  
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenInterceptor())
                .addPathPatterns("/**").
                excludePathPatterns("/error","/login","/create_user",
                        "/getAllUrl","/test1","/test.html","/res/**");
    }

}

excludePathPatterns可以配置放行一些不需要驗證的接口,例如登錄、獲取資源等

注意:在配置WebAppConfig的時候,注意最好使用@Configuration+實現WebMvcConfigurer接口的方式。因爲如果使用WebMvcConfigurationSupport的話,會造成spring自身的autoConfiguartion被覆蓋,導致一些自動配置失效。所以,最新版的spring實現方式,最好用這種方式。

jwt:

public class JwtUtil {
    /**
     * 過期時間一天,
     * TODO 正式運行時修改爲15分鐘
     */
    private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;
    /**
     * token私鑰
     */
    private static final String TOKEN_SECRET = "f26e587c28064d0e855e72c0a6a0e618";

    /**
     * 校驗token是否正確
     *
     * @param token 密鑰
     * @return 是否正確
     */
    public static boolean verify(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 獲得token中的信息無需secret解密也能獲得
     *
     * @return token中包含的用戶名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("loginName").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 獲取登陸用戶ID
     *
     * @param token
     * @return
     */
    public static String getUserId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("userId").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成簽名,15min後過期
     *
     * @param username 用戶名
     * @return 加密的token
     */
    public static String sign(String username, String userId) {
        //            過期時間
        Date date = new Date(System.currentTimeMillis() + 30*EXPIRE_TIME);
//            私鑰及加密算法
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
//            設置頭部信息
        Map<String, Object> header = new HashMap<>(2);
        header.put("typ", "JWT");
        header.put("alg", "HS256");
        // 附帶username,userId信息,生成簽名
        return JWT.create()
                .withHeader(header)
                .withClaim("loginName", username)
                .withClaim("userId", userId)
                .withExpiresAt(date)
                .sign(algorithm);
    }

}   

八、跨域問題

@Configuration
public class WebAppConfig implements WebMvcConfigurer{
    /**
     * 頁面跨域訪問Controller過濾
     *
     * @return
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        WebMvcConfigurer.super.addCorsMappings(registry);
        registry.addMapping("/**")
                .allowedHeaders("*")
                .allowedMethods("*")
//                .allowedOrigins("http://localhost:8083","http://localhost:8080");
                .allowedOrigins("*");

    }
}

一般情況,對於跨域問題,後端的處理方式爲配置cors。但是要注意:在前後端分離的跨域配置裏面,origin的配置尤其重要。一般設置爲前端的域名,這樣會安全的多。

九、全局異常處理

@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 系統異常處理
     * 系統錯誤 500
     * 業務錯誤 400
     * 請求不存在 404
     * token失效 401
     *
     * @throws Exception
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ResponseResult defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        LogUtil.error(e.toString());
        // todo 寫入日誌

        ResponseResult responseResult;
        if (e instanceof BusinessException) {
            responseResult = ResponseUtil.makeErr(((BusinessException) e).getMsg());
        } else if (e instanceof TokenException) {
            responseResult = ResponseUtil.make401Err();
        } else if (e instanceof org.springframework.web.servlet.NoHandlerFoundException) {
            responseResult = ResponseUtil.make404Err();
        } else {
            responseResult = ResponseUtil.make500Err();
        }
        return responseResult;
    }
}
@Controller
public class ErrorController implements org.springframework.boot.web.servlet.error.ErrorController {

    @RequestMapping(value = "/error", method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public ResponseResult error(HttpServletRequest request, HttpServletResponse response) {
        //採用/error直接返回404json的方式,這樣瀏覽器出現404則爲json而不是404空白網頁
        return ResponseUtil.make404Err();
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }
}

目前後端處理了401、404、500這幾種異常類型,尤其要注意404,因爲它比較特殊。當時開發的時候在對401、404的問題上進行一番探究。

按道理:如果一個不存在(沒有註冊controller)請求,一般情況話應該是404;一個存在的請求,並且沒有token驗證通過,纔會是401

如果,我們不去處理404的話,直接被tokenInterceptor處理了,最終會返回的是401錯誤,而不是404。所以,我們需要在異常處理裏面處理好404的異常。

postman

運行之後,就可以用postman訪問localhost:9090/xxx來測試接口了

這裏再教大家一個方法,設置全局的token。因爲,登錄之後,很多接口都是需要token來訪問的,所以全局變量token無疑是最好的方式,postman也支持。

設置response的全局變量設置

//把json字符串轉化爲對象
var data=JSON.parse(responseBody);

//獲取data對象的utoken值。
var token=data.data.token;

//設置成全局變量
pm.globals.set("token", token);

使用token

OK 這樣的話,登錄成功之後,其他需要token的接口就可以直接使用啦。

打包

使用maven插件install即可在target下生成demo-0.0.1-SNAPSHOT.jar包,jar的名字可以通過pom.xml來修改

<groupId>com.jafir.springboot</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>

支持https

在application.yaml中配置

server:
  ssl:
    key-store-password: 7632785
    key-store-type: PKCS12
    key-store: /Users/jafir/Downloads/upload/cert/*.keep999.cn.pfx

我這裏使用的pkcs12的證書,證書是使用arme.sh免費申請的。tomcat支持jks、pkcs12(pkx)的證書,arme.sh也可以直接轉成pkcs的證書,詳情可以參看Docker nginx https二級域名無端口訪問多個web項目

路徑的話可以是絕對路徑,也可以是相對路徑。在服務器docker容器中,所以我用的是絕對路徑,映射的服務器的證書文件目錄。

大致提下創建容器命令

docker run --name gps -p 9090:9090 -d -v /mydockerdata/java/gps/upload:/Users/jafir/Downloads/upload    -v /mydockerdata/arme/out/*.keep999.cn/*.keep999.cn.pfx:/Users/jafir/Downloads/upload/cert/*.keep999.cn.pfx      gps:1.0 

十、關於前後端分離

現在越來越多的大中型項目都開始採用前後端分離的模式來進行開發了,前後端分離,有這些好處:

1、前端網頁加載更快,容災增強,解耦,減輕服務器壓力

2、代碼分離,前端較爲熱門,更新快,改動頻繁,拆分之後更易迭代更新,更多去處理用戶交互、性能優化方面的問題;後端較爲穩定,純接口提供,更多去處理微服務、數據庫優化、分佈式、容災、大數據方面的問題(術業有專攻,職責清晰)

3、後端數據可供移動端、前端、甚至桌面應用等多端使用

4、分離之後,對於微服務方面可以做更多的擴展,爲擴展後分布式下的高效、穩定可靠的運行打下基礎

總結

java後端功能較爲簡單,即爲純提供數據接口。對於java後端,其實更多的學問和難度在於面對大數量級的處理,分佈式、微服務、集羣管理、負載均衡、容災系統、安全架構、日常監控、數據庫優化、高併發處理、消息中間件等等很多東西。此項目是指基礎的練手項目,以後的路還很長啊,所以,要加油啊💪

接下來,數據也有了,用戶管理也有了,基本的東西都湊齊了。就差前端web來展示地圖相關的實時定位、歷史軌跡、用戶管理等功能展示了。

請移步GPS定位系統(四)——Vue前端

關於作者

作者是一個熱愛學習、開源、分享,傳播正能量,喜歡打籃球、頭髮還很多的程序員-。-

熱烈歡迎大家關注、點贊、評論交流!

簡書:https://www.jianshu.com/u/d234d1569eed

github:https://github.com/fly7632785

CSDN:https://blog.csdn.net/fly7632785

掘金:https://juejin.im/user/5efd8d205188252e58582dc7/posts

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章