利用Spring Boot如何開(kāi)發(fā)REST服務(wù)詳解
REST服務(wù)介紹
RESTful service是一種架構(gòu)模式,近幾年比較流行了,它的輕量級(jí)web服務(wù),發(fā)揮HTTP協(xié)議的原生的GET,PUT,POST,DELETE。 REST模式的Web服務(wù)與復(fù)雜的SOAP和XML-RPC對(duì)比來(lái)講明顯的更加簡(jiǎn)潔,越來(lái)越多的web服務(wù)開(kāi)始采用REST風(fēng)格設(shè)計(jì)和實(shí)現(xiàn)。例如,Amazon.com提供接近REST風(fēng)格的Web服務(wù)進(jìn)行圖書(shū)查找;雅虎提供的Web服務(wù)也是REST風(fēng)格的。REST 并非始終是正確的選擇。 它作為一種設(shè)計(jì) Web 服務(wù)的方法而變得流行,這種方法對(duì)專(zhuān)有中間件(例如某個(gè)應(yīng)用程序服務(wù)器)的依賴(lài)比基于 SOAP 和 WSDL 的方法更少。 在某種意義上,通過(guò)強(qiáng)調(diào)URI和HTTP等早期 Internet 標(biāo)準(zhǔn),REST 是對(duì)大型應(yīng)用程序服務(wù)器時(shí)代之前的 Web 方式的回歸。
如下圖示例:

