『高階篇』docker之開發使用者服務EdgeService(13)
上一節開發了使用者服務,即將開發的是使用者服務EdgeService,從這個呼叫關係,可以看到使用者的EdgeService是一個服務的服務,首選呼叫使用者服務,對使用者資訊基本的操作,呼叫資訊服務實現傳送簡訊,傳送郵件,還需要實現登入和註冊的功能,並且登入是一個單點登入需要支援其他的系統,支援課程的登入的EdgeService,對他的要求是無狀態的,還需要集中式的快取redis。這麼多服務集中於一身說明它是一個非常複雜的服務,不過也沒關係,我們從頭到尾把他開發完成。原始碼:https://github.com/limingios/msA-docker
新建maven模組user-edge-service
- 引入user-thrift-service-api 和 message-thrift-service-api的pom檔案
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.3.RELEASE</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.idig8</groupId> <artifactId>user-edge-service</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId> <version>0.10.0</version> </dependency> <dependency> <groupId>com.idig8</groupId> <artifactId>user-thrift-service-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.idig8</groupId> <artifactId>message-thrift-service-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> </dependencies> </project>
-
redis的工具類,用於無狀態的儲存,token資訊 儲存用的userInfo
RedisConfig
package com.idig8.user.redis; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; /** * Redis快取配置類 */ @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.password}") private String password; //快取管理器 @Bean public CacheManager cacheManager(RedisTemplate redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); //設定快取過期時間 cacheManager.setDefaultExpiration(10000); return cacheManager; } @Bean public JedisConnectionFactory redisConnectionFactory() { JedisConnectionFactory factory = new JedisConnectionFactory(); factory.setHostName(host); factory.setPort(port); factory.setTimeout(timeout); factory.setPassword(password); return factory; } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){ StringRedisTemplate template = new StringRedisTemplate(factory); setSerializer(template);//設定序列化工具 template.afterPropertiesSet(); return template; } private void setSerializer(StringRedisTemplate template){ Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setValueSerializer(jackson2JsonRedisSerializer); } }
RedisClient
package com.idig8.user.redis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; /** * Created by liming */ @Component public class RedisClient { @Autowired private RedisTemplate redisTemplate; public <T> T get(String key) { return (T)redisTemplate.opsForValue().get(key); } public void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } public void set(String key, Object value, int timeout) { redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); } public void expire(String key, int timeout) { redisTemplate.expire(key, timeout, TimeUnit.SECONDS); } }
- Response 和 LoginResponse 統一返回字串
Response
package com.idig8.user.response; import java.io.Serializable; /** * Created by liming */ public class Response implements Serializable { public static final Response USERNAME_PASSWORD_INVALID = new Response("1001", "username or password invalid"); public static final Response MOBILE_OR_EMAIL_REQUIRED = new Response("1002", "mobile or email is required"); public static final Response SEND_VERIFYCODE_FAILED = new Response("1003", "send verify code failed"); public static final Response VERIFY_CODE_INVALID = new Response("1004", "verifyCode invalid"); public static final Response SUCCESS = new Response(); private String code; private String message; public Response() { this.code = "0"; this.message = "success"; } public Response(String code, String message) { this.code = code; this.message = message; } public static Response exception(Exception e) { return new Response("9999", e.getMessage()); } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
LoginResponse
package com.idig8.user.response; /** * Created by liming */ public class LoginResponse extends Response { private String token; public LoginResponse(String token) { this.token = token; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } }
- 客戶端訪問通過和服務端相同的協議進行通訊
package com.idig8.user.thrift; import com.idig8.thrift.message.MessageService; import com.idig8.thrift.user.UserService; import org.apache.thrift.TServiceClient; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class ServiceProvider { @Value("${thrift.user.ip}") private String serverIp; @Value("${thrift.user.port}") privateint serverPort; @Value("${thrift.message.ip}") private String messageServerIp; @Value("${thrift.message.port}") private int messageServerPort; private enum ServiceType { USER, MESSAGE } public UserService.Client getUserService() { return getService(serverIp, serverPort, ServiceType.USER); } public MessageService.Client getMessasgeService() { return getService(messageServerIp, messageServerPort, ServiceType.MESSAGE); } public <T> T getService(String ip, int port, ServiceType serviceType) { TSocket socket = new TSocket(ip, port, 3000); TTransport transport = new TFramedTransport(socket); try { transport.open(); } catch (TTransportException e) { e.printStackTrace(); return null; } TProtocol protocol = new TBinaryProtocol(transport); TServiceClient result = null; switch (serviceType) { case USER: result = new UserService.Client(protocol); break; case MESSAGE: result = new MessageService.Client(protocol); break; } return (T)result; } }
-
controller 引入thrift的service方法和redis的操作工具類 用於redis的操作
> 因為userInfo是通過thrift自動升成的,裡面很多方法太過麻煩,不利於開發檢視資料內容,最好的方式自己建立一個物件,將自動升成userInfo轉換成自定義的UserDTO,USerDTO最好的方式是在thrift的工程中進行的。如果多個專案,比較方便,單獨的使用者的edgeservice中進行DTO的話,只能他自己用業務不清晰。
package com.idig8.user.controller; import com.idig8.thrift.user.UserInfo; import com.idig8.thrift.user.dto.UserDTO; import com.idig8.user.redis.RedisClient; import com.idig8.user.response.LoginResponse; import com.idig8.user.response.Response; import com.idig8.user.thrift.ServiceProvider; import org.apache.commons.lang.StringUtils; import org.apache.thrift.TException; import org.apache.tomcat.util.buf.HexUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import java.security.MessageDigest; import java.util.Random; @Controller public class UserController { @Autowired private ServiceProvider serviceProvider; @Autowired private RedisClient redisClient; @RequestMapping(value = "/login",method = RequestMethod.POST) @ResponseBody public Response login(@RequestParam("username")String username, @RequestParam("password")String password){ //1. 驗證使用者名稱密碼 UserInfo userInfo = null; try { userInfo = serviceProvider.getUserService().getUserByName(username); } catch (TException e) { e.printStackTrace(); return Response.USERNAME_PASSWORD_INVALID; } if (userInfo == null){ return Response.USERNAME_PASSWORD_INVALID; } if(!userInfo.getPassword().equalsIgnoreCase(md5(password))){ return Response.USERNAME_PASSWORD_INVALID; } //2. 生成token String token = genToken(); //3. 快取使用者 //因為userInfo是通過thrift自動升成的,裡面很多方法太過麻煩,不利於開發檢視資料內容 //最好的方式自己建立一個物件,將自動升成userInfo轉換成自定義的UserDTO redisClient.set(token,toDTO(userInfo)); return new LoginResponse(token); } @RequestMapping(value = "/sendVerifyCode", method = RequestMethod.POST) @ResponseBody public Response sendVerifyCode(@RequestParam(value="mobile", required = false) String mobile, @RequestParam(value="email", required = false) String email) { String message = "Verify code is:"; String code = randomCode("0123456789", 6); try { boolean result = false; if(StringUtils.isNotBlank(mobile)) { result = serviceProvider.getMessasgeService().sendMobileMessage(mobile, message+code); redisClient.set(mobile, code); } else if(StringUtils.isNotBlank(email)) { result = serviceProvider.getMessasgeService().sendEmailMessage(email, message+code); redisClient.set(email, code); } else { return Response.MOBILE_OR_EMAIL_REQUIRED; } if(!result) { return Response.SEND_VERIFYCODE_FAILED; } } catch (TException e) { e.printStackTrace(); return Response.exception(e); } return Response.SUCCESS; } @RequestMapping(value="/register", method = RequestMethod.POST) @ResponseBody public Response register(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(value="mobile", required = false) String mobile, @RequestParam(value="email", required = false) String email, @RequestParam("verifyCode") String verifyCode) { if(StringUtils.isBlank(mobile) && StringUtils.isBlank(email)) { return Response.MOBILE_OR_EMAIL_REQUIRED; } if(StringUtils.isNotBlank(mobile)) { String redisCode = redisClient.get(mobile); if(!verifyCode.equals(redisCode)) { return Response.VERIFY_CODE_INVALID; } }else { String redisCode = redisClient.get(email); if(!verifyCode.equals(redisCode)) { return Response.VERIFY_CODE_INVALID; } } UserInfo userInfo = new UserInfo(); userInfo.setUsername(username); userInfo.setPassword(md5(password)); userInfo.setMobile(mobile); userInfo.setEmail(email); try { serviceProvider.getUserService().regiserUser(userInfo); } catch (TException e) { e.printStackTrace(); return Response.exception(e); } return Response.SUCCESS; } private UserDTO toDTO(UserInfo userInfo) { UserDTO userDTO = new UserDTO(); BeanUtils.copyProperties(userInfo, userDTO); return userDTO; } private String genToken() { return randomCode("0123456789abcdefghijklmnopqrstuvwxyz", 32); } private String randomCode(String s, int size) { StringBuilder result = new StringBuilder(size); Random random = new Random(); for(int i=0;i<size;i++) { int loc = random.nextInt(s.length()); result.append(s.charAt(loc)); } return result.toString(); } private String md5(String password) { try { MessageDigest md5 = MessageDigest.getInstance("MD5"); byte[] md5Bytes = md5.digest(password.getBytes("utf-8")); return HexUtils.toHexString(md5Bytes); } catch (Exception e) { e.printStackTrace(); } return null; } }
測試準備工作
- 資料庫內新增一條記錄(如果不新增記錄會報getUsername是null,通過線上網站生成MD5的密碼加密)
- 啟動user-thrift-service 和 message-thrift-service 2個服務
- 啟動user-edge-service
- 工具呼叫介面訪問user-edge-service裡面的介面,檢視是否正常返回呼叫服務是否成功
- 登入操作檢視是否redis內有儲存內容
流程梳理
- 建立2個thrift api專案 user 和 message 通過thrift檔案生成對應語言的方法。
- 建立2個服務端專案,各自引入user和message的api專案。
- 多種語言比較特殊,例如message裡面需要兩邊都通過python端需要通過thirft生成對應的python程式碼方便python製作server端。java端呼叫需要通過
thirft升成對應的java程式碼方便其他專案的引用。 - 如果都是單語言的話,都是java的話,只需要生成一個thrift的java程式碼,和對應的server端服務端程式碼就可以了。
- 對於使用端只需要引用2個api就可以實現RPC的呼叫。但是需要記住的是他們之前的協議和傳輸內容必須一致才可以完成通訊。
user-edge-service-client 新建–單點登入的服務
package com.idig8.user.client; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.idig8.thrift.user.dto.UserDTO; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import org.codehaus.jackson.map.ObjectMapper; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.TimeUnit; /** * Created by Michael on 2017/10/31. */ public abstract class LoginFilter implements Filter { private static Cache<String, UserDTO> cache = CacheBuilder.newBuilder().maximumSize(10000) .expireAfterWrite(3, TimeUnit.MINUTES).build(); public void init(FilterConfig filterConfig) throws ServletException { } public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; String token = request.getParameter("token"); if(StringUtils.isBlank(token)) { Cookie[] cookies = request.getCookies(); if(cookies!=null) { for(Cookie c : cookies) { if(c.getName().equals("token")) { token = c.getValue(); } } } } UserDTO userDTO = null; if(StringUtils.isNotBlank(token)) { userDTO = cache.getIfPresent(token); if(userDTO==null) { userDTO = requestUserInfo(token); if(userDTO!=null) { cache.put(token, userDTO); } } } if(userDTO==null) { response.sendRedirect("http://www.mooc.com/user/login"); return; } login(request, response, userDTO); filterChain.doFilter(request, response); } protected abstract String userEdgeServiceAddr(); protected abstract void login(HttpServletRequest request, HttpServletResponse response, UserDTO userDTO); private UserDTO requestUserInfo(String token) { String url = "http://"+userEdgeServiceAddr()+"/user/authentication"; HttpClient client = new DefaultHttpClient(); HttpPost post = new HttpPost(url); post.addHeader("token", token); InputStream inputStream = null; try { HttpResponse response = client.execute(post); if(response.getStatusLine().getStatusCode()!= HttpStatus.SC_OK) { throw new RuntimeException("request user info failed! StatusLine:"+response.getStatusLine()); } inputStream = response.getEntity().getContent(); byte[] temp = new byte[1024]; StringBuilder sb = new StringBuilder(); int len = 0; while((len = inputStream.read(temp))>0) { sb.append(new String(temp,0,len)); } UserDTO userDTO = new ObjectMapper().readValue(sb.toString(), UserDTO.class); return userDTO; } catch (IOException e) { e.printStackTrace(); } finally { if(inputStream!=null) { try{ inputStream.close(); }catch(Exception e) { e.printStackTrace(); } } } return null; } public void destroy() { } }
需要引入單點登入的模組進行實現的功能
PS:其實通過梳理髮現這個還是有套路可尋的如何多語言進行通訊,先生成對應的語言的程式碼,然後通過rpc的服務端和客戶端,他們之前進行協議話的通訊,服務端完成自身的業務邏輯,客戶端就獲取返回的結果。
ofollow,noindex" target="_blank" href="http://zhanzhang.baidu.com/sitesubmit/index?sitename=https://idig8.com/2018/10/14/gaojipiandockerzhikaifayonghufuwuedgeservice13/">百度未收錄
>>原創文章,歡迎轉載。轉載請註明:轉載自IT人故事會,謝謝!
>>原文連結地址: