商城(1):项目搭建、通用异常处理器与枚举详解

因为马上要上要去互联网公司上班了,所以先学个互联网项目练手吧,长时间不练确实手生。这里做的是某马培训机构的一个商城项目。其实这个项目跟实习的时候做的一个体育软件很像,只是商城使用了分布式,今后2C十有八九都会使用分布式吧。

在校招的时候我就是拿这个项目忽悠的,被问成了傻子,但是正是被不断的提问,也慢慢学到了解决方案,比如优雅的解决缓存穿透,秒杀如何实现等,如果一味的从网上抄,那面试可能要凉一半,今后的学习过程中我会补充被面到的问题以及我的回答,面试官的回答,去哪儿网和美团的二面面试官都给了区别与网上的很好的答案,最后也会补充一些零碎的知识点。

1 项目介绍

该商城和京东或者天猫商城是类似的,只是目前没有做到那么细,只实现了主线(购物)功能。乐优商城分是一个B2C的商城,分为前台和后台,后台是展现给商家的,商家可以添删改查商品,进行权限管理等。而前台就跟我们日常在京东购物时所展现给我们的系统。无论是前台还是后台系统,都共享相同的微服务集群,包括:

  • 商品微服务:商品及商品分类、品牌、库存等的服务
  • 搜索微服务:实现搜索功能
  • 订单微服务:实现订单相关
  • 购物车微服务:实现购物车相关功能
  • 用户中心:用户的登录注册等功能
  • Eureka注册中心
  • Zuul网关服务
  • Spring Cloud Config配置中心

系统架构

整个系统的架构如下图所示

在这里插入图片描述

技术选型

前端技术:

  • 基础的HTML、CSS、JavaScript(基于ES6标准)
  • JQuery
  • Vue.js 2.0以及基于Vue的框架:Vuetify
  • 前端构建工具:WebPack
  • 前端安装包工具:NPM
  • Vue脚手架:Vue-cli
  • Vue路由:vue-router
  • ajax框架:axios
  • 基于Vue的富文本框架:quill-editor

前端的技术不会过多去说,了解下就好,学那点皮毛就行了。还是把注意力集中到后端。

后端技术:

  • 基础的SpringMVC、Spring 5.0和MyBatis3
  • Spring Boot 2.0.1版本
  • Spring Cloud 版本 Finchley.RS2
  • Redis-4.0
  • RabbitMQ-3.4
  • Elasticsearch-5.6.8
  • nginx-1.10.2:
  • FastDFS - 5.0.8
  • MyCat
  • Thymeleaf

开发工具Idea
以上并不是全部,还会涉及到一些实际开发中的技巧。

2 项目环境搭建

2.1 父工程

创建Project,名称为leyou,打包方式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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.parent</groupId>
    <artifactId>leyou</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.SR2</spring-cloud.version>
        <mapper.starter.version>2.0.2</mapper.starter.version>
        <mysql.version>5.1.32</mysql.version>
        <pageHelper.starter.version>1.2.3</pageHelper.starter.version>
        <leyou.latest.version>1.0.0-SNAPSHOT</leyou.latest.version>
        <fastDFS.client.version>1.26.1-RELEASE</fastDFS.client.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- springCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- 通用Mapper启动器 -->
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
                <version>${mapper.starter.version}</version>
            </dependency>
            <!-- 分页助手启动器 -->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pageHelper.starter.version}</version>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!--FastDFS客户端-->
            <dependency>
                <groupId>com.github.tobato</groupId>
                <artifactId>fastdfs-client</artifactId>
                <version>${fastDFS.client.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2.2 注册中心微服务

在leyou项目下,创建模块(module),名称为ly-registry

依赖如下:

<?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>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.common</groupId>
    <artifactId>ly-registry</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
</project>

添加配置文件application.yaml,添加内容如下:

server:
  port: 10086
spring:
  application:
    name: ly-registry
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

创建启动类LyRegistryApplication如下

@SpringBootApplication
@EnableEurekaServer
public class LyRegistryApplication {
    public static void main(String[] args) {
        SpringApplication.run(LyRegistryApplication.class);
    }
}

此时的结构图如下:
在这里插入图片描述

2.3 网关微服务

在leyou项目下创建ly-gateway模块,导入依赖如下

<?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>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.common</groupId>
    <artifactId>ly-gateway</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
</project>

添加配置文件application.yaml,内容如下

server:
  port: 10010
spring:
  application:
    name: api-gateway
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
zuul:
  prefix: /api # 添加路由前缀

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 5000 # 熔断超时时长:10000ms
ribbon:
  ConnectTimeout: 1000 # 连接超时时间(ms)
  ReadTimeout: 3500 # 通信超时时间(ms)
  MaxAutoRetriesNextServer: 0 # 同一服务不同实例的重试次数
  MaxAutoRetries: 0 # 同一实例的重试次数

创建启动类如下

@SpringCloudApplication
@EnableZuulProxy
public class LyZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(LyZuulApplication.class);
    }
}

