快速搭建SpringCloud分布式项目--商城
前言
本文采用spring-cloud-alibaba快速搭建一个分布式项目,分为以下服务模块:
1、用户模块 (cloud-user) : 用户信息
2:账户模块 (cloud-account) : 用户账户
3:商品模块 (cloud-goods) : 包含商品,订单
4:工具类模块 (cloud-commons) :做为依赖导入各个模块
5:gateway (gateway) :服务走网关
流程为用户注册,登录,创建账户,充值,下单,本文只是快速构建分布式项目,后续会基于以上模块添加分布式事务,分布式锁,日志收集等。
分布式事务:暂无地址
分布式锁:暂无地址
日志收集:点击跳转
一、快速构建
1、创建一个maven项目做为父项目
创建成功删除src目录,将此项目做为父级项目
2、引入依赖
先做说明,后贴出全部依赖,先思考起一个spring cloud项目都需要什么东西, 1:服务之间的通信(远程调用)。2:请求的统一性(所有请求到gateway,gateway负责转发)。3:权限验证。
本文快速构建使用 spring cloud alibaba ,所以也是要引入对应的依赖,使用nacos做为注册中心,gateway为网关,loadbalancer为负载均衡器,openfeign远程调用,springsecurity权限,jwt token。
以下是父级项目全部依赖(pom.xml):
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.14</version>
<relativePath/>
</parent>
<!-- 跟项目有关的信息-->
<groupId>com.example</groupId>
<artifactId>cloud-demo-k</artifactId>
<version>1.0-SNAPSHOT</version>
<name>cloud-demo</name>
<description>Base Cloud</description>
<!-- 打包方式 pom jar war-->
<packaging>pom</packaging>
<properties>
<!-- 打包编码-->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Java的版本号,如果跟使用的不一样会报错-->
<java.version>1.8</java.version>
<spring-cloud-alibaba-version>2021.0.4.0</spring-cloud-alibaba-version>
<spring-cloud-version>2021.0.5</spring-cloud-version>
<druid.version>1.2.15</druid.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<jwt.version>0.11.5</jwt.version>
<fastjson-version>1.2.83_noneautotype</fastjson-version>
<mysql-connector-java.version>8.0.33</mysql-connector-java.version>
<nacos.client.version>2.3.0</nacos.client.version>
<jedis.version>3.7.1</jedis.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 开发web应用程序依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 监控程序运行状态、线程状态-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- nacos配置中心来做配置管理 begin-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>${nacos.client.version}</version>
</dependency>
<!-- 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 远程调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 加载bootstrap.yml文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 链路追踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson-version}</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<!--打包跳过test-->
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
3、创建用户模块
创建时还是选择maven,父级选择我们创建的父级项目,其他的根据自己的需求填写
创建成功后,父级项目出现子级module视为成功关联
然后在目录下创建启动类,UserApplication,加入注册中心、远程调用的注册
新增配置文件,配置端口,数据库,nacos配置(docker安装nacos不再详细说明,如需要请查看我的另外一篇 docker快速安装nacos)
application.properties
server.port=8080
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://192.168.168.129:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#初始化时建立物理连接个数
spring.datasource.druid.initial-size=5
#最小连接池数量
spring.datasource.druid.min-idle=5
#最大连接池数量
spring.datasource.druid.max-active=15
#获取连接时最大等待时间
spring.datasource.druid.max-wait=5000
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
bootstrap.properties 将IP赋值为自己的nacos地址
spring.application.name=user
spring.cloud.nacos.config.group=DEFAULT_GRP
spring.cloud.nacos.config.file-extension=properties
#nacos注册中心地址,将IP赋值为自己的nacos地址
spring.cloud.nacos.config.server-addr=ip:8848
#要加这句,不然启动的时候会将nacos地址赋值为127.0.0.1,加了这句就会赋值自己的地址
spring.cloud.nacos.server-addr=ip:8848
启动测试一下,启动成功。
nacos也成功注册服务,至此第一个服务算是完成
4、创建商品模块、创建用户账户模块
与用户模块一样创建(我是直接复制然后修改的),同样创建启动类,两个properties文件,修改一下端口即可
5、创建commons模块(工具类模块,作为依赖导入各个模块)
commons模块也通用按照用户模块那样创建,唯一不同的是commons模块没有启动类,也没有properties文件。
以下先贴出所用到的工具类截图,请自行导包,如果不需要,也可以忽略,创建自己的commons类。
代码如下:(自行创建文件)
/**
* jwt token的创建和解析
*/
public class JwtBuild {
private final String keyStr = "4SdJY8vGBDpPH3ZXqWpJezHSvNFktdKy";
private Key key;
public JwtBuild(boolean isCreateKey, String str) {
if (isCreateKey) {
this.createKey();
} else {
if (StringUtils.isBlank(str)) {
str = keyStr;
}
//使用这个会报错加密长度不够,影响安全的错误
//key = Keys.hmacShaKeyFor(Decoders.BASE64.decode("4SdJY8vGBDpPH3ZXqWpJezHSvNFktdKy=="));
//使用这个不会
key = Keys.hmacShaKeyFor(Base64.getEncoder().encode(str.getBytes()));
}
}
public void createKey() {
key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
}
public String getStrKey() {
return Encoders.BASE64.encode(key.getEncoded());
}
public void loadKey(String strKey) {
key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(strKey));
}
/**
* @param map 数据
* @param expireTime 过期时间
* @param unit 过期时间类型
* @return
*/
public String getToken(Map<String, Object> map, long expireTime, ChronoUnit unit) {
return Jwts.builder().
setClaims(map).
setExpiration(Date.from(Instant.now().plus(expireTime, unit))).
signWith(key).
compact();
}
/**
* @param data 数据
* @param expireTime 过期时间
* @param unit 过期时间类型
* @return
*/
public String getToken(String data, long expireTime, ChronoUnit unit) {
return Jwts.builder().
setSubject(data).
setExpiration(Date.from(Instant.now().plus(expireTime, unit))).
signWith(key).
compact();
}
public String getTokenData(String token) {
return this.getTokenClaims(token).getSubject();
}
public Claims getTokenClaims(String token) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
} catch (Exception e) {
throw new RuntimeException("登录失效");
}
}
}
/**
* 放行的路由
*/
public class ChainReleaseConfig {
public static List<String> RELEASE_URL = new ArrayList<>();
static {
RELEASE_URL.add("/user/login");
RELEASE_URL.add("/account/business/register");
}
}
/**
* 验证token失败时触发
*/
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
JSONObject resp = new JSONObject();
resp.put("code", ResultCode.LOGOUT);
resp.put("msg", "登录失效");
resp.put("data", null);
PrintWriterResult.result(response, resp);
}
}
/**
* 验证token的拦截器
*/
@Slf4j
public class JwtFilter extends OncePerRequestFilter implements Ordered {
JwtBuild jwtBuild = new JwtBuild(false, null);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
log.info(requestURI);
if (!ChainReleaseConfig.RELEASE_URL.contains(requestURI) && !requestURI.contains("private")) {
try {
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
throw new BusinessException("头部未携带token");
}
JwtBuild jwtBuild = new JwtBuild(false, null);
String str = jwtBuild.getTokenData(token);
Users user = JSONObject.parseObject(str, Users.class);
UsernamePasswordAuthenticationToken userToken =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(userToken);
request.setAttribute("login_data_bean", user);
} catch (Exception e) {
throw new BusinessException("认证失败");
}
}
filterChain.doFilter(request, response);
}
@Override
public int getOrder() {
return 1;
}
}
public class PrintWriterResult {
public static void result(HttpServletResponse response, JSONObject resp) {
try {
response.setStatus(200);
response.setContentType("application/json;charset=UTF-8");
PrintWriter pw = response.getWriter();
pw.write(resp.toString());
pw.flush();
pw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* spring security配置
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig{
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
/**
* 明文密码 NoOpPasswordEncoder.getInstance()
* new BCryptPasswordEncoder()
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 解决跨域问题(重要) 只有在前端请求接口时才发现需要这个
.cors().and().formLogin().and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(new JwtAuthenticationEntryPoint())
// .accessDeniedHandler(jwtAccessDeniedHandler)
.and()
//设置无状态的连接,即不创建session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
//路径设置uri,即Mapping,不包含项目路径 anonymous允许匿名用户访问 permitAll () 无条件允许访问
.antMatchers("/private/**").permitAll()
.antMatchers("/user/login").permitAll()
.antMatchers("/account/business/register").permitAll()
//配置允许匿名访问的路径
.anyRequest().authenticated();
//配置自己的jwt验证过滤器
http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
http.headers().cacheControl();
//.antMatchers("/admin/**").hasRole("ADMIN");配置只有ADMIN 权限的用户才能访问该路由
return http.build();
}
}
/**
* spring security 在反序列化的时候权限列表会丢失,原因是在spring序列化的时候遇到 GrantedAuthority 时会跳过字段不进行序列化
* 解决方案:1、在验证过滤器或实现UserDetailsService的service里手动加载权限列表
* 2、重新实现一个 SimpleGrantedAuthority 方法 实现GrantedAuthority接口,实体类中权限列表里指定类型为自己所创建的 SimpleGrantedAuthorityWrapper
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SimpleGrantedAuthorityWrapper implements GrantedAuthority {
private String role;
@Override
public String getAuthority() {
return role;
}
}
/**
* spring security用到的实体类
*/
@Data
public class Users implements UserDetails {
private static final long serialVersionUid = 1L;
private String id;
private String username;
private String password;
//权限列表
private List<SimpleGrantedAuthorityWrapper> authorities;
private String roleKey;
private String menuKey;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<SimpleGrantedAuthorityWrapper> authorities) {
this.authorities = authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public String toString() {
return JSONObject.toJSONString(this);
}
}
@RestController
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class BusinessAdvice {
@ExceptionHandler(value = Exception.class)
public ResponseResult handle(Exception ex) {
// ex.printStackTrace();
PrintStackTrace.error(ex);
ResponseResult response = new ResponseResult();
response.setCode(ResultCode.ERROR);
response.setMsg("未知错误");
return response;
}
/**
* SQLIntegrityConstraintViolationException是DataAccessException子类,
* 如果使用ExceptionHandler(SQLIntegrityConstraintViolationException.class),会捕获不到异常
*
* @param ex
* @return
*/
@ExceptionHandler(value = DataAccessException.class)
public ResponseResult handle(SQLIntegrityConstraintViolationException ex) {
ResponseResult response = new ResponseResult();
response.setCode(ResultCode.ERROR);
PrintStackTrace.error(ex);
if (ex.getMessage().contains("Duplicate entry")) {
String s = ex.getMessage().split(" ")[2];
s = s + " 已存在";
response.setMsg(s);
return response;
}
response.setMsg("未知错误");
return response;
}
@ExceptionHandler(value = BusinessException.class)
public ResponseResult handle(BusinessException ex) {
ResponseResult response = new ResponseResult();
response.setCode(ResultCode.FAIL);
response.setMsg(ex.getMessage());
return response;
}
@ExceptionHandler(value = AccessDeniedException.class)
public ResponseResult handle(AccessDeniedException ex) {
ResponseResult response = new ResponseResult();
response.setCode(ResultCode.NO_AUTH);
response.setMsg("无操作权限");
return response;
}
}
public class BusinessException extends RuntimeException{
public BusinessException(String message){
super(message);
}
}
@Slf4j
public class PrintStackTrace {
public static void error(Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw, true));
log.error(sw.toString());
}
}
@Configuration
public class FilterRegistration {
@Bean
public FilterRegistrationBean<OncePerRequestFilter> servletFilter() {
FilterRegistrationBean<OncePerRequestFilter> filter = new FilterRegistrationBean<>();
filter.setFilter(new ServletFilter());
filter.addUrlPatterns("/*");
filter.setName("ServletFilter");
filter.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filter;
}
}
/**
* 拦截请求,对请求做打印
*/
@Slf4j
public class RequestFilter extends HttpServletRequestWrapper {
@Getter
private HttpServletRequest request;
@Getter
private HttpServletResponse response;
private Map<String, String> requestHeaders;
public Users getLoginData() {
return (Users) request.getAttribute("login_data_bean");
}
public RequestFilter(HttpServletRequest request, HttpServletResponse response) {
super(request);
this.request = request;
this.response = response;
}
public JSONObject getRequestBodyJson() {
Map<String, String> headers = this.getHeaders();
String ip;
try {
ip = this.request.getHeader("x-forwarded-for").split(",")[0];
} catch (Exception e) {
ip = "请求来源未知";
}
JSONObject bodyJson;
String body = null;
try {
body = this.getBodyData();
bodyJson = JSONObject.parseObject(body);
if (null == bodyJson) {
bodyJson = new JSONObject();
}
} catch (Exception e) {
bodyJson = new JSONObject();
this.getData(bodyJson, body);
}
Map<String, String[]> params = this.request.getParameterMap();
for (String key : params.keySet()) {
String val = params.get(key)[0];
bodyJson.put(key, val);
}
log.info("请求来源:{},请求方法:{},请求参数:{}", ip, request.getMethod(), bodyJson);
return bodyJson;
}
public Map<String, String> getHeaders() {
if (requestHeaders != null) {
return requestHeaders;
}
Map<String, String> heads = new HashMap<>(30);
Enumeration<String> headerNames = this.request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = this.request.getHeader(name);
heads.put(name, value);
}
requestHeaders = heads;
return heads;
}
// 解析请求体中的参数
// 这里简单示例,如果请求体是 application/x-www-form-urlencoded 格式,则可以使用类似于 URLDecoder 的方式解析参数
// 如果请求体是 JSON 格式,则可以使用 JSON 库进行解析
// 根据实际情况自行解析逻辑,并将参数放入 Map 中
// 下面使用示例:x-www-form-urlencoded 格式
public JSONObject getData(JSONObject bodyJson, String body) {
if (StringUtils.isNotBlank(body)) {
try {
if (StringUtils.isNotBlank(body)) {
String[] str = body.split("&");
for (String s : str) {
String[] val = s.split("=");
try {
bodyJson.put(val[0], val[1]);
} catch (ArrayIndexOutOfBoundsException ae) {
bodyJson.put(val[0], "");
}
}
}
} catch (Exception e) {
}
}
return bodyJson;
}
public String getBodyData() {
try {
this.request.setCharacterEncoding("utf-8");
//读取body请求体数据,转为string
return this.readLine(this.request.getReader());
} catch (IOException e) {
return null;
}
}
public String readLine(BufferedReader reader) throws IOException {
// 逐行读取请求体中的数据
String line;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
public class ServletFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
RequestFilter requestFilter = new RequestFilter(request, response);
filterChain.doFilter(requestFilter, response);
}
}
/**
* 统一返回格式
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult {
private int code;
private String msg;
private JSONObject data;
ResponseResult(int code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = (JSONObject) data;
}
public JSONObject getData() {
if (null == this.data) {
data = new JSONObject();
}
return data;
}
public void setData(JSONObject data) {
this.data = data;
}
}
public class ResultCode {
/**
* 统一返回业务编码
* 0:成功
* 1:业务未执行成功
* -1:出现异常
* 9:登录失效
*/
public final static int SUCCESS = 0;
public final static int FAIL = 1;
public final static int ERROR = -1;
public final static int NO_AUTH = 11;
public final static int LOGOUT = 22;
}
/**
* 解析远程调用返回参数
*/
public class ApiParse {
public static void parse(ResponseResult result) {
if (ResultCode.SUCCESS != result.getCode()) {
throw new BusinessException(result.getMsg());
}
}
}
/**
* 检验参数是否为空
*/
public class ParamsInspect {
public static void IsNull(String param, String remark) {
if (StringUtils.isBlank(param)) {
throw new BusinessException(remark + "不能为空");
}
}
public static void IsNumber(String number, String remark) {
try {
if (-1 == Decimal.bigOrSmall(number, "0")) {
throw new BusinessException(remark + "有误");
}
} catch (NumberFormatException n) {
throw new BusinessException(remark + "格式有误");
}
}
}
public class Decimal {
/**
* 比较大小
* a < b 返回-1
* a = b 返回 0
* a > b 返回 1
*
* @param a
* @param b
* @return
*/
public static int bigOrSmall(String a, String b) {
BigDecimal a1 = new BigDecimal(a);
BigDecimal b1 = new BigDecimal(b);
return a1.compareTo(b1);
}
}
public class TimeTools {
private static final DateTimeFormatter FORMATTER_MSEC = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
private static final DateTimeFormatter FORMATTER_SEC = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter FORMATTER_FIFTEEN_MIN = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
private static final DateTimeFormatter FORMATTER_DAY = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final DateTimeFormatter FORMATTER_INTERVAL_DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter FORMATTER_MONTHS = DateTimeFormatter.ofPattern("yyyyMM");
private static final DateTimeFormatter FORMATTER_HOURS = DateTimeFormatter.ofPattern("HH");
private static final DateTimeFormatter FORMATTER_MIN = DateTimeFormatter.ofPattern("HHmm");
private static final DateTimeFormatter FORMATTER_DAYS = DateTimeFormatter.ofPattern("dd");
private static final ZoneId CN_ZONE = ZoneId.of("Asia/Shanghai");
private static final ZoneId UTC_ZONE = ZoneId.of("UTC");
public static String getTime() {
return LocalDateTime.now().format(FORMATTER_SEC);
}
public static String getMillisecTime() {
return LocalDateTime.now().format(FORMATTER_MSEC);
}
public static String getDate() {
return LocalDateTime.now().format(FORMATTER_DAY);
}
}
//获取订单id
public class Serial {
private static final String USER_ID = "12345678998123";
private static final AtomicInteger counter = new AtomicInteger(0);
/**
* 方式1 : 当前日期+用户ID+随机数
* @param userId
* @return
*/
public static String getOrderId(String userId) {
StringBuilder sb = new StringBuilder();
// 添加当前日期
sb.append(TimeTools.getTime());
// 添加用户ID
sb.append(userId);
// 添加自增计数器
int count = counter.incrementAndGet();
sb.append(String.format("%04d", count % 10000));
return sb.toString();
}
}
以上就是暂时所用到的工具类,后续如果有新的,再继续添加。
新建好以上文件后,对commons模块进行打包
先clean,再install到本地,成功之后在每个模块都引入commons模块即可,这样commons就完成了。
注意:引入commons模块后,用到commons模块的服务,启动类上需要增加一个扫描
6、创建gateway模块
gateway模块的pom文件有所不同,1:需要单独引入gateway的依赖,2:需要排除springsecurity,spring-boot-start-web的依赖,如此即可。
1:创建gateway启动类。
2:在服务之间的相互调用的时候,我们采取了一个统一的前缀private,因为内部调用的接口,不考虑做加密验证,所以在gateway网关中排除一下private。首先创建一个拦截器,然后去判断是否包含private,如果包含,不予放行。
@Configuration
@Slf4j
public class GatewayFilter implements GlobalFilter, Ordered {
private static final byte[] bytes;
static {
JSONObject json = new JSONObject();
json.put("code", 1);
json.put("msg", "UNAUTHORIZED");
bytes = json.toString().getBytes();
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String rawPath = request.getURI().getRawPath();
log.info(rawPath);
if (rawPath.contains("private")){
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.writeWith(Mono.just(response.bufferFactory().wrap(bytes)));
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 2;
}
}
3:创建application.yml文件
# 网关端口
server:
port: 10010
# 服务名称
spring:
application:
name: gateway
cloud:
# 全局跨域处理
gateway:
enabled: true
globalcors:
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题(浏览器跨域预检请求)
cors-configurations:
'[/**]':
allowed-origin-patterns: "*" # 允许哪些网站的跨域请求
# - "http://localhost:8090"
# - "http://localhost:8091"
allowed-headers: "*" # 允许在请求中携带的头信息
allowed-methods: "*" # 允许的跨域ajax的请求方式
# - "GET"
# - "POST"
# - "DELETE"
# - "PUT"
# - "OPTIONS"
allow-credentials: true # 是否允许携带cookie
routes: # 网关路由配置
# - id: user # 路由id 不重复就行
# uri: http://localhost:8001 # 路由的目标地址 http就是具体地址 lb就是负载均衡,后面跟服务名称
# predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
# - Path=/api/** # 这里是按照路径匹配
# filters: #过滤器
# - AddRequestHeader=This is a header! # 添加请求头
- id: user
uri: lb://user
predicates:
- Path=/user/**
filters:
- StripPrefix=1
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix=1
- id: account
uri: lb://account
predicates:
- Path=/account/**
filters:
- StripPrefix=1
4:创建bootstrap.properties文件
spring.application.name=gateway
#nacos注册中心地址
spring.cloud.nacos.config.group=DEFAULT_GRP
spring.cloud.nacos.config.file-extension=properties
spring.cloud.nacos.config.server-addr=192.168.168.129:8848
#要加这句,不然启动的时候会将nacos地址赋值为127.0.0.1,加了这句就会赋值nacos
spring.cloud.nacos.server-addr=192.168.168.129:8848
ok,到这里我们的基础架构是搭建好了,先clean再install一下,如果install成功了,就可以启动看下是否启动成功,是否成功注册到nacos,如果出现以下问题,是因为版本的问题。
可以在每个模块的pom文件中添加以下代码,重新指定版本号
<build>
<plugins>
<!--修改maven-resources-plugin版本-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<!--启动时不进行检查-->
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
全部服务启动一下,看下是否全部注册到nacos
至此一个分布式的架构算是搭建完成了。
-----------------------------------分割线-----------------------------------------
如果仅仅只需要快速构建springcloud项目,以上就可以了,下面的是拓展的商城类的测试demo,可以忽略掉
二、测试
首先提供几张表结构
用户账户模块:
create table user_account( --用户账户表
user_id varchar(14) not null primary key, --用户ID
balance decimal(20,2) default 0, --用户余额
create_time varchar(14) not null, --创建时间
index indUserId(user_id)
);
create table account_detail( --账户明细表
user_id varchar(14) not null primary key, --用户ID
bal decimal(20,2), --操作金额
after_bal decimal(20,2), --操作后金额
pay_type int, --操作类别 1增加 2扣减
type int, --操作类型 1下单 2订单退回
create_time varchar(14) not null, --操作时间
remarks varchar(200) not null, --操作说明
explains varchar(200), --操作备注
index userId_type_ind(user_id,type)
)
用户模块:
create table users( --用户表
id varchar(13) not null primary key, --用户ID
username varchar(40) not null unique, --用户名
password varchar(100) not null, --密码
status int default 1, --账号状态 1可用 2禁用
create_time varchar(14) not null, --创建时间 yyyyMMddHHmmss
login_time varchar(14), --上次登录时间
index id_index(id),
index username_index(username),
index stats_index(status)
)
商品订单模块:
create table article( --商品表
id varchar(13) not null primary key, --商品ID
name varchar(50) not null, --商品名称
summary varchar(200) not null, --商品简介
price decimal(20,2) default 0, --商品价格、单价
number int default 1, --商品数量、库存
status int default 1, --商品状态 1展示 2隐藏
create_time varchar(14) not null, --创建时间
index name_ind(name),
index name_summary_ind(name,summary)
);
create table orders( --订单表
order_id varchar(32) not null primary key, --订单号
user_id varchar(13) not null, --用户ID
art_id varchar(32) not null, --商品ID
unit_price decimal(20,2) default 0, --商品单价
number int default 1, --购买数量
status int default 1, --订单状态 1已下单 2配送中 3订单完成 4订单失败
remarks varchar(200) default null, --备注
create_time varchar(14) not null, --下单时间
index userId_ind(user_id)
)
1、用户模块
登录注册功能:
先贴目录结构,后贴代码
@Mapper
public interface UserMapper {
@Select("select * from users where username = #{username} and status = 1")
Users selectUserByUserName(String username);
@Insert("insert into users(id,username,password,create_time) values(#{id},#{username},#{password},#{create_time})")
int register(@Param("id") String id, @Param("username") String username, @Param("password") String password,
@Param("create_time") String createTime);
}
public interface UsersService {
/**
* 注册
*
* @param username 用户账号
* @param password 用户密码,md5加密
*/
void register(String username, String password);
}
public interface LoginService {
String[] login(String username, String password);
}
//springsecurity登录校验账号密码
@Service
public class UserService implements UserDetailsService {
private static final String ROLE_PRE_FIX = "ROLE_";
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = userMapper.selectUserByUserName(username);
if (null == users) {
throw new BusinessException("账号/密码密码有误");
}
List<SimpleGrantedAuthorityWrapper> list = new ArrayList<>();
list.add(new SimpleGrantedAuthorityWrapper(ROLE_PRE_FIX + "admin"));
list.add(new SimpleGrantedAuthorityWrapper("qry"));
list.add(new SimpleGrantedAuthorityWrapper("upd"));
list.add(new SimpleGrantedAuthorityWrapper("add"));
list.add(new SimpleGrantedAuthorityWrapper("del"));
users.setAuthorities(list);
return users;
}
}
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
AuthenticationManager authenticationManager;
@Override
public String[] login(String username, String password) {
UsernamePasswordAuthenticationToken userToken =
new UsernamePasswordAuthenticationToken(username, password);
//此处经过一系类内部类后会调用至UserDetailsService的load方法
//load方法的入参name就是token里的name
Authentication authentication;
try {
authentication = authenticationManager.authenticate(userToken);
} catch (AuthenticationException a) {
a.printStackTrace();
throw new BusinessException("登录失败,账号/密码有误");
}
Users users = (Users) authentication.getPrincipal();
users.setPassword(null);
JwtBuild jwtBuild = new JwtBuild(false,null);
String token = jwtBuild.getToken(users.toString(), 24, ChronoUnit.HOURS);
return new String[]{token, users.getId()};
}
}
@Service
@Slf4j
public class UsersServiceImpl implements UsersService {
@Autowired
UserMapper userMapper;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserAccountService userAccountService;
@Override
@Transactional(rollbackFor = Exception.class)
public void register(String username, String password) {
ParamsInspect.IsNull(username, "账号");
ParamsInspect.IsNull(password, "密码");
String id = String.valueOf(System.currentTimeMillis());
String registerTime = TimeTools.getTime();
// password = BCrypt.hashpw(password, BCrypt.gensalt());
password = passwordEncoder.encode(password);
if (1 != userMapper.register(id, username, password, registerTime)) {
throw new BusinessException("新增失败");
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("userId", id);
//远程调用,注册账户
ApiParse.parse(userAccountService.addAccount(jsonObject.toJSONString()));
}
}
@RestController
@RequestMapping("/user")
public class LoginController {
@Autowired
LoginService loginService;
@PostMapping("/login")
public ResponseResult login(@RequestBody JSONObject req) {
String[] login = loginService.login(req.getString("username"), req.getString("password"));
JSONObject retData = new JSONObject();
retData.put("token", login[0]);
retData.put("uid", login[1]);
return new ResponseResult(ResultCode.SUCCESS, "登录成功", retData);
}
}
@RestController
@RequestMapping("/account/business")
@CrossOrigin(value = "*")
public class UserController {
@Autowired
UsersService usersService;
@PostMapping("/register")
public ResponseResult register(RequestFilter requestFilter) {
JSONObject bodyJson = requestFilter.getRequestBodyJson();
usersService.register(bodyJson.getString("username"), bodyJson.getString("password"));
return new ResponseResult(ResultCode.SUCCESS, null, null);
}
@PostMapping("/demo")
public ResponseResult demo (){
return new ResponseResult(ResultCode.SUCCESS,null,null);
}
}
2、账户模块
@Mapper
public interface AccountMapper {
@Insert("insert into user_account(user_id,create_time) value (#{user_id},#{create_time})")
int addAccount(@Param("user_id") String userId, @Param("create_time") String createTime);
}
public interface AccountService {
/**
* 注册时添加账户
*
* @param userId
* @return
*/
void addAccount(String userId);
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
AccountMapper accountMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void addAccount(String userId) {
ParamsInspect.IsNull(userId, "用户ID");
if (1 != accountMapper.addAccount(userId, TimeTools.getTime())) {
throw new BusinessException("添加失败");
}
}
}
@RestController
@RequestMapping("/private")
public class ApiController {
@Autowired
AccountService accountService;
//注册时添加账户
@PostMapping("/add/account")
public ResponseResult addAccount(@RequestBody String body) {
JSONObject jsonObject = JSONObject.parseObject(body);
accountService.addAccount(jsonObject.getString("userId"));
return new ResponseResult(ResultCode.SUCCESS, null, null);
}
}
访问 http://127.0.0.1:10010/user/account/business/register 调用注册接口,注意,密码为md5加密。
测试登录接口
注意:接口请求的端口是10010的gateway端口,再由gateway转发到其他服务
订单模块省略,本文完成。