并不包含全部视频内容,大部分都按照操作文档来手搓代码,资料,代码都上传git。
〇、实际代码
0.1 Result封装
package com.sky.result;
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
0.2 全局异常处理
package com.sky.handler;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
}
参数BaseException,我们自定义的异常都是BaseException的子类。BaseException继承RuntimeException运行时异常。
0.3 拦截器-令牌校验
校验令牌,令牌校验失败,就会抛出异常,如果抛出异常,就会设置响应状态码为401
package com.sky.interceptor;
import com.sky.constant.JwtClaimsConstant;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:{}", empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
0.4 ThreadLocal封装
package com.sky.context;
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
0.5 对象转换器
java序列化与反序列化,将指定的类型转为指定的格式。0.6中将会用到这个对象。
package com.sky.json;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
//public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
0.6 SpringMVC消息 转换器
用于统一时间的格式。
MappingJackson2HttpMessageConverter对象有个很相似,不要倒错包。
在配置类WebMvcConfiguration中添加消息转换器。
/**
* 扩展Spring MVC框架的消息转化器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//converters中存取了全部的转换对象,有很多
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转化器加入容器中,默认是最后的优先级,所以要设置优先级最先执行。
converters.add(0,converter);
}
0.7 AOP公共字段填充
技术点:枚举、注解、AOP、反射
0.7.1 自定义注解 AutoFill
package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
0.7.2 定义枚举(操作的类型)
insert和update操作的属性不同。
package com.sky.enumeration;
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
0.7.3 定义切面类
切入点表达式+通知,进行逻辑操作。
这个位置有约定,修改时间,参数要是实体,不能穿属性,如果不是实体反射会找不到。
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充...");
//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType = autoFill.value();//获得数据库操作类型
//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT){
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE){
//为2个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
0.7.4 给方法添加注解
给添加和修改的方法添加注解,来进行捕获。
注解中的value属性,就是枚举类型,来标记是什么操作。
package com.sky.mapper;
@Mapper
public interface CategoryMapper {
/**
* 插入数据
* @param category
*/
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
" VALUES" +
" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
@AutoFill(value = OperationType.INSERT)
void insert(Category category);
/**
* 根据id修改分类
* @param category
*/
@AutoFill(value = OperationType.UPDATE)
void update(Category category);
}
0.8 OSS对象存储
出现过的问题:
上传成功了,但是回显时图片不显示。其实上传没问题。回显的地址其实也没问题。原因在于alioss权限要设置为公共读写,不要设置私有,设置私有后。上传成功后,就会在图片地址后面拼接一些英文数字字符串。这样就导致回显时,因为没有后面拼接的字符串,他就会加载不出来图片。
1定义OSS相关配置
application-dev.yml
sky:
alioss:
endpoint: oss-cn-hangzhou.aliyuncs.com
access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
bucket-name: sky-take-out
application.yml
sky:
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
2读取OSS配置
用于封装这四个属性,封装yml中的值。
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
3). 生成OSS工具类对象
创建配置类,项目启动时,创建OSSutil对象(并加入基本的四个属性值)。
要加@Bean,要不然后面注入不进去,扫描不到。
package com.sky.config;
import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
工具类封装
第一个参数是byte数组,就是文件对象封装的数组。
第二个参数,就是在服务器中存储的名字。
package com.sky.utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
4). 定义文件上传接口
注入工具类,完成逻辑开发
package com.sky.controller.admin;
import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀 dfdfdf.png
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名称
String objectName = UUID.randomUUID().toString() + extension;
//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}", e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
0.9 redis编写配置类(非必须)
前序步骤详情,查看8.7.1
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为
StringRedisSerializer序列化器。
package com.sky.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("开始创建redis模板对象...");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
0.10 HttpClient封装
封装get和post请求。
package com.sky.utils;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Http工具类
*/
public class HttpClientUtil {
static final int TIMEOUT_MSEC = 5 * 1000;
/**
* 发送GET方式请求
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url,Map<String,String> paramMap){
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
CloseableHttpResponse response = null;
try{
URIBuilder builder = new URIBuilder(url);
if(paramMap != null){
for (String key : paramMap.keySet()) {
builder.addParameter(key,paramMap.get(key));
}
}
URI uri = builder.build();
//创建GET请求
HttpGet httpGet = new HttpGet(uri);
//发送请求
response = httpClient.execute(httpGet);
//判断响应状态
if(response.getStatusLine().getStatusCode() == 200){
result = EntityUtils.toString(response.getEntity(),"UTF-8");
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (paramMap != null) {
//构造json格式数据
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
jsonObject.put(param.getKey(),param.getValue());
}
StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置数据类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}
}
一、nginx相关
1.1 使用
nginx.exe:启动,如果启动不成功,查看logs中的日志信息,排除一下是否是端口被占用,nginx默认是80端口。
html:打包后的代码放在此文件夹。多个前段项目都可以放进来。
只需要修改下配置文件。配置文件在conf文件夹中。
配置两个service即可,分别对应两个前端项目。
代码详情如下:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root "D:/dev/nginx-1.14.0/html/tijian";
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
server {
listen 81;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root "D:/dev/nginx-1.14.0/html/tijiancms";
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
1.2 反向代理和负载均衡
1.2.1 介绍
前端访问的地址和后端接口真实的地址不一致。
nginx 反向代理的好处:
-
提高访问速度
因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。
-
进行负载均衡
所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。
-
保证后端服务安全
因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。
1.2.2配置
都是在nginx.conf中进行配置。
1.2.2.1 反向代理
server{
listen 80;
server_name localhost;
location /api/{
proxy_pass http://localhost:8080/admin/; #反向代理
}
}
proxy_pass:该指令是用来设置代理服务器的地址,可以是主机名称,IP地址加端口号等形式。
如上代码的含义是:监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来。
图解:
红色区域进行替换。
1.2.2.2 负载均衡
负载均衡底层,就是使用的反向代理。
webservers就是一个ip端口组。
upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
server{
listen 80;
server_name localhost;
location /api/{
proxy_pass http://webservers/admin;#负载均衡
}
}
upstream:如果代理服务器是一组服务器的话,我们可以使用upstream指令配置后端服务器组。
如上代码的含义是:监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://webservers/admin,根据webservers名称找到一组服务器,根据设置的负载均衡策略(默认是轮询)转发到具体的服务器。
注:upstream后面的名称可自定义,但要上下保持一致。
nginx 负载均衡策略:
名称 | 说明 |
---|---|
轮询 | 默认方式 |
weight | 权重方式,默认为1,权重越高,被分配的客户端请求就越多 |
ip_hash | 依据ip分配方式,这样每个访客可以固定访问一个后端服务 |
least_conn | 依据最少连接方式,把请求优先分配给连接数少的后端服务 |
url_hash | 依据url分配方式,这样相同的url会被分配到同一个后端服务 |
fair | 依据响应时间方式,响应时间短的服务将会被优先分配 |
具体配置方式:
upstream webservers{
server 192.168.100.128:8080 weight=90;
server 192.168.100.129:8080 weight=10;
}
upstream webservers{
ip_hash;
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
其他的类似。
二、代码相关
2.1 build
创建一个VO对象,此处使用的是builder构造器。当然去new对象也可以。但是使用builder(),在实体中要加入@Builder注解
2.2 属性配置封装
使用场景:
拿到属性类中的实际值。jwtProperties 是上方依赖注入的。
这是一个属性类。真正的结果,在yml配置文件中。
数据封装在配置文件中。
2.3 登录md5密码加密
思路:
用户输入账号密码,传输到后端。后端根据用户名查询,返回整条user数据(判断用户是否存在)。在使用传输过来的密码,将此密码进行md5加密与查询出来的数据比对。(用户存在,判断密码是否正确)
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();
//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);
//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}
//密码比对
//需要进行md5加密,然后再进行比对
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}
if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}
//3、返回实体对象
return employee;
}
DigestUtils是spring提供的md5加密。
2.4 异常处理(全局异常处理器)
代码中尽量避免字符串的出现,所以都维护在常量类中。
操作步骤
1.将抛出的异常的类名复制出来,方便在全局异常处理器中进行捕获。
SQLIntegrityConstraintViolationException
2. 重写exceptionHandler方法,别忘了加上@ExceptionHandler注解。
3.代码实现捕获指定异常。
代码:
第二个方法是。
package com.sky.handler;
import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
*
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex) {
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
/**
* 捕获用户名重复异常(添加用户操作)
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
// Duplicate entry 'zhangsan' for key 'employee.idx_username'
log.error("异常信息:{}", ex.getMessage());
//提示前端 用户名已经存在,并且将用户名取出来
String message = ex.getMessage();
if (message.contains("Duplicate entry")) {
//空格进行分隔,取出第三个字符串,就是用户名
String[] strArray = message.split(" ");
String msg = strArray[2] + MessageConstant.YI_CUN_ZAI;//拼接已存在
return Result.error(msg);
}
//如果不是,返回未知错误。
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
2.5 ThreadLocal的使用
1.存入
//将当前员工id存入BaseContext中。BaseContext是对ThreadLocal的封装
BaseContext.setCurrentId(empId);
package com.sky.interceptor;
import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:{}", empId);
//将当前员工id存入BaseContext中。BaseContext是对ThreadLocal的封装
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
2.取出
serviceImpl中,取出 BaseContext.getCurrentId()
@Override
public void insertEmp(EmployeeDTO employeeDTO) {
Employee employee = Employee.builder()
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
//设置默认密码123456,并进行加密
.password(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()))
//设置状态,默认为1
.status(StatusConstant.ENABLE)
//从BaseContext中获取存储的信息
.createUser(BaseContext.getCurrentId())
.updateUser(BaseContext.getCurrentId())
.build();
BeanUtils.copyProperties(employeeDTO, employee);
employeeMapper.insertEmp(employee);
}
2.6 日期的格式
解决方式:
1). 方式一
在属性上加上注解,对日期进行格式化
但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。
2). 方式二(推荐 )
在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
/**
* 扩展Spring MVC框架的消息转化器
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转化器加入容器中
converters.add(0,converter);
}
对象转换器就是0.5
三、常识规范理解
3.1 dto、vo、pojo、Result
vo中v联想view,所示是返回给前端想要的属性。
dto是前端给后端传输的属性。
pojo是和数据库相对应的实体。
result规定,前后端传输都会封装为Result对象。
result其中包含:
{
"code": 0,
"data": {},
"msg": "string"
}
3.2 log
添加@Slf4j注解后,就可以使用log.info()了。最原始的方法就是 new。
3.3 唯一约束异常
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'zhangsan' for key 'employee.idx_username'
这个已经提示了idx_username是索引名称。Duplicate 重复。
解决办法:查看2.4
3.4 ThreadLocal存储
介绍:
ThreadLocal 并不是一个Thread,而是Thread的局部变量。 ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
常用方法:
-
public void set(T value) 设置当前线程的线程局部变量的值
-
public T get() 返回当前线程所对应的线程局部变量的值
-
public void remove() 移除当前线程的线程局部变量
一次请求,tomcat就会给我们分配一个线程。这一个请求中线程共享数据。
例如:在拦截器中输出,controller中输出,controller调用service再调用mapper,service输出,mapper输出 的 线程id都是一致的。
我们应该在拦截器的位置将数据(例如:当前人id)存储到ThreaLocal中,方便后续直接拿取。
封装详情见:0.4
使用详情见:2.5
3.5 请求参数相关
3.5.1 query
常见与get请求,query就是路径后面拼接。
3.5.2 body(json)
常见于post请求,参数都放在请求体中
3.5.3 header
请求头参数,token。在请求是携带在请求头中。
3.6 @JsonFormat
次注解用于格式化日期
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
3.7 Mybatis别名
mybatis:
#mapper配置文件
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sky.entity
参数类型,就可以直接写别名
3.8 数据类型
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
private Long id; 实体中用Long来对应。
3.9 路径参数+query参数
因为是post请求,所以参数要放在body中。
路径参数与http字符串参数 。
接口中id是Long类型,所以接口文档中用interger,不能用string,也不能用Integer(id会为null)。
3.10 相关注解
@PathVariable 路径参数
@RequestBody 前端传输json格式
@RequestParam 解析参数
正常接收
使用注解来解析,让springmvc来解决
3.11 yml配置属性
3.12 集合mybatis
对于数据是集合的,不用去遍历集合,一次一次的insert。可以批量操作,运用foreach。
错误示例:
flavors.stream().forEach(flavor ->{
//插入口味表
dishFlavorMapper.insertFavor(flavor);
});
3.13 mybatis插入操作返回主键id
useGeneratedKeys="true" keyProperty="id"
keyProperty这个id要和实体中相对应。
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status)
values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})
</insert>
这样在service中,就可以直接获取主键了。
//向菜品表插入1条数据
dishMapper.insert(dish);//后绪步骤实现
//获取insert语句生成的主键值
Long dishId = dish.getId();
3.14 bean名称
默认是类名首字母小写
当两个class名重复,使用@RestController("adminShopController") //指定bean的名称
3.15 ThreadLocal理解思路
java后端配置拦截器,解析token,会获取到用户的id。再将id设置到ThreadLocal中。因为每个请求都是单独的线程。
3.16 大数据量mysql优化
详情见数据库优化文章。
四、配置相关
4.1 mybatis
4.1.1 驼峰命名
mybatis:
configuration:
#开启驼峰命名
map-underscore-to-camel-case: true
五、状态码
5.1 401
401状态码是HTTP协议中的一个状态码,表示“未授权”(Unauthorized)
解决:查看请求是否进入到controller中,如果没进入,就要考虑拦截器。
可以查看token是否校验通过。
六、工具的使用
6.1 apifox
6.1.1 全局参数
右上角可以设置环境,也可以设置全局参数,例如token。
6.2 idea
6.2.1 抽离代码
ctrl + alt + m 抽方法
七、优雅代码
7.1 builder
实体上要添加@Builder注解
Employee employee = Employee.builder()
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
//设置默认密码123456,并进行加密
.password(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()))
//设置状态,默认为1
.status(StatusConstant.ENABLE)
//从BaseContext中获取存储的信息
.createUser(BaseContext.getCurrentId())
.updateUser(BaseContext.getCurrentId())
.build();
7.2 option判空
7.3 定义常量
public static final String KEY = "SHOP_STATUS";
package com.sky.controller.admin;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;
@RestController("adminShopController") //指定bean的名称
@Slf4j
@RequestMapping("/admin/shop")
public class ShopController {
public static final String KEY = "SHOP_STATUS";
@Autowired
RedisTemplate redisTemplate;
/**
* 设置店铺营业状态
* @param status
* @return
*/
@PutMapping("/{status}")
public Result updateStatus(@PathVariable Integer status){
log.info("设置店铺的营业状态为:{}",status == 1 ? "营业中" : "打烊中");
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set(KEY,status);
return Result.success();
}
/**
* 获取店铺的营业状态
* @return
*/
@GetMapping("/status")
public Result<Integer> getStatus(){
ValueOperations valueOperations = redisTemplate.opsForValue();
Integer shopStatus = (Integer) valueOperations.get(KEY);
log.info("获取店铺营业状态为:{}",shopStatus == 1 ? "营业中" : "打烊中");
return Result.success(shopStatus);
}
}
7.4 抽离代码
相同的代码,都抽离到if else 外面。
八、redis相关
Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件。
官网:Redis - The Real-time Data Platform 中文网:Redis中文网
Redis安装包分为windows版和Linux版:
-
Windows版下载地址:Releases · microsoftarchive/redis · GitHub
-
Linux版下载地址: Index of /releases/
8.0 redis安装
1)在Windows中安装Redis(项目中使用)
Redis的Windows版属于绿色软件,直接解压即可使用,解压后目录结构如下:
2)在Linux中安装Redis(简单了解)
在Linux系统安装Redis步骤:
-
将Redis安装包上传到Linux
-
解压安装包,命令:tar -zxvf redis-4.0.0.tar.gz -C /usr/local
-
安装Redis的依赖环境gcc,命令:yum install gcc-c++
-
进入/usr/local/redis-4.0.0,进行编译,命令:make
-
进入redis的src目录进行安装,命令:make install
安装后重点文件说明:
-
/usr/local/redis-4.0.0/src/redis-server:Redis服务启动脚本
-
/usr/local/redis-4.0.0/src/redis-cli:Redis客户端脚本
-
/usr/local/redis-4.0.0/redis.conf:Redis配置文件
8.1 Redis服务启动与停止
redis-server.exe redis.windows.conf
Redis服务默认端口号为 6379 ,通过快捷键Ctrl + C 即可停止Redis服务
当Redis服务启动成功后,可通过客户端进行连接。
8.2 客户端连接
再开一个cmd窗口
redis-cli.exe
exit退出,keys * 查看
通过redis-cli.exe命令默认连接的是本地的redis服务,并且使用默认6379端口。也可以通过指定如下参数连接:
-
-h ip地址
-
-p 端口号
-
-a 密码(如果需要)
8.3 修改配置文件
直接全局搜索pass。
设置Redis服务密码,修改redis.windows.conf
requirepass 123456
注意:
-
修改密码后需要重启Redis服务才能生效
-
Redis配置文件中 # 表示注释
重启Redis后,再次连接Redis时,需加上密码,否则连接失败。
redis-cli.exe -h localhost -p 6379 -a 123456
此时,-h 和 -p 参数可省略不写。因为是本地
8.4 可视化工具
git开发中有。或者sky资料中。
8.5* 常用数据类型及特点
解释说明:
-
字符串(string):普通字符串,Redis中最简单的数据类型
-
哈希(hash):也叫散列,类似于Java中的HashMap结构
-
列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList
-
集合(set):无序集合,没有重复元素,类似于Java中的HashSet
-
有序集合(sorted set/zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
8.6* redis常用命令
8.6.1 字符串操作命令
Redis 中字符串类型常用命令:
-
SET key value 设置指定key的值
-
GET key 获取指定key的值
-
SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
-
SETNX key value 只有在 key 不存在时设置 key 的值
更多命令可以参考Redis中文网:Redis中文网
操作
过期时间。刷新查看
> localhost connected!
> set name jack
OK
> get name
jack
> get abc
null
> setex code 30 123
OK
> setex code1 30 234
OK
> setex code1 30 234
OK
> get code1
234
> get code1
null
> setnx key1 itcast
1
> setnx key1 itheima
0
> get key1
itcast
8.6.2 哈希操作命令
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:
操作
> localhost connected!
> hset user name gzy
1
> hset user age 18
1
> hdel user name
1
> hget user name
null
> hset user name gzy1
1
> hkeys user
age
name
> kvals user
ERR unknown command 'kvals'
> hvals user
18
gzy1
8.6.3 列表操作
Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:
-
LPUSH key value1 [value2] 将一个或多个值插入到列表头部
-
LRANGE key start stop 获取列表指定范围内的元素
-
RPOP key 移除并获取列表最后一个元素
-
LLEN key 获取列表长度
-
BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止
操作
> localhost connected!
> lpush dz 1 2 3
3
> lrange dz
ERR wrong number of arguments for 'lrange' command
> lrange dz 0 -1
3
2
1
> lrange dz 0 2
3
2
1
> lrange dz 0 1
3
2
> rpop dz
1
> rpop dz
2
> lrange dz 0 -1
3
> llen dz
1
8.6.4 集合操作命令-无序去重
无序的,和插入顺序无关。
Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令:
-
SADD key member1 [member2] 向集合添加一个或多个成员
-
SMEMBERS key 返回集合中的所有成员
-
SCARD key 获取集合的成员数
-
SINTER key1 [key2] 返回给定所有集合的交集
-
SUNION key1 [key2] 返回所有给定集合的并集
-
SREM key member1 [member2] 移除集合中一个或多个成员
操作
> localhost connected!
> sadd set1 a 1 b 2
4
> smembers set1
2
a
b
1
> scard set1
4
> sadd set2 a b c d
4
> sinter set1 set2
a
b
> sunion set1 set2
2
1
a
c
b
d
> srem set1 a
1
8.6.5 有序操作命令 -根据double排序,不允许重复
有序集合,主要是用double类型来排序的。
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:
常用命令:
-
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
-
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
-
ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
-
ZREM key member [member ...] 移除有序集合中的一个或多个成员
操作
> localhost connected!
> zadd zset1 10.0 a 10.5 b
2
> zadd zset1 10.2 c
1
> zrange zset1 0 -1
a
c
b
> zrange zset1 0 -1 withscores
a
10
c
10.199999999999999
b
10.5
> zincrby zset1 5.0 a
15
> zrange zset1 0 -1 withscores
c
10.199999999999999
b
10.5
a
15
> zrem zset1 b
1
> zrange zset1 0 -1 withscores
c
10.199999999999999
a
15
8.6.6 通用命令
Redis的通用命令是不分数据类型的,都可以使用的命令:
-
KEYS pattern 查找所有符合给定模式( pattern)的 key
-
EXISTS key 检查给定 key 是否存在
-
TYPE key 返回 key 所储存的值的类型
-
DEL key 该命令用于在 key 存在是删除 key
代码
> localhost connected!
> keys *
set2
name
zset1
user
dz
key1
set1
> keys set*
set2
set1
> exists key1
1
> exists key
0
> type name
string
> type nmae
none
> type set1
set
> type dz
list
> del dz1
0
> del dz
1
> del set1 set2
2
8.7 java中操作redis
Spring Data Redis 是 Spring 的一部分,提供了在 Spring 应用中通过简单的配置就可以访问 Redis 服务,对 Redis 底层开发包进行了高度封装。在 Spring 项目中,可以使用Spring Data Redis来简化 Redis 操作。
Spring Boot提供了对应的Starter,maven坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:
-
ValueOperations:string数据操作
-
SetOperations:set类型数据操作
-
ZSetOperations:zset类型数据操作
-
HashOperations:hash类型的数据操作
-
ListOperations:list类型的数据操作
8.7.1 环境搭建
1). 导入Spring Data Redis的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2). 配置Redis数据源
sky:
redis:
host: localhost
port: 6379
password: 123456
database: 10
解释说明:
database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。
可视化工具中默认使用的是0.
可以通过修改Redis配置文件来指定数据库的数量。
在application.yml中添加读取application-dev.yml中的相关Redis配置
spring:
profiles:
active: dev
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}
3). 编写配置类,创建RedisTemplate对象
package com.sky.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
log.info("开始创建redis模板对象...");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
序列化器
解释说明:
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为
StringRedisSerializer序列化器。
4). 通过RedisTemplate对象操作Redis
在test下新建测试类
package com.sky.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;
@SpringBootTest
public class SpringDataRedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTemplate(){
System.out.println(redisTemplate);
//string数据操作
ValueOperations valueOperations = redisTemplate.opsForValue();
//hash类型的数据操作
HashOperations hashOperations = redisTemplate.opsForHash();
//list类型的数据操作
ListOperations listOperations = redisTemplate.opsForList();
//set类型数据操作
SetOperations setOperations = redisTemplate.opsForSet();
//zset类型数据操作
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}
}
说明RedisTemplate对象注入成功,并且通过该RedisTemplate对象获取操作5种数据类型相关对象。
上述环境搭建完毕后,接下来,我们就来具体对常见5种数据类型进行操作。
8.7.2 代码实现
package com.sky.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.*;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@SpringBootTest
public class SpringDataRedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void testRedisTempldate(){
System.out.println(redisTemplate);
ValueOperations valueOperations = redisTemplate.opsForValue();
HashOperations hashOperations = redisTemplate.opsForHash();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}
/**
* 操作字符串类型的数据
*/
@Test
public void testString(){
//set get setex setnx
ValueOperations valueOperations = redisTemplate.opsForValue();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
//set get
valueOperations.set("name","小明");
String name = (String) valueOperations.get("name");
System.out.println(name);
//setex
valueOperations.set("code","1234",3, TimeUnit.MINUTES);
valueOperations.setIfAbsent("lock","1");
valueOperations.setIfAbsent("lock","2");
}
/**
* 操作hash
*/
@Test
public void testHash(){
//hset hget hdel hkeys hvals
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("100","name","tom");
hashOperations.put("100","age","20");
hashOperations.put("100","gender","1");
//拿
String name = (String) hashOperations.get("100", "name");
System.out.println(name);
//获取指定key的所有key
Set keys = hashOperations.keys("100");
System.out.println(keys);
//获取指定key的value
List values = hashOperations.values("100");
System.out.println(values);
//删除
hashOperations.delete("100","age","name");
}
/**
* 操作列表类型的数据
*/
@Test
public void testList(){
//lpush lrange rpop llen
ListOperations listOperations = redisTemplate.opsForList();
listOperations.leftPushAll("mylist","a","b","c");
listOperations.leftPush("mylist","d");
List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);
listOperations.rightPop("mylist");
Long size = listOperations.size("mylist");
System.out.println(size);
}
/**
* 操作集合类型的数据
*/
@Test
public void testSet(){
//sadd smembers scard sinter sunion srem
SetOperations setOperations = redisTemplate.opsForSet();
setOperations.add("set1","a","b","c","d");
setOperations.add("set2","a","b","x","y");
Set members = setOperations.members("set1");
System.out.println(members);
Long size = setOperations.size("set1");
System.out.println(size);
Set intersect = setOperations.intersect("set1", "set2");
System.out.println(intersect);
Set union = setOperations.union("set1", "set2");
System.out.println(union);
setOperations.remove("set1","a","b");
}
/**
* 操作有序集合类型的数据
*/
@Test
public void testZset(){
//zadd zrange zincrby zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1","a",10);
zSetOperations.add("zset1","b",12);
zSetOperations.add("zset1","c",9);
zSetOperations.add("zset1","c",13);
Set zset1 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset1);
zSetOperations.incrementScore("zset1","c",10);
zSetOperations.remove("zset1","a","b");
}
/**
* 通用命令操作
*/
@Test
public void testCommon(){//通用类型直接用redisTemplate来操作
//keys exists type del
Set keys = redisTemplate.keys("*");
System.out.println(keys);
Boolean name = redisTemplate.hasKey("name");
Boolean set1 = redisTemplate.hasKey("set1");
for (Object key : keys) {
DataType type = redisTemplate.type(key);
System.out.println(type.name());
}
redisTemplate.delete("mylist");
}
}
@SpringbootTest不用要注释掉
8.7.3 redis缓存菜品
8.7.3.1 缓存数据-查询
一个用户点击1次,后台就会执行很多查询sql。当很多用户去操作。数据库压力就会很大。但用户每次的查询所展示的效果都是一样的。所以要缓存起来。正常是操作硬盘,缓存后是操作内存,速度大大提升了。
先到控制台,找到,我们对应哪个接口。就是请求controller。 对其操作redis。但是要保证数据的一致性。当缓存的菜品发生改变后,要清楚缓存重新添加。
实现思路
key可以存分类id,value对于java来说是一个集合,但是redis的类型和java类型是不一样的,redis得用string存储。相当于对list集合进行序列化存储。
代码
注意:取的时候用什么类型,存的时候就用什么类型。要对应,强转一下即可。
package com.sky.controller.user;
import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
//构造redis中的key,规则:dish_分类id
String key = "redis_" + categoryId;
//查询redis中是否存在菜品数据 . 这个位置放进去的是什么类型数据,取的时候就用什么类型,强转一下。
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
//如果存在,直接返回,无序查询数据库
if (list != null && list.size() > 0){
return Result.success(list);
}
//如果不存在,查询数据库,并写入redis中
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
}
8.7.3.2 数据一致性-修改,清理缓存
一下情况,数据有变,需要清理缓存重新添加。
代码
主要代码,已经抽离为方法了。
private void cleanCache(String pattern) {
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
全部代码:整个controller管理端的。
package com.sky.controller.admin;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;
@RestController
@Slf4j
@RequestMapping("/admin/dish")
public class DishController {
@Autowired
DishService dishService;
@Autowired
RedisTemplate redisTemplate;
/**
* 添加菜品
* @param dishDTO
* @return
*/
@PostMapping
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品:{}",dishDTO);
dishService.save(dishDTO);
//清理缓存数据
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);
return Result.success();
}
/**
* 分页
* @param dto
* @return
*/
@GetMapping("/page")
public Result<PageResult> page(DishPageQueryDTO dto){
log.info("菜品分页查询:{}",dto);
PageResult page = dishService.page(dto);
return Result.success(page);
}
/**
* 删除菜品,支持批量删除
* @param ids
* @return
*/
@DeleteMapping
public Result del(String ids){
log.info("删除菜品:{}",ids);
dishService.del(ids);
//清理缓存,不用那么复杂,直接全清除
cleanCache("dish_*");
return Result.success();
}
/**
* 回显数据,根据id查询
* @param id
* @return
*/
@GetMapping("/{id}")
public Result<DishVO> selectById(@PathVariable Long id){
log.info("根据id查询菜品:{}",id);
DishVO dishVO = dishService.selectById(id);
return Result.success(dishVO);
}
/**
* 修改菜品
* @param dishDTO
* @return
*/
@PutMapping
public Result update(@RequestBody DishDTO dishDTO){
log.info("修改菜品:{}",dishDTO);
dishService.update(dishDTO);
//清理缓存,如果改变了分类,其实是复杂的,因为有一个分类少1个,有一个分类多1个,所以不要搞复杂。直接全部清理
cleanCache("dish_*");
return Result.success();
}
/**
* 修改状态
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
public Result updateStatus(@PathVariable Integer status , Long id){
log.info("修改状态id,status---》 {},{}",id,status);
dishService.updateStatus(status,id);
//清理缓存,不要复杂,直接全删除
cleanCache("dish_*");
return Result.success();
}
private void cleanCache(String pattern) {
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
@GetMapping("/list")
public Result<List<Dish>> selectDishByCategoryId(Long categoryId){
log.info("根据分类id,查询菜品:{}",categoryId);
List<Dish> dishes = dishService.selectDishByCategoryId(categoryId);
return Result.success(dishes);
}
}
九、Spring Cache相关
9.1 基础
也就是说需要导入Spring Cache的maven坐标,使用哪个实现,就导入哪个实现的坐标,例如redis的坐标。Spring Cache的注解是通用的,对于缓存实现来说。直接使用注解就行。
起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version>
</dependency>
常用注解 :
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能,通常加在启动类上 |
@Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
Cacheable既可以取也可以放,CachePut只能放。
9.2 用法
配置文件
server:
port: 8888
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_cache_demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
redis:
host: localhost
port: 6379
password: 123456
database: 1
logging:
level:
com:
itheima:
mapper: debug
service: info
controller: info
pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
9.2.1 @CachePut的使用
spEL表达式 就是 spring el 表达式,固定写法 #开头。user要上下对应。
user. .叫做对象导航
代码
key的写法非常灵活。
package com.itheima.controller;
import com.itheima.entity.User;
import com.itheima.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserMapper userMapper;
@PostMapping
@CachePut(cacheNames = "userCache" ,key = "#user.id ") //key的生成:userCache::1 (默认样式)
// @CachePut(cacheNames = "userCache" ,key = "#result.id ") //对象导航
// @CachePut(cacheNames = "userCache" ,key = "#p0.id ") //p0第一个参数 p1第二个参数 也可以替换为a0
// @CachePut(cacheNames = "userCache" ,key = "#root.args[0].id ") //第一个参数
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
}
redis的展示,是可以树形结构的,以:分隔。
9.2.2 @Cacheable的使用
key需要用到el表达式 #id,就是到redis中查询的id。
代码
id要对应上。去redis中名为userCache找。 先查缓存,没有执行逻辑,执行完自动缓存。
底层就是aop,其实使用的是代理对象,在执行逻辑前,先去redis中查找......
/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据, *调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@GetMapping
@Cacheable(cacheNames = "userCache" ,key = "#id")
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
9.2.3 @CacheEvict的使用
代码
@DeleteMapping
@CacheEvict(cacheNames = "userCache" , key = "#id")
public void deleteById(Long id){
userMapper.deleteById(id);
}
用id=1测试,1已经被删除。
代码
删除名userCache下的所有键值对。
@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache" , allEntries = true) //allEntries 删除userCache下的所有键值对
public void deleteAll(){
userMapper.deleteAll();
}
9.3 缓存套餐
1.在启动类上添加注解
2.在用户端,查询list接口上,添加缓存,缓存套餐。有直接用,没有缓存。
3.保证数据一致性,当删除修改新增时,删除缓存。在管理端进行。
新增的套餐,先删除所有缓存,用户再执行list时,在重新加载到缓存。要不新增的套餐不会加入到缓存,因为一致从缓存中取数据。
删除套餐
修改套餐
十、HttpClient相关
10.1 基础
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包(客户端可以通过java程序构造http发送请求,例如小程序),并且它支持 HTTP 协议最新的版本和建议。我觉得可以联想成前端vue的ajax,axios
就是这一步,请求微信接口服务的。
HttpClient的maven坐标:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
HttpClient的核心API:
-
HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
-
HttpClients:可认为是构建器,可创建HttpClient对象。
-
CloseableHttpClient:实现类,实现了HttpClient接口。
-
HttpGet:Get方式请求类型。
-
HttpPost:Post方式请求类型。
HttpClient发送请求步骤:
-
创建HttpClient对象
-
创建Http请求对象
-
调用HttpClient的execute方法发送请求
我们并没有引入,但是也可以使用,是因为阿里云oss依赖传递,也使用了HttpClient的jar包。
10.2 测试
测试get和post请求
package com.sky.test;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class HttpClientTest {
/**
* 测试httpClient发送get请求
*/
@Test
public void testGet() throws Exception{
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8090/user/shop/status");
//发送请求,接受响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);
//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("状态码:"+statusCode);
HttpEntity entity = response.getEntity();
String entityStr = EntityUtils.toString(entity);
System.out.println("响应结果:"+entityStr);
//关闭资源
httpClient.close();
response.close();
}
/**
* 测试通过httpclient发送POST方式的请求
*/
@Test
public void testPOST() throws Exception{
// 创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8090/admin/employee/login");
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity entity = new StringEntity(jsonObject.toString());
//指定请求编码方式
entity.setContentEncoding("utf-8");
//数据格式
entity.setContentType("application/json");
httpPost.setEntity(entity);
//发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);
//解析返回结果
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为:" + statusCode);
HttpEntity entity1 = response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("响应数据为:" + body);
//关闭资源
response.close();
httpClient.close();
}
}