此时项目结构图如下

在这里插入图片描述

2.4 通用模块

在leyou项目创建通用模块ly-common,注意,这个模块并不会作为一个微服务,仅仅用来存放通用类的,比如常用的utils类,接下来就会创建项目会用到的,Cookie工具类,Json工具类,ID生成器通以及Number工具类,当然这些都是授课老师写的。如果需要就copy下来留着以后用。

依赖如下

<?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>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.common</groupId>
    <artifactId>ly-common</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>
    </dependencies>
</project>

Cookie工具类:

package com.leyou.common.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

/**
 * 
 * Cookie 工具类
 *
 */
public final class CookieUtils {

	protected static final Logger logger = LoggerFactory.getLogger(CookieUtils.class);

	/**
	 * 得到Cookie的值, 不编码
	 * 
	 * @param request
	 * @param cookieName
	 * @return
	 */
	public static String getCookieValue(HttpServletRequest request, String cookieName) {
		return getCookieValue(request, cookieName, false);
	}

	/**
	 * 得到Cookie的值,
	 * 
	 * @param request
	 * @param cookieName
	 * @return
	 */
	public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
		Cookie[] cookieList = request.getCookies();
		if (cookieList == null || cookieName == null){
			return null;			
		}
		String retValue = null;
		try {
			for (int i = 0; i < cookieList.length; i++) {
				if (cookieList[i].getName().equals(cookieName)) {
					if (isDecoder) {
						retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
					} else {
						retValue = cookieList[i].getValue();
					}
					break;
				}
			}
		} catch (UnsupportedEncodingException e) {
			logger.error("Cookie Decode Error.", e);
		}
		return retValue;
	}

	/**
	 * 得到Cookie的值,
	 * 
	 * @param request
	 * @param cookieName
	 * @return
	 */
	public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
		Cookie[] cookieList = request.getCookies();
		if (cookieList == null || cookieName == null){
			return null;			
		}
		String retValue = null;
		try {
			for (int i = 0; i < cookieList.length; i++) {
				if (cookieList[i].getName().equals(cookieName)) {
					retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
					break;
				}
			}
		} catch (UnsupportedEncodingException e) {
			logger.error("Cookie Decode Error.", e);
		}
		return retValue;
	}

	/**
	 * 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue) {
		setCookie(request, response, cookieName, cookieValue, -1);
	}

	/**
	 * 设置Cookie的值 在指定时间内生效,但不编码
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage) {
		setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
	}

	/**
	 * 设置Cookie的值 不设置生效时间,但编码
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, boolean isEncode) {
		setCookie(request, response, cookieName, cookieValue, -1, isEncode);
	}

	/**
	 * 设置Cookie的值 在指定时间内生效, 编码参数
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
		doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
	}

	/**
	 * 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
	 */
	public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
		doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
	}

	/**
	 * 删除Cookie带cookie域名
	 */
	public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
		doSetCookie(request, response, cookieName, "", -1, false);
	}

	/**
	 * 设置Cookie的值,并使其在指定时间内生效
	 * 
	 * @param cookieMaxage
	 *            cookie生效的最大秒数
	 */
	private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
		try {
			if (cookieValue == null) {
				cookieValue = "";
			} else if (isEncode) {
				cookieValue = URLEncoder.encode(cookieValue, "utf-8");
			}
			Cookie cookie = new Cookie(cookieName, cookieValue);
			if (cookieMaxage > 0)
				cookie.setMaxAge(cookieMaxage);
			if (null != request)// 设置域名的cookie
				cookie.setDomain(getDomainName(request));
			cookie.setPath("/");
			response.addCookie(cookie);
		} catch (Exception e) {
			logger.error("Cookie Encode Error.", e);
		}
	}

	/**
	 * 设置Cookie的值,并使其在指定时间内生效
	 * 
	 * @param cookieMaxage
	 *            cookie生效的最大秒数
	 */
	private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
		try {
			if (cookieValue == null) {
				cookieValue = "";
			} else {
				cookieValue = URLEncoder.encode(cookieValue, encodeString);
			}
			Cookie cookie = new Cookie(cookieName, cookieValue);
			if (cookieMaxage > 0)
				cookie.setMaxAge(cookieMaxage);
			if (null != request)// 设置域名的cookie
				cookie.setDomain(getDomainName(request));
			cookie.setPath("/");
			response.addCookie(cookie);
		} catch (Exception e) {
			logger.error("Cookie Encode Error.", e);
		}
	}

	/**
	 * 得到cookie的域名
	 */
	private static final String getDomainName(HttpServletRequest request) {
		String domainName = null;

		String serverName = request.getRequestURL().toString();
		if (serverName == null || serverName.equals("")) {
			domainName = "";
		} else {
			serverName = serverName.toLowerCase();
			serverName = serverName.substring(7);
			final int end = serverName.indexOf("/");
			serverName = serverName.substring(0, end);
			final String[] domains = serverName.split("\\.");
			int len = domains.length;
			if (len > 3) {
				// www.xxx.com.cn
				domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
			} else if (len <= 3 && len > 1) {
				// xxx.com or xxx.cn
				domainName = domains[len - 2] + "." + domains[len - 1];
			} else {
				domainName = serverName;
			}
		}

		if (domainName != null && domainName.indexOf(":") > 0) {
			String[] ary = domainName.split("\\:");
			domainName = ary[0];
		}
		return domainName;
	}

}

