【快速搭建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转发到其他服务

订单模块省略,本文完成。

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Spring Cloud是一种基于Spring框架的开源分布式架构解决方案,它提供了一系列工具和组件,用于简化构建分布式系统的过程。 搭建Spring Cloud分布式架构主要包括以下几个步骤: 1. 环境准备:首先要准备好需要的开发环境,包括Java开发环境、Eclipse或者IntelliJ IDEA等开发工具,以及Maven项目管理工具。 2. 创建项目:通过Maven创建一个Spring Boot项目作为基础项目。可以使用Spring Initializr或者手动创建一个基本的Spring Boot项目。 3. 添加依赖:在创建的Spring Boot项目中,添加Spring Cloud的相关依赖,如spring-cloud-starter-netflix-eureka-server、spring-cloud-starter-config等,这些依赖将提供各种分布式系统所需的功能。 4. 配置服务注册中心:通过在配置文件中配置服务注册中心,可以使用Eureka或Consul等作为服务注册中心。服务注册中心用于服务的发现与注册,确保每个服务的可用性。 5. 编写业务代码:在项目中编写对应的微服务业务代码,如提供用户服务、订单服务等。每个微服务都是独立的应用程序,可以通过服务间的调用来实现不同微服务之间的协作。 6. 配置服务间的通信:通过使用Feign或Ribbon等组件,可以方便地实现服务间的通信。Feign提供了声明式的HTTP客户端,而Ribbon可以实现客户端负载均衡等功能。 7. 配置服务网关:使用Zuul等组件配置服务网关,可以实现对外部的请求进行路由和负载均衡。服务网关可以提供统一的API入口,并且可以进行安全认证等操作。 8. 配置分布式配置中心:通过使用Spring Cloud Config,可以将所有的配置文件放在统一的配置中心,实现动态的配置管理。 9. 配置服务容错保护:使用Hystrix等组件,可以实现对服务的容错保护。Hystrix可以控制对依赖服务的访问,防止级联故障并提供故障恢复机制。 10. 部署和运行:最后,将编写好的项目打包成JAR包,并进行部署和运行,可以使用Docker等技术进行容器化部署,提高项目的可扩展性和可维护性。 通过以上步骤,就可以搭建起一个基于Spring Cloud分布式架构。Spring Cloud提供了丰富的组件和功能,可以帮助开发人员快速构建和部署分布式系统,并提高系统的可用性和可扩展性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值