使用REST的關(guān)鍵是如何抽象資源,抽象得越精確,對(duì)REST的應(yīng)用就越好。
REST服務(wù)關(guān)鍵原則:
1. 給一切物體一個(gè)ID
2.連接物體在一起
3.使用標(biāo)準(zhǔn)方法
4.資源多重表述
5.無(wú)狀態(tài)通信
本文介紹如何基于Spring Boot搭建一個(gè)簡(jiǎn)易的REST服務(wù)框架,以及如何通過(guò)自定義注解實(shí)現(xiàn)Rest服務(wù)鑒權(quán)
搭建框架
pom.xml
首先,引入相關(guān)依賴(lài),數(shù)據(jù)庫(kù)使用mongodb,同時(shí)使用redis做緩存
注意:這里沒(méi)有使用tomcat,而是使用undertow
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <!--redis支持--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--mongodb支持--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>
引入spring-boot-starter-web支持web服務(wù)
引入spring-boot-starter-data-redis 和spring-boot-starter-data-mongodb就可以方便的使用mongodb和redis了
配置文件
profiles功能
為了方便 區(qū)分開(kāi)發(fā)環(huán)境和線上環(huán)境,可以使用profiles功能,在application.properties里增加
spring.profiles.active=dev
然后增加application-dev.properties作為dev配置文件。
mondb配置
配置數(shù)據(jù)庫(kù)地址即可
spring.data.mongodb.uri=mongodb://ip:port/database?readPreference=primaryPreferred
redis配置
spring.redis.database=0 # Redis服務(wù)器地址 spring.redis.host=ip # Redis服務(wù)器連接端口 spring.redis.port=6379 # Redis服務(wù)器連接密碼(默認(rèn)為空) spring.redis.password= # 連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制) spring.redis.pool.max-active=8 # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制) spring.redis.pool.max-wait=-1 # 連接池中的最大空閑連接 spring.redis.pool.max-idle=8 # 連接池中的最小空閑連接 spring.redis.pool.min-idle=0 # 連接超時(shí)時(shí)間(毫秒) spring.redis.timeout=0
數(shù)據(jù)訪問(wèn)
mongdb
mongdb訪問(wèn)很簡(jiǎn)單,直接定義接口extends MongoRepository即可,另外可以支持JPA語(yǔ)法,例如:
@Component
public interface UserRepository extends MongoRepository<User, Integer> {
public User findByUserName(String userName);
}
使用時(shí),加上@Autowired注解即可。
@Component
public class AuthService extends BaseService {
@Autowired
UserRepository userRepository;
}
Redis訪問(wèn)
使用StringRedisTemplate即可直接訪問(wèn)Redis
@Component
public class BaseService {
@Autowired
protected MongoTemplate mongoTemplate;
@Autowired
protected StringRedisTemplate stringRedisTemplate;
}
儲(chǔ)存數(shù)據(jù):
.stringRedisTemplate.opsForValue().set(token_key, user.getId()+"",token_max_age, TimeUnit.SECONDS);
刪除數(shù)據(jù):
stringRedisTemplate.delete(getFormatToken(accessToken,platform));
Web服務(wù)
定義一個(gè)Controller類(lèi),加上RestController即可,使用RequestMapping用來(lái)設(shè)置url route
@RestController
public class AuthController extends BaseController {
@RequestMapping(value = {"/"}, produces = "application/json;charset=utf-8", method = {RequestMethod.GET, RequestMethod.POST})
@ResponseBody
public String main() {
return "hello world!";
}
}
現(xiàn)在啟動(dòng),應(yīng)該就能看到hello world!了
服務(wù)鑒權(quán)
簡(jiǎn)易accessToken機(jī)制
提供登錄接口,認(rèn)證成功后,生成一個(gè)accessToken,以后訪問(wèn)接口時(shí),帶上accessToken,服務(wù)端通過(guò)accessToken來(lái)判斷是否是合法用戶(hù)。
為了方便,可以將accessToken存入redis,設(shè)定有效期。
String token = EncryptionUtils.sha256Hex(String.format("%s%s", user.getUserName(), System.currentTimeMillis()));
String token_key = getFormatToken(token, platform);
this.stringRedisTemplate.opsForValue().set(token_key, user.getId()+"",token_max_age, TimeUnit.SECONDS);
攔截器身份認(rèn)證
為了方便做統(tǒng)一的身份認(rèn)證,可以基于Spring的攔截器機(jī)制,創(chuàng)建一個(gè)攔截器來(lái)做統(tǒng)一認(rèn)證。
public class AuthCheckInterceptor implements HandlerInterceptor {
}
要使攔截器生效,還需要一步,增加配置:
@Configuration
public class SessionConfiguration extends WebMvcConfigurerAdapter {
@Autowired
AuthCheckInterceptor authCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
super.addInterceptors(registry);
// 添加攔截器
registry.addInterceptor(authCheckInterceptor).addPathPatterns("/**");
}
}
自定義認(rèn)證注解
為了精細(xì)化權(quán)限認(rèn)證,比如有的接口只能具有特定權(quán)限的人才能訪問(wèn),可以通過(guò)自定義注解輕松解決。在自定義的注解里,加上roles即可。
/**
* 權(quán)限檢驗(yàn)注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthCheck {
/**
* 角色列表
* @return
*/
String[] roles() default {};
}
檢驗(yàn)邏輯:
只要接口加上了AuthCheck注解,就必須是登陸用戶(hù)
如果指定了roles,則除了登錄外,用戶(hù)還應(yīng)該具備相應(yīng)的角色。
String[] ignoreUrls = new String[]{
"/user/.*",
"/cat/.*",
"/app/.*",
"/error"
};
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
// 0 檢驗(yàn)公共參數(shù)
if(!checkParams("platform",httpServletRequest,httpServletResponse)){
return false;
}
// 1、忽略驗(yàn)證的URL
String url = httpServletRequest.getRequestURI().toString();
for(String ignoreUrl :ignoreUrls){
if(url.matches(ignoreUrl)){
return true;
}
}
// 2、查詢(xún)驗(yàn)證注解
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 查詢(xún)注解
AuthCheck authCheck = method.getAnnotation(AuthCheck.class);
if (authCheck == null) {
// 無(wú)注解,不需要
return true;
}
// 3、有注解,先檢查accessToken
if(!checkParams("accessToken",httpServletRequest,httpServletResponse)){
return false;
}
// 檢驗(yàn)token是否過(guò)期
Integer userId = authService.getUserIdFromToken(httpServletRequest.getParameter("accessToken"),
httpServletRequest.getParameter("platform"));
if(userId==null){
logger.debug("accessToken timeout");
output(ResponseResult.Builder.error("accessToken已過(guò)期").build(),httpServletResponse);
return false;
}
// 4、再檢驗(yàn)是否包含必要的角色
if(authCheck.roles()!=null&&authCheck.roles().length>0){
User user = authService.getUser(userId);
boolean isMatch = false;
for(String role : authCheck.roles()){
if(user.getRole().getName().equals(role)){
isMatch = true;
break;
}
}
// 角色未匹配,驗(yàn)證失敗
if(!isMatch){
return false;
}
}
return true;
}
服務(wù)響應(yīng)結(jié)果封裝
增加一個(gè)Builder,方便生成最終結(jié)果
public class ResponseResult {
public static class Builder{
ResponseResult responseResult;
Map<String,Object> dataMap = Maps.newHashMap();
public Builder(){
this.responseResult = new ResponseResult();
}
public Builder(String state){
this.responseResult = new ResponseResult(state);
}
public static Builder newBuilder(){
return new Builder();
}
public static Builder success(){
return new Builder("success");
}
public static Builder error(String message){
Builder builder = new Builder("error");
builder.responseResult.setError(message);
return builder;
}
public Builder append(String key,Object data){
this.dataMap.put(key,data);
return this;
}
/**
* 設(shè)置列表數(shù)據(jù)
* @param datas 數(shù)據(jù)
* @return
*/
public Builder setListData(List<?> datas){
this.dataMap.put("result",datas);
this.dataMap.put("total",datas.size());
return this;
}
public Builder setData(Object data){
this.dataMap.clear();
this.responseResult.setData(data);
return this;
}
boolean wrapData = false;
/**
* 將數(shù)據(jù)包裹在data中
* @param wrapData
* @return
*/
public Builder wrap(boolean wrapData){
this.wrapData = wrapData;
return this;
}
public String build(){
JSONObject jsonObject = new JSONObject();
jsonObject.put("state",this.responseResult.getState());
if(this.responseResult.getState().equals("error")){
jsonObject.put("error",this.responseResult.getError());
}
if(this.responseResult.getData()!=null){
jsonObject.put("data", JSON.toJSON(this.responseResult.getData()));
}else if(dataMap.size()>0){
if(wrapData) {
JSONObject data = new JSONObject();
dataMap.forEach((key, value) -> {
data.put(key, value);
});
jsonObject.put("data", data);
}else{
dataMap.forEach((key, value) -> {
jsonObject.put(key, value);
});
}
}
return jsonObject.toJSONString();
}
}
private String state;
private Object data;
private String error;
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public ResponseResult(){}
public ResponseResult(String rc){
this.state = rc;
}
/**
* 成功時(shí)返回
* @param rc
* @param result
*/
public ResponseResult(String rc, Object result){
this.state = rc;
this.data = result;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
調(diào)用時(shí)可以?xún)?yōu)雅一點(diǎn)
@RequestMapping(value = {"/user/login","/pc/user/login"}, produces = "application/json;charset=utf-8", method = {RequestMethod.GET, RequestMethod.POST})
@ResponseBody
public String login(String userName,String password,Integer platform) {
User user = this.authService.login(userName,password);
if(user!=null){
// 登陸
String token = authService.updateToken(user,platform);
return ResponseResult.Builder
.success()
.append("accessToken",token)
.append("userId",user.getId())
.build();
}
return ResponseResult.Builder.error("用戶(hù)不存在或密碼錯(cuò)誤").build();
}
protected String error(String message){
return ResponseResult.Builder.error(message).build();
}
protected String success(){
return ResponseResult.Builder
.success()
.build();
}
protected String successDataList(List<?> data){
return ResponseResult.Builder
.success()
.wrap(true) // data包裹
.setListData(data)
.build();
}
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Java Socket編程筆記_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
Socket對(duì)于我們來(lái)說(shuō)就非常實(shí)用了。下面是本次學(xué)習(xí)的筆記。主要分異常類(lèi)型、交互原理、Socket、ServerSocket、多線程這幾個(gè)方面闡述2017-05-05
Java負(fù)載均衡策略的實(shí)現(xiàn)詳解
這篇文章主要介紹了Java負(fù)載均衡策略的實(shí)現(xiàn),負(fù)載均衡在Java領(lǐng)域中有著廣泛深入的應(yīng)用,不管是大名鼎鼎的nginx,還是微服務(wù)治理組件如dubbo,feign等,負(fù)載均衡的算法在其中都有著實(shí)際的使用,需要的朋友可以參考下2022-07-07
詳解SpringBoot啟動(dòng)項(xiàng)目后執(zhí)行方法的幾種方式
在項(xiàng)目開(kāi)發(fā)中某些場(chǎng)景必須要用到啟動(dòng)項(xiàng)目后立即執(zhí)行方式的功能,本文主要聊聊實(shí)現(xiàn)立即執(zhí)行的幾種方法,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09
springboot中的controller注意事項(xiàng)說(shuō)明
這篇文章主要介紹了springboot中的controller注意事項(xiàng)說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
Java用BigDecimal解決double類(lèi)型相減時(shí)可能存在的誤差
這篇文章主要介紹了Java用BigDecimal解決double類(lèi)型相減時(shí)可能存在的誤差,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
Java C++題解leetcode886可能的二分法并查集染色法
這篇文章主要為大家介紹了Java C++題解leetcode886可能的二分法并查集染色法實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10