JSON工具类

package com.leyou.common.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * @author: HuYi.Zhang
 * @create: 2018-04-24 17:20
 **/
public class JsonUtils {

    public static final ObjectMapper mapper = new ObjectMapper();

    private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);

    public static String serialize(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj.getClass() == String.class) {
            return (String) obj;
        }
        try {
            return mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            logger.error("json序列化出错:" + obj, e);
            return null;
        }
    }

    public static <T> T parse(String json, Class<T> tClass) {
        try {
            return mapper.readValue(json, tClass);
        } catch (IOException e) {
            logger.error("json解析出错:" + json, e);
            return null;
        }
    }

    public static <E> List<E> parseList(String json, Class<E> eClass) {
        try {
            return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, eClass));
        } catch (IOException e) {
            logger.error("json解析出错:" + json, e);
            return null;
        }
    }

    public static <K, V> Map<K, V> parseMap(String json, Class<K> kClass, Class<V> vClass) {
        try {
            return mapper.readValue(json, mapper.getTypeFactory().constructMapType(Map.class, kClass, vClass));
        } catch (IOException e) {
            logger.error("json解析出错:" + json, e);
            return null;
        }
    }

    public static <T> T nativeRead(String json, TypeReference<T> type) {
        try {
            return mapper.readValue(json, type);
        } catch (IOException e) {
            logger.error("json解析出错:" + json, e);
            return null;
        }
    }
    @Data
    @AllArgsConstructor
    @NoArgsConstructor//构造函数
    static class User {
        String name;
        Integer age;
    }
    public static void main(String[] args) {
        User user = new User("Jack", 21);
        String json = JsonUtils.serialize(user);
        System.out.println(json);
    }
}

ID生成器,注意这个是使用了雪花算法的工具类,分布式系统中保证id唯一性的,当时面携程的时候就考了我这个问题,反正两个人的谈话牛头不对马嘴。

package com.leyou.common.utils;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;

/**
 * <p>名称:IdWorker.java</p>
 * <p>描述:分布式自增长ID</p>
 * <pre>
 *     Twitter的 Snowflake JAVA实现方案
 * </pre>
 * 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用:
 * 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
 * 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,
 * 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),
 * 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
 * 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),
 * 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。
 * <p>
 * 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))
 *
 * @author Polim
 */
public class IdWorker {
    // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
    private final static long twepoch = 1288834974657L;
    // 机器标识位数
    private final static long workerIdBits = 5L;
    // 数据中心标识位数
    private final static long datacenterIdBits = 5L;
    // 机器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 数据中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒内自增位
    private final static long sequenceBits = 12L;
    // 机器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 数据中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 时间毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生产id时间戳 */
    private static long lastTimestamp = -1L;
    // 0,并发控制
    private long sequence = 0L;

    private final long workerId;
    // 数据标识id部分
    private final long datacenterId;

    public IdWorker(){
        this.datacenterId = getDatacenterId(maxDatacenterId);
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }
    /**
     * @param workerId
     *            工作机器ID
     * @param datacenterId
     *            序列号
     */
    public IdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    /**
     * 获取下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;

        return nextId;
    }

    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * <p>
     * 获取 maxWorkerId
     * </p>
     */
    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(datacenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
         /*
          * GET jvmPid
          */
            mpid.append(name.split("@")[0]);
        }
      /*
       * MAC + PID 的 hashcode 获取16个低位
       */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    /**
     * <p>
     * 数据标识id部分
     * </p>
     */
    protected static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        } catch (Exception e) {
            System.out.println(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }


}

数字工具如下:

package com.leyou.common.utils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author: HuYi.Zhang
 * @create: 2018-04-25 09:13
 **/
public class NumberUtils {

    public static boolean isInt(Double num) {
        return num.intValue() == num;
    }

    /**
     * 判断字符串是否是数值格式
     * @param str
     * @return
     */
    public static boolean isDigit(String str){
        if(str == null || str.trim().equals("")){
            return false;
        }
        return str.matches("^\\d+$");
    }

    /**
     * 将一个小数精确到指定位数
     * @param num
     * @param scale
     * @return
     */
    public static double scale(double num, int scale) {
        BigDecimal bd = new BigDecimal(num);
        return bd.setScale(scale, RoundingMode.HALF_UP).doubleValue();
    }

    // 从字符串中根据正则表达式寻找,返回找到的数字数组
    public static Double[] searchNumber(String value, String regex){
        List<Double> doubles = new ArrayList<>();
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(value);
        if(matcher.find()) {
            MatchResult result = matcher.toMatchResult();
            for (int i = 1; i <= result.groupCount(); i++) {
                doubles.add(Double.valueOf(result.group(i)));
            }
        }
        return doubles.toArray(new Double[doubles.size()]);
    }

    /**
     * 生成指定位数的随机数字
     * @param len
     * @return
     */
    public static String generateCode(int len){
        len = Math.min(len, 8);
        int min = Double.valueOf(Math.pow(10, len - 1)).intValue();
        int num = new Random().nextInt(Double.valueOf(Math.pow(10, len + 1)).intValue() - 1) + min;
        return String.valueOf(num).substring(0,len);
    }
}

此时的结构如下

在这里插入图片描述

基础部分的搭建到这就搭建完成了,接下来做商城微服务环境的搭建

3 商品微服务

在leyou项目下创建商品微服务ly-item,注意打包方式为pom

接着在ly-item模块下,创建ly-item-interface模块:用来存放实体类,比如商品微服务向外提供了查询商品的接口,然后订单微服务调用了,但是订单微服务里面是没有商品类的。所以将实体类写到该模块,然后在提供服务的模块(后面写)中引入依赖即可,别的微服务调用的时候也就有了商品实体类。

注意:前两个模块均无需引入依赖,暂时只是创建出来,还什么都没做。

最后在ly-item模块下,创建ly-item-service模块:对外提供商品相关服务

添加依赖如下

<?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>
        <artifactId>ly-item</artifactId>
        <groupId>com.leyou.service</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.service</groupId>
    <artifactId>ly-item-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.service</groupId>
            <artifactId>ly-item-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.leyou.common</groupId>
            <artifactId>ly-common</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

添加配置文件application.yaml,如下:

server:
  port: 8081
spring:
  application:
    name: item-service
  datasource:
    url: jdbc:mysql://localhost:3306/yun6
    username: root
    password: 123456
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1

创建启动类

@SpringBootApplication
@EnableDiscoveryClient
public class LyItemApplication {
    public static void main(String[] args) {
        SpringApplication.run(LyItemApplication.class);
    }
}

结构如下

在这里插入图片描述

4 通用异常处理器

异常处处可见,所以提前写个使用与所有异常的通用异常处理器。

在ly-item-interface中添加实体类

package com.leyou.item.pojo;

import lombok.Data;

@Data
public class Item {
    private Integer id;
    private String name;
    private Long price;
}

在ly-item-service中添加Controller

@RestController
@RequestMapping("item")
public class ItemController {
    @Autowired
    private ItemService itemService;

    @PostMapping
    public ResponseEntity<Item> saveItem(Item item){
        if(item.getPrice() == null) {
            throw new LyException(ExceptionEnum.PRICE_CANNOT_BE_NULL);
        }
        return ResponseEntity.status(HttpStatus.CREATED).body(itemService.saveItem(item));
    }
}

注意在这里 ResponseEntity比@ResponseBody更实用,@ResponseBody是将响应结果放在了http响应体中,但是状态码是在响应行。ResponseEntity可以同时设置响应行和响应体。

LyException如下

package com.leyou.common.exception;

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class LyException extends RuntimeException{
    private ExceptionEnum exceptionEnum;
}

枚举类ExceptionEnum如下:

package com.leyou.common.enums;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum ExceptionEnum {
    PRICE_CANNOT_BE_NULL(400,"价格不能为空")
    ;
    private int status;
    private String msg;
}

异常抛出后,虽然SpringMVC会帮我们处理,但是默认的响应状态码都是500,所以还是自己来写异常处理器。(关于状态码问题是为了符合RestFul风格),参数错误应该返回状态码201而不是500

package com.leyou.common.advice;

@ControllerAdvice //默认是处理controler注解的类抛出的异常
public class CommonExceptionHandler {

    @ExceptionHandler(LyException.class)  //处理什么异常
    public ResponseEntity<ExceptionResult> handlerExc(LyException e){
        return ResponseEntity.status(e.getExceptionEnum().getStatus()).body(new ExceptionResult(e.getExceptionEnum()));
    }
}

响应ExceptionResult如下

package com.leyou.common.vo;

import com.leyou.common.enums.ExceptionEnum;
import lombok.Data;

@Data
public class ExceptionResult {
    private int status;
    private String message;
    private Long timestamp;

    public ExceptionResult(ExceptionEnum e) {
        this.status = e.getStatus();
        this.message = e.getMsg();
        this.timestamp = System.currentTimeMillis();
    }
}

整个结构如下

在这里插入图片描述
在这里插入图片描述

启动后,使用postman进行测试

1、传入price进行访问

在这里插入图片描述

2、不带参数进行访问

在这里插入图片描述

4 枚举

之前使用了枚举,如果对枚举不熟,下面做详细的介绍。

1、了解Enum

源码不多,就全部扣下来了

public abstract class Enum<E extends Enum<E>>
    implements Comparable<E>, Serializable {
	//枚举常数名字
	private final String name;
	//枚举常数的序号,默认0开始
	private final int ordinal;
	//返回该枚举常数的名字
    public final String name() {
        return name;
    }
	//同上
	public String toString() {
        return name;
    }
	//返回该枚举常数的序号
	public final int ordinal() {
        return ordinal;
    }
	//构造函数,传人枚举名字和序号
	protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
	//返回指定名字的枚举类型常数,静态的,使用Emun直接来调用
	public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                            String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }
	hashCode(),equals,compareTo(),getDeclaringClass()方法不需要了解了
}

做些测试

public enum Action {
	STOP,RIGHT,LEFT,UP,DOWN
}
public class EnumTest {
	public static void main(String[] args) {
		Action left = Action.LEFT;
		String left2 = left.toString();
		System.out.println(left2);
		
		Action left3 = Enum.valueOf(Action.class, "LEFT");
		String left4 = left3.name();
		int ordinal = left3.ordinal();
		System.out.println(ordinal);
		System.out.println(left4);
		
		Action left5 = Action.valueOf("LEFT");
		System.out.println(left5);

		Action[] actions = Action.values();
		for (Action action : actions) {
			System.out.println(action.name() + " " + action.ordinal());
		}
	}
}
输出:
LEFT
2
LEFT
LEFT
LEFT
2
LEFT
LEFT
STOP 0
RIGHT 1
LEFT 2
UP 3
DOWN 4

下面对Action进行反编译查看(搜狗浏览器下载目录下面有反编译工具)

public final class Action extends Enum
{

    private Action(String s, int i)
    {
        super(s, i);
    }

    public static Action[] values()
    {
        Action aaction[];
        int i;
        Action aaction1[];
        System.arraycopy(aaction = ENUM$VALUES, 0, aaction1 = new Action[i = aaction.length], 0, i);
        return aaction1;
    }

    public static Action valueOf(String s)
    {
        return (Action)Enum.valueOf(com/scu/enu/Action, s);
    }

    public static final Action STOP;
    public static final Action RIGHT;
    public static final Action LEFT;
    public static final Action UP;
    public static final Action DOWN;
    private static final Action ENUM$VALUES[];

    static 
    {
        STOP = new Action("STOP", 0);
        RIGHT = new Action("RIGHT", 1);
        LEFT = new Action("LEFT", 2);
        UP = new Action("UP", 3);
        DOWN = new Action("DOWN", 4);
        ENUM$VALUES = (new Action[] {
            STOP, RIGHT, LEFT, UP, DOWN
        });
    }
}

枚举类实际上是继承了Enum这个类,且定义为final,也就是不能被继承。

一旦这个类被加载后,静态块里面会初始化成员变量,也就是5个Action实例以及一个Action数组。

注:所有的实例都是static final的,构造方法也是私有的。构造方法中调用了super(s, i);表示调用了Enum类的构造方法private Enum(String name, int ordinal),为name何ordinal赋值,默认就是从0开始的。

另外提供了values()方法返回所有枚举类。

此外对继承来的values(class,name)进行了重写

2、枚举的高级运用

(1)、自定义构造函数

枚举类,除了由编译器自动生成的private构造函数外,也可以自行定义构造函数,条件是不得为公开,也不得在构造函数里面调用super(反编译后能看到自行调用)。

前面说到ordinal是实例顺序,默认0开始,如果不满足我们的需求,那么可以自行定义。

public enum Action2 {
	STOP(2),RIGHT(3),LEFT(5),UP(9),DOWN(7);
	
	private int value;
	private Action2(int value){
		this.value = value;
	}
	
	public int getValue(){
		return value;
	}
}
public class EnumTest2 {
	public static void main(String[] args) {
	
		Action2 left = Action2.LEFT;
		int value = left.getValue();
		System.out.println(value);
		
		Action2[] values = Action2.values();
		for (Action2 action2 : values) {
			System.out.println(action2.name() + " " 
						+ action2.getValue() + " " + action2.ordinal());
		}
		
	}
	
}
输出:
5
STOP 2 0
RIGHT 3 1
LEFT 5 2
UP 9 3
DOWN 7 4

反编译后

public final class Action2 extends Enum
{

    private Action2(String s, int i, int value)
    {
        super(s, i);
        this.value = value;
    }

    public int getValue()
    {
        return value;
    }

    public static Action2[] values()
    {
        Action2 aaction2[];
        int i;
        Action2 aaction2_1[];
        System.arraycopy(aaction2 = ENUM$VALUES, 0, aaction2_1 = new Action2[i = aaction2.length], 0, i);
        return aaction2_1;
    }

    public static Action2 valueOf(String s)
    {
        return (Action2)Enum.valueOf(com/scu/enu/Action2, s);
    }

    public static final Action2 STOP;
    public static final Action2 RIGHT;
    public static final Action2 LEFT;
    public static final Action2 UP;
    public static final Action2 DOWN;
    private int value;
    private static final Action2 ENUM$VALUES[];

    static 
    {
        STOP = new Action2("STOP", 0, 2);
        RIGHT = new Action2("RIGHT", 1, 3);
        LEFT = new Action2("LEFT", 2, 5);
        UP = new Action2("UP", 3, 9);
        DOWN = new Action2("DOWN", 4, 7);
        ENUM$VALUES = (new Action2[] {
            STOP, RIGHT, LEFT, UP, DOWN
        });
    }
}

再给个常见的例子

public enum Action5 {
	STOP("停止",4),RIGHT("向右",3),LEFT("向左",5),UP("向上",9),DOWN("向下",7);
	
	private int value;
	private String name;
	private Action5(int value){
		this.value = value;
	}
	private Action5(String name,int value){
		this.value = value;
		this.name = name;
	}
	public int getValue(){
		return value;
	}
	public String getName(){
		return name;
	}
}
Action5[] values5 = Action5.values();
	for (Action5 action2 : values5) {
		System.out.println(action2.getName() + " " 
					+ action2.getValue() );
	}
输出
停止 4
向右 3
向左 5
向上 9
向下 7

其实,构造函数里面,多传了个参数而已,其他是不变的,只是我们多加了个属性。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Action5.java

package com.scu.enu;

public final class Action5 extends Enum
{

    private Action5(String s, int i, int value)
    {
        super(s, i);
        this.value = value;
    }

    private Action5(String s, int i, String name, int value)
    {
        super(s, i);
        this.value = value;
        this.name = name;
    }

    public int getValue()
    {
        return value;
    }

    public String getName()
    {
        return name;
    }

    public static Action5[] values()
    {
        Action5 aaction5[];
        int i;
        Action5 aaction5_1[];
        System.arraycopy(aaction5 = ENUM$VALUES, 0, aaction5_1 = new Action5[i = aaction5.length], 0, i);
        return aaction5_1;
    }

    public static Action5 valueOf(String s)
    {
        return (Action5)Enum.valueOf(com/scu/enu/Action5, s);
    }

    public static final Action5 STOP;
    public static final Action5 RIGHT;
    public static final Action5 LEFT;
    public static final Action5 UP;
    public static final Action5 DOWN;
    private int value;
    private String name;
    private static final Action5 ENUM$VALUES[];

    static 
    {
        STOP = new Action5("STOP", 0, "\u505C\u6B62", 4);
        RIGHT = new Action5("RIGHT", 1, "\u5411\u53F3", 3);
        LEFT = new Action5("LEFT", 2, "\u5411\u5DE6", 5);
        UP = new Action5("UP", 3, "\u5411\u4E0A", 9);
        DOWN = new Action5("DOWN", 4, "\u5411\u4E0B", 7);
        ENUM$VALUES = (new Action5[] {
            STOP, RIGHT, LEFT, UP, DOWN
        });
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值