1. JWT令牌
1. 编写JWT工具类
<!--JWT令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
public class JWTUtils {
private static final String jwtToken = "123456Mszlu!@#$$";
public static String createToken(Long userId){
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
String token = jwtBuilder.compact();
return token;
}
public static Map<String, Object> checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
//测试用
public static void main(String[] args) {
BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
//加密所需的salt
textEncryptor.setPassword("mszlu_blog_$#@wzb_&^%$#");
//要加密的数据(数据库的用户名或密码)
String username = textEncryptor.encrypt("root");
String password = textEncryptor.encrypt("root");
System.out.println("username:"+username);
System.out.println("password:"+password);
}
}
2. 业务层登录注册退出案例
@Service
@Transactional
public class LoginServiceImpl implements LoginService {
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
private static final String slat = "mszlu!@#";
@Override
public Result login(LoginParam loginParam) {
/**
* 1. 检查参数是否合法
* 2. 根据用户名和密码去user表中查询 是否存在
* 3. 如果不存在 登录失败
* 4. 如果存在 ,使用jwt 生成token 返回给前端
* 5. token放入redis当中,redis token:user信息 设置过期时间
* (登录认证的时候 先认证token字符串是否合法,去redis认证是否存在)
*/
String account = loginParam.getAccount();
String password = loginParam.getPassword();
if (StringUtils.isBlank(account) || StringUtils.isBlank(password)){
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
}
password = DigestUtils.md5Hex(password + slat);
SysUser sysUser = sysUserService.findUser(account,password);
if (sysUser == null){
return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
}
String token = JWTUtils.createToken(sysUser.getId());
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
return Result.success(token);
}
//取出tocken
@Override
public SysUser checkToken(String token) {
if (StringUtils.isBlank(token)){
return null;
}
Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
if (stringObjectMap == null){
return null;
}
String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
if (StringUtils.isBlank(userJson)){
return null;
}
SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
return sysUser;
}
@Override
public Result logout(String token) {
redisTemplate.delete("TOKEN_"+token);
return Result.success(null);
}
@Override
public Result register(LoginParam loginParam) {
/**
* 1. 判断参数 是否合法
* 2. 判断账户是否存在,存在 返回账户已经被注册
* 3. 不存在,注册用户
* 4. 生成token
* 5. 存入redis 并返回
* 6. 注意 加上事务,一旦中间的任何过程出现问题,注册的用户 需要回滚
*/
String account = loginParam.getAccount();
String password = loginParam.getPassword();
String nickname = loginParam.getNickname();
if (StringUtils.isBlank(account)
|| StringUtils.isBlank(password)
|| StringUtils.isBlank(nickname)
){
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
}
SysUser sysUser = sysUserService.findUserByAccount(account);
if (sysUser != null){
return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),"账户已经被注册了");
}
sysUser = new SysUser();
sysUser.setNickname(nickname);
sysUser.setAccount(account);
sysUser.setPassword(DigestUtils.md5Hex(password+slat));
sysUser.setCreateDate(System.currentTimeMillis());
sysUser.setLastLogin(System.currentTimeMillis());
sysUser.setAvatar("/static/img/logo.b3a48c0.png");
sysUser.setAdmin(1); //1 为true
sysUser.setDeleted(0); // 0 为false
sysUser.setSalt("");
sysUser.setStatus("");
sysUser.setEmail("");
this.sysUserService.save(sysUser);
String token = JWTUtils.createToken(sysUser.getId());
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
return Result.success(token);
}
}
2. 自定义注解
一般写在 handle 包下进行拦截处理
//对加了@Controller注解的方法进行拦截处理 AOP的实现
@ControllerAdvice
public class AllExceptionHandler {
//进行异常处理,处理Exception.class的异常
//这个是拦截所有异常
@ExceptionHandler(Exception.class)
@ResponseBody //返回json数据
public Result doException(Exception ex){
ex.printStackTrace();
return Result.fail(-999,"系统异常");
}
}
5. 拦截器
1. 基本实现
应用场景:当某个资源我们每次请求都需要访问时,可以利用拦截器。
1. 拦截器实现
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)){
return true;
}
String token = request.getHeader("Authorization");
log.info("=================request start===========================");
String requestURI = request.getRequestURI();
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}", token);
log.info("=================request end===========================");
if (token == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//是登录状态,放行
return true;
}
}
2. 拦截器生效
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test");
}
}
2. 微服务中的实现-GateWay网关实现过滤器功能
规定对符合某种规则的的路径,进行一系列拦截处理
下面以一个 验证登陆状态 作为案例
1. 服务集成Redis
1. 依赖
2. 配置
3. 序列化器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
data:
redis:
host: localhost
port: 6379
@Configuration
public class RedisConfig {
@Bean
@Primary
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//String的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//序列号key value
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2. gateway模块添加全局Filter,统一处理登录
/**
* 全局Filter,统一处理会员登录
*/
@Slf4j
@Component
// GlobalFilter: GateWay里面的,通过它实现全局过滤器
// Ordered: 设置过滤器优先级
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private RedisTemplate<String , String> redisTemplate;
// 用来存储访问路径
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
log.info("path {}", path);
// 封装了一个方法,验证登录状态
UserInfo userInfo = this.getUserInfo(request);
//api接口,异步请求,与path会匹配,校验用户必须登录
if(antPathMatcher.match("/api/**/auth/**", path)) {
if(null == userInfo) { // 没有登陆,获取response 向前端发消息
ServerHttpResponse response = exchange.getResponse();
return out(response, ResultCodeEnum.LOGIN_AUTH);
}
}
// 方行的代码
return chain.filter(exchange);
}
@Override
public int getOrder() { //优先级
return 0;
}
// 给一个response和一个消息,单纯的向前端发送一个错误信息
private Mono<Void> out(ServerHttpResponse response, ResultCodeEnum resultCodeEnum) {
Result result = Result.build(null, resultCodeEnum);
byte[] bits = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
// 主要完成1.获取token 2.向Redis里查此token 3.查到转为实体类
private UserInfo getUserInfo(ServerHttpRequest request) {
String token = "";
List<String> tokenList = request.getHeaders().get("token");
if(null != tokenList) {
token = tokenList.get(0);
}
if(!StringUtils.isEmpty(token)) {
String userInfoJSON = redisTemplate.opsForValue().get("user:spzx:"+token);
if(StringUtils.isEmpty(userInfoJSON)) {
return null ;
}else {
return JSON.parseObject(userInfoJSON , UserInfo.class) ;
}
}
return null;
}
}
4. Redis缓存与Spring Cache框架
1. Spring Cache注解介绍
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能 |
@Cacheable | 在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
1. 加入Redis起步依赖与 Spring Cache
<!-- redis的起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2. 加入Redis配置信息
spring:
# Redis的相关配置
data:
redis:
host: localhost
port: 6379
3. 配置Redis序列化器
import org.springframework.cache.CacheManager;
@Configuration
public class RedisConfig {
@Bean
public CacheManager cacheManager(LettuceConnectionFactory connectionFactory) {
//定义序列化器
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
//过期时间600秒
.entryTtl(Duration.ofSeconds(600))
// 配置序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer));
RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
4. 启动类加入
@EnableCaching
5. 使用
// 查询数据先看Redis中是否有数据
// 将查询到的数据保存至Redis,key为 userCache::userId
@Cacheable(value = "userCache" , key = "#userId")
public User findById(Long userId) {
log.info("用户数据查询成功...");
User user = new User() ;
user.setAge(23);
user.setUserName("xz");
return user;
}
// 将插入的数据保存至Redis;使用返回数据user的username作为key
@CachePut(value = "userCache", key = "#user.userName")
public User saveUser(User user) {
log.info("用户数据保存成功...");
return user ;
}
// 删除数据同时将Redis数据删除
@CacheEvict(value = "userCache" , key = "#userId")
public void deleteById(Long userId) {
log.info("用户数据删除成功...");
}
6. 不带Spring Cache的使用方法
在每一个ServiceImpl中注入RedisTemplate
@Autowired
private RedisTemplate<String , String> redisTemplate ;
@Override
public List<Category> findOneCategory() {
// 利用key查找json字符串
String categoryListJSON = redisTemplate.opsForValue().get("category:one");
// 如果不是空,返回实体类数据
if(!StringUtils.isEmpty(categoryListJSON)) {
// 转换Json字符串
List<Category> categoryList = JSON.parseArray(categoryListJSON, Category.class);
return categoryList ;
}
// 如果是空,则需要查询数据库
List<Category> categoryList = categoryMapper.findOneCategory();
// 设置过期时间
redisTemplate.opsForValue().set("category:one" , JSON.toJSONString(categoryList) , 7 , TimeUnit.DAYS);
return categoryList ;
}
5. 图片服务器:七里云、Minon
· 七里云·代码示例
1. 需要前往 七里云-开发者中心 :https://developer.qiniu.com/,注册
2. 保存自己的key与password
2. 在 对象存储 - 空间管理 - 创建空间 - 选择地区:会生成测试域名
前端
export function upload(formdata) {
return request({
headers: {'Content-Type': 'multipart/form-data'},
url: '/upload',
method: 'post',
data: formdata
})
}
1. 导入依赖,配置文件
# 上传文件总的最大值
spring.servlet.multipart.max-request-size=20MB
# 单个文件的最大值
spring.servlet.multipart.max-file-size=2MB
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.7.0, 7.7.99]</version>
</dependency>
2. 书写七牛云工具类
@Component
public class QiniuUtils {
//测试的域名,有时加http,有时不加
public static final String url = "***********.com/";
private final String accessKey = "**********************";
private final String accessSecretKey = "***********************";
public boolean upload(MultipartFile file, String fileName){
//构造一个带指定 Region 对象的配置类
// 选择自己对应的地区
Configuration cfg = new Configuration(Region.huadong());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
String bucket = "xz-spzx-project";
//默认不指定key的情况下,以文件内容的hash值作为文件名
try {
byte[] uploadBytes = file.getBytes();
Auth auth = Auth.create(accessKey, accessSecretKey);
String upToken = auth.uploadToken(bucket);
Response response = uploadManager.put(uploadBytes, fileName, upToken);
//解析上传成功的结果
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
}
3. controller接口书写
@RestController
@RequestMapping("/admin/system")
public class FileUploadController {
@Autowired
private FileUploadService fileUploadService;
@Operation(summary = "图片上传")
@PostMapping(value = "/fileUpload")
public Result<String> fileUploadService(@RequestParam(value = "file") MultipartFile multipartFile) {
String fileUrl = fileUploadService.fileUpload(multipartFile) ;
return Result.build(fileUrl , ResultCodeEnum.SUCCESS);
}
}
4. service层书写
@Override
public String fileUpload(MultipartFile multipartFile) {
String fileName = UUID.randomUUID().toString() + "." + StringUtils.substringAfterLast(multipartFile.getOriginalFilename(), ".");
boolean upload = qiniuUtils.upload(multipartFile, fileName);
if (upload){
System.out.println(QiniuUtils.url + fileName);
return QiniuUtils.url + fileName;
}
return "500";
}
· Minon
1. 官网下载minon.exe
放到一个目录里,此目录下创建data,既此目录有 data文件夹和minon.exe
2. 启动成功后,可以通过给出的端口,key,password进入客户端
1. 第一个9000端口是我们上传文件要写的接口
2. 第二个端口不定,是我们的客户端
3. 创建一个桶,起名字,设置为公共的public,创建accessKey和secreKey
3. java工具模块里引入依赖
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.2</version>
</dependency>
4. controller层
@RestController
@RequestMapping("/admin/system")
public class FileUploadController {
@Autowired
private FileUploadService fileUploadService ;
@PostMapping(value = "/fileUpload")
public Result<String> fileUploadService(@RequestParam(value = "file") MultipartFile multipartFile) {
String fileUrl = fileUploadService.fileUpload(multipartFile) ;
return Result.build(fileUrl , ResultCodeEnum.SUCCESS) ;
}
}
5. service层
@Service
public class FileUploadServiceImpl implements FileUploadService {
@Autowired
private MinioProperties minioProperties ;
@Override
public String fileUpload(MultipartFile multipartFile) {
try {
// 创建一个Minio的客户端对象
MinioClient minioClient = MinioClient.builder()
.endpoint(minioProperties.getEndpointUrl())
.credentials(minioProperties.getAccessKey(), minioProperties.getSecreKey())
.build();
// 判断桶是否存在
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioProperties.getBucketName()).build());
if (!found) { // 如果不存在,那么此时就创建一个新的桶
minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioProperties.getBucketName()).build());
} else { // 如果存在打印信息
System.out.println("Bucket 'spzx-bucket' already exists.");
}
// 设置存储对象名称
String dateDir = DateUtil.format(new Date(), "yyyyMMdd");
String uuid = UUID.randomUUID().toString().replace("-", "");
//20230801/443e1e772bef482c95be28704bec58a901.jpg
String fileName = dateDir+"/"+uuid+multipartFile.getOriginalFilename();
System.out.println(fileName);
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(minioProperties.getBucketName())
.stream(multipartFile.getInputStream(), multipartFile.getSize(), -1)
.object(fileName)
.build();
minioClient.putObject(putObjectArgs) ;
return minioProperties.getEndpointUrl() + "/" + minioProperties.getBucketName() + "/" + fileName ;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
6. 利用定义资源类知识将下面四个数据加入yml
spzx: minio: endpointUrl: http://169.254.40.162:9000 accessKey: secreKey: bucketName: spzx-bucket
6. ThreadLocal获取当前用户·拦截器
ThreadLocal可以从一次 请求开始 到这次 请求结束 之间 完成数据共享
一般放在common模块工具类中
1. 基本实现
1. 创建工具类ThreadLocal类
public class ThreadLocalUtil {
// 创建一个ThreadLocal对象
private static final ThreadLocal<SysUser> threadLocal = new ThreadLocal<>() ;
// 定义存储数据的静态方法
public static void set(SysUser sysUser) {
threadLocal.set(sysUser);
}
// 定义获取数据的方法
public static SysUser get() {
return threadLocal.get() ;
}
// 删除数据的方法
public static void remove() {
threadLocal.remove();
}
}
2. 拦截器中去使用
使用ThreadLocal一定要在拦截器中关闭,否则会发生内存泄漏
@Component
public class LoginInterceptor implements HandlerInterceptor {//拦截器
@Autowired
private RedisTemplate<String , String> redisTemplate ;
@Override //true就放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求方式
String method = request.getMethod()
if("OPTIONS".equals(method)) { // 如果是跨域预检请求,直接放行
return true ;
}
// 获取token
String token = request.getHeader("token");
String sysUserInfoJson = redisTemplate.opsForValue().get("user:login:" + token);
//主要逻辑详见拦截器
//主要逻辑详见拦截器
//主要逻辑详见拦截器
// 将用户数据存储到ThreadLocal中
SysUser sysUser = JSON.parseObject(sysUserInfoJson, SysUser.class);
ThreadLocalUtil.set(sysUser);
// 延长Redis中的用户数据的有效时间
// 只有真正退出本用户了,才会结束(保证本用户一直可以有效)
redisTemplate.expire("user:login:" + token , 30 , TimeUnit.MINUTES) ;
// 放行
return true ;
}
@Override //请求结束,执行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ThreadLocalUtil.remove(); // 移除threadLocal中的数据
}
}
2. 微服务中实现
1. 引入依赖
<!-- spring boot web开发所需要的起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
2. 利用基本实现
这里比基础实现要简单些,因为一般而言,基础实现要完成登陆状态效验和本功能,但是微服务中网关会进行登录状态,因此就只需要简单的加入到线程类中去就好
public class UserLoginAuthInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String , String> redisTemplate ;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果token不为空,那么此时验证token的合法性
String userInfoJSON = redisTemplate.opsForValue().get("user:spzx:" + request.getHeader("token"));
ThreadLocalUtil.setUserInfo(JSON.parseObject(userInfoJSON , UserInfo.class));
return true ;
}
@Override //请求结束,执行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ThreadLocalUtil.remove(); // 移除threadLocal中的数据
}
}
3. 创建自定义注解,将配置类和拦截器导入
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
@Import(value = { UserLoginAuthInterceptor.class , UserWebMvcConfiguration.class})
public @interface EnableUserWebMvcConfiguration {
}
4. 加入到需要使用此功能的服务中的的启动类上
@EnableUserWebMvcConfiguration
7. 统一异常处理
1. 创建自定义异常类
可以建在 exception 包下
这个例子里 ResultCodeEnum 是将一些错误码错误消息封装起来了
@Data
public class GuiguException extends RuntimeException {
private Integer code ; // 错误状态码
private String message ; // 错误消息
private ResultCodeEnum resultCodeEnum ; // 封装错误状态码和错误消息
public GuiguException(ResultCodeEnum resultCodeEnum) {
this.resultCodeEnum = resultCodeEnum ;
this.code = resultCodeEnum.getCode() ;
this.message = resultCodeEnum.getMessage();
}
public GuiguException(Integer code , String message) {
this.code = code ;
this.message = message ;
}
}
2. 创建统一异常处理类
一般建在公共模块的 handle 包下
/**
* 统一异常处理类
*/
@ControllerAdvice
public class GlobalExceptionHandler {
//所有异常
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception e){
e.printStackTrace();
return Result.build(null , 201,"出现了异常") ;
}
//自定义异常
@ExceptionHandler(value = GuiguException.class)
@ResponseBody
public Result error(GuiguException exception) {
exception.printStackTrace();
return Result.build(null , exception.getResultCodeEnum()) ;
}
}
3. 找地方抛出这个错误
if(sysUser == null) {
throw new GuiguException(ResultCodeEnum.LOGIN_ERROR);
}
4. ResultCodeEnum封装的如下
@Getter // 提供获取属性值的getter方法
public enum ResultCodeEnum {
SUCCESS(200 , "操作成功") ,
LOGIN_ERROR(201 , "用户名或者密码错误"),
VALIDATECODE_ERROR(202 , "验证码错误") ,
LOGIN_AUTH(208 , "用户未登录"),
USER_NAME_IS_EXISTS(209 , "用户名已经存在"),
SYSTEM_ERROR(9999 , "您的网络有问题请稍后重试"),
NODE_ERROR( 217, "该节点下有子节点,不可以删除"),
DATA_ERROR(204, "数据异常"),
ACCOUNT_STOP( 216, "账号已停用"),
STOCK_LESS( 219, "库存不足"),
;
private Integer code ; // 业务状态码
private String message ; // 响应消息
private ResultCodeEnum(Integer code , String message) {
this.code = code ;
this.message = message ;
}
}
8. Security权限管理
所谓权限控制,就是为各个角色分配不同权限,使用Security会更加安全。
要有权限表(包含了权限id,权限路径)、角色与权限对应表
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1. 配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();//彩虹表加密策略,md5并不安全
}
public static void main(String[] args) {
//加密策略 彩虹表
String mszlu = new BCryptPasswordEncoder().encode("mszlu");
System.out.println(mszlu);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启登录认证
// .antMatchers("/user/findAll").hasRole("admin") //访此接口需要admin的角色
.antMatchers("/css/**").permitAll()//所有都可以
.antMatchers("/img/**").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/plugins/**").permitAll()
.antMatchers("/admin/**").access("@authService.auth(request,authentication)")
//@authService表示我们需要自定义一个service 来去实现实时的权限认证
.antMatchers("/pages/**").authenticated()
.and().formLogin()
.loginPage("/login.html") //自定义的登录页面
.loginProcessingUrl("/login") //登录处理接口
//前端name为username和password
.usernameParameter("username") //定义登录时的用户名的key 默认为username
.passwordParameter("password") //定义登录时的密码key,默认是password
.defaultSuccessUrl("/pages/main.html")
.failureUrl("/login.html")
.permitAll() //通过 不拦截,更加前面配的路径决定,这是指和登录表单相关的接口 都通过
.and().logout() //退出登录配置
.logoutUrl("/logout") //退出登录接口
.logoutSuccessUrl("/login.html")
.permitAll() //退出登录的接口放行
.and()
.httpBasic()
.and()
.csrf().disable() //csrf关闭 如果是自定义登录页面 需要关闭
.headers().frameOptions().sameOrigin();
}
}
2. 将登录的信息交给spring security
继承UserDetailsService,重写方法loadUserByUsername
判断数据库中是否有username这个用户,有的话将账号密码等交给spring security
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.GrantedAuthority;
@Component
@Slf4j
public class SecurityUserService implements UserDetailsService {
@Autowired
private AdminService adminService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username:{}",username);
//当用户登录的时候,springSecurity 就会将请求 转发到此
//根据用户名 查找用户,不存在 抛出异常,存在 将用户名,密码,授权列表 组装成springSecurity的User对象 并返回
Admin adminUser = adminService.findAdminByUserName(username);
if (adminUser == null){
throw new UsernameNotFoundException("用户名不存在");
}
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new User(username,adminUser.getPassword(), authorities);
//剩下的认证 就由框架帮我们完成
return userDetails;
}
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("123456"));
}
}
3. 查询用户是否存在
普通的查询数据库是否有username这个用户
@Service
public class AdminService {
@Autowired
private AdminMapper adminMapper;
@Autowired
private PermissionMapper permissionMapper;
public Admin findAdminByUserName(String username){
LambdaQueryWrapper<Admin> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Admin::getUsername,username).last("limit 1");
Admin adminUser = adminMapper.selectOne(queryWrapper);
return adminUser;
}
public List<Permission> findPermissionsByAdminId(Long adminId){
return permissionMapper.findPermissionsByAdminId(adminId);
}
}
public interface AdminMapper extends BaseMapper<Admin> {
}
public interface PermissionMapper extends BaseMapper<Permission> {
List<Permission> findPermissionsByAdminId(Long adminId);
}
4. 自定义的service
主要写逻辑的地点
查询到的已经登录的用户,需要通过查询它的权限表,从而给出对应的权限
@Service
@Slf4j
public class AuthService {
@Autowired
private AdminService adminService;
public boolean auth(HttpServletRequest request, Authentication authentication){
String requestURI = request.getRequestURI();
log.info("request url:{}", requestURI);
//true代表放行 false 代表拦截
Object principal = authentication.getPrincipal();
// anonymousUser 表示游客
if (principal == null || "anonymousUser".equals(principal)){
//未登录
return false;
}
UserDetails userDetails = (UserDetails) principal;
String username = userDetails.getUsername();
Admin admin = adminService.findAdminByUserName(username);
if (admin == null){
return false;
}
if (admin.getId() == 1){
//认为是超级管理员
return true;
}
List<Permission> permissions = adminService.findPermissionsByAdminId(admin.getId());
// 先去除请求的路径中可能存在的 ?
requestURI = StringUtils.split(requestURI,'?')[0];
// 相匹配成功的权限才会权限通过
for (Permission permission : permissions) {
//权限表中路径字段是否与发出的路径一致呢
if (requestURI.equals(permission.getPath())){
log.info("权限通过");
return true;
}
}
return false;
}
}
9. 利用ObjectMapper类解决JS无法识别17位及以上的数值
//防止前端 精度损失 把id转为string
// 分布式id 比较长,传到前端 会有精度损失,必须转为string类型 进行传输,就不会有问题了
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
10. mybatis-plus提供的字段自动填充
11. AOP实现自定义注解+Log日志打印
通过创建common-log模块,实现Log日志
需要使用的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
1. 自定义Log注解
//此注解是放到方法上的注解
@Target({ElementType.METHOD})
//此注解在运行时生效
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
//自定义注解的一些属性,设置一些默认值
// 模块名称,比如:"订单模块"
public String title();
// 自定义的操作人类型枚举类
public OperatorType operatorType() default OperatorType.MANAGE;
// 业务类型(0其它 1新增 2修改 3删除)
public int businessType() ;
// 是否保存请求的参数
public boolean isSaveRequestData() default true;
// 是否保存响应的参数
public boolean isSaveResponseData() default true;
}
自定义的操作人类型枚举类
public enum OperatorType { // 操作人类别
OTHER, // 其他
MANAGE, // 后台用户
MOBILE // 手机端用户
}
2. 定义切面类
@Aspect //定义切面
@Component //交给spring管理
@Slf4j //日志
public class LogAspect {
//注入service,注意:他的实现类可以不在此模块下
@Autowired
private AsyncOperLogService asyncOperLogService ;
// 环绕通知定义
@Around(value = "@annotation(sysLog)")
public Object doAroundAdvice(ProceedingJoinPoint joinPoint , Log sysLog) {
//业务执行前代码,构建前置参数
//SysOperLog 对应数据库信息
SysOperLog sysOperLog = new SysOperLog() ;
//自定义工具类将SysOperLog 不断完善信息
LogUtil.beforeHandleLog(sysLog , joinPoint , sysOperLog) ;
Object proceed = null;
try { // 代码执行进入到catch中,业务方法执行产生异常
proceed = joinPoint.proceed(); // 执行业务方法
//业务成功后执行的代码
//自定义工具类将SysOperLog 不断完善信息
LogUtil.afterHandlLog(sysLog , proceed , sysOperLog , 0 , null) ;
} catch (Throwable e) {
//业务失败后执行的代码
e.printStackTrace(); // 打印异常信息
//自定义工具类将SysOperLog 不断完善信息
LogUtil.afterHandlLog(sysLog , proceed , sysOperLog , 1 , e.getMessage());
//切记抛出异常!!!,否则事务失效
//原因:事务也基于aop实现且优先级高于自定义aop
throw new RuntimeException();
}
//这里可以写将返回结果保存至数据库
asyncOperLogService.saveSysOperLog(sysOperLog);
return proceed ; // 返回执行结果
}
}
例子:工具类和实体类
@Data
public class SysOperLog extends BaseEntity {
private String title; // 模块标题
private String method; // 方法名称
private String requestMethod; // 请求方式
private String operatorType; // 操作类别(0其它 1后台用户 2手机端用户)
private Integer businessType ; // 业务类型(0其它 1新增 2修改 3删除)
private String operName; // 操作人员
private String operUrl; // 请求URL
private String operIp; // 主机地址
private String operParam; // 请求参数
private String jsonResult; // 返回参数
private Integer status; // 操作状态(0正常 1异常)
private String errorMsg; // 错误消息
}
public class LogUtil {
//操作执行之后调用
public static void afterHandlLog(Log sysLog, Object proceed,
SysOperLog sysOperLog, int status ,
String errorMsg) {
//判断注解是否需要执行后参数
if(sysLog.isSaveResponseData()) {
sysOperLog.setJsonResult(JSON.toJSONString(proceed));
}
//设置状态
sysOperLog.setStatus(status);
//设置错误信息
sysOperLog.setErrorMsg(errorMsg);
}
//操作执行之前调用
public static void beforeHandleLog(Log sysLog,
ProceedingJoinPoint joinPoint,
SysOperLog sysOperLog) {
// 设置操作模块名称
sysOperLog.setTitle(sysLog.title());
//设置操作人类型
sysOperLog.setOperatorType(sysLog.operatorType().name());
// 获取目标方法信息
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature() ;
Method method = methodSignature.getMethod();
// 设置方法名称
sysOperLog.setMethod(method.getDeclaringClass().getName());
// 获取请求相关参数
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//设置请求方法
sysOperLog.setRequestMethod(request.getMethod());
//设置URL信息
sysOperLog.setOperUrl(request.getRequestURI());
//设置IP信息
sysOperLog.setOperIp(request.getRemoteAddr());
// 判断是否需要执行后参数,设置请求参数
if(sysLog.isSaveRequestData()) {
String requestMethod = sysOperLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
String params = Arrays.toString(joinPoint.getArgs());
sysOperLog.setOperParam(params);
}
}
sysOperLog.setOperName(AuthContextUtil.get().getUserName());
}
}
3. 通过自定义注解让Spring Boot可以扫描到
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
// 通过Import注解导入日志切面类到Spring容器中
@Import(value = LogAspect.class)
public @interface EnableLogAspect {
}
4. 在主服务里的启动类上加上
@EnableLogAspect
5. 测试使用
@Log(title = "角色添加",businessType = 0) //添加Log注解,设置属性
@PostMapping(value = "/saveSysRole")
public Result saveSysRole(@RequestBody SysRole SysRole) {
sysRoleService.saveSysRole(SysRole) ;
return Result.build(null , ResultCodeEnum.SUCCESS) ;
}
11. 阿里云短信服务
1. 前往 阿里云 - 云市场 - 短信服务
找到一个服务点开后,点击使用即可,我使用的是-【三网106短信】
2. 进入 阿里云管控中心查看AppCode
3. 复制Java代码的请求示例
public static void main(String[] args) {
String host = "https://gyytz.market.alicloudapi.com";
String path = "/sms/smsSend";
String method = "POST";
String appcode = "你自己的AppCode";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("mobile", "mobile"); //手机号
querys.put("param", "**code**:12345,**minute**:5"); //短信验证码
//smsSignId(短信前缀)和templateId(短信模板),可登录国阳云控制台自助申请。参考文档:http://help.guoyangyun.com/Problem/Qm.html
querys.put("smsSignId", "2e65b1bb3d054466b82f0c9d125465e2"); //短信前缀id
querys.put("templateId", "908e94ccf08b4476ba6c876d13f084ad"); //模板id
Map<String, String> bodys = new HashMap<String, String>();
try {
/**
* 重要提示如下:
* HttpUtils请从\r\n\t \t* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java\r\n\t \t* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
}
}
4. 固定的工具类与导入依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.3.7.v20160115</version>
</dependency>
import org.apache.http.HttpResponse;
import org.apache.commons.lang3.StringUtils;
import java.security.cert.X509Certificate;
public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
Map<String, String> bodys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Put String
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Put stream
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (!StringUtils.isBlank(path)) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
sbQuery.append(query.getValue());
}
if (!StringUtils.isBlank(query.getKey())) {
sbQuery.append(query.getKey());
if (!StringUtils.isBlank(query.getValue())) {
sbQuery.append("=");
sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
private static HttpClient wrapClient(String host) {
HttpClient httpClient = new DefaultHttpClient();
if (host.startsWith("https://")) {
sslClient(httpClient);
}
return httpClient;
}
private static void sslClient(HttpClient httpClient) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] xcs, String str) {
}
public void checkServerTrusted(X509Certificate[] xcs, String str) {
}
};
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", 443, ssf));
} catch (KeyManagementException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}
4. 向自己代码中改造请求示例
注意:根据模板自行改造,不一样的服务会有不一样的代码
修改如下:
1. AppCode
2. 模板ID:template_id;短信前缀:smsSignId
3. 手机号、验证码等
@Service
public class SmsServiceImpl implements SmsService {
@Autowired
private RedisTemplate<String , String> redisTemplate ;
@Override
public void sendValidateCode(String phone) {
// 为了测试使用,不用每次都发短信
String code = redisTemplate.opsForValue().get("phone:code:" + phone);
if(StringUtils.hasText(code)) {
return;
}
// 生成验证码
String validateCode = RandomStringUtils.randomNumeric(4);
// 过期时间
redisTemplate.opsForValue().set("phone:code:" + phone , validateCode , 5 , TimeUnit.MINUTES);
sendSms(phone , validateCode) ;
}
// 发送短信方法
public void sendSms(String phone, String validateCode) {
String host = "https://gyytz.market.alicloudapi.com";
String path = "/sms/smsSend";
String method = "POST";
String appcode = "****************************";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
Map<String, String> querys = new HashMap<String, String>();
querys.put("mobile", phone);
querys.put("param", "**code**:"+validateCode+",**minute**:5");
//smsSignId(短信前缀)和templateId(短信模板),可登录国阳云控制台自助申请。参考文档:http://help.guoyangyun.com/Problem/Qm.html
querys.put("smsSignId", "2e65b1bb3d054466b82f0c9d125465e2");
querys.put("templateId", "908e94ccf08b4476ba6c876d13f084ad");
Map<String, String> bodys = new HashMap<String, String>();
try {
/**
* 重要提示如下:
* HttpUtils请从\r\n\t \t* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java\r\n\t \t* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
throw new XZException(ResultCodeEnum.SYSTEM_ERROR);
}
}
}
12. 线程池技术优化查询、更新操作并行
13. 关于spring boot、spring cloud注解
14. MySql主从复制优化
15. Mybatis-puls常用方法
16. Elements plus中树形数据的处理方式
Elements plus里数据里,有child格式数据,属于数据里的数据,此时
17. 跨域问题
由于从8000端口想8001端口发送会出现跨域问题,需要配置类解决它
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
}
18. 验证码图片生成
用hutool工具包中的工具类生成图片验证码
19. mybatis与sql的妙用
1. mybatis的通过集合批量插入
利用 <foreach> 插入多条语句
<insert id="doAssign">
insert into sys_role_menu (
role_id,
menu_id,
create_time , update_time , is_deleted , is_half
) values
<foreach collection="menuIdList" item="menuInfo" separator=",">
(#{roleId} , #{menuInfo.id} , now() , now() , 0 , #{menuInfo.isHalf})
</foreach>
</insert>
2. sql语句多表查询-left join table_name on ***.id=***.id
category_brand 联合 category 表,brand表,根据category_brand中两个 id 值取他们的与之对应的数据。
- 当一个 id 有多个对应的数据会取下面的数据
- 多表查询,所有列名都要用表名去 点 .
<sql id="where">
<where>
<if test="categoryId != null and categoryId != ''">
and cb.category_id = #{categoryId}
</if>
<if test="brandId != null and brandId != ''">
and cb.brand_id = #{brandId}
</if>
and cb.is_deleted = 0
</where>
</sql>
<select id="findByPage" resultType="product.CategoryBrand">
select
cb.id,cb.brand_id,cb.category_id,cb.create_time,cb.update_time,
c.name as categoryName,
b.name as brandName, b.logo
from category_brand cb
left join category c on c.id = cb.category_id
left join brand b on b.id = cb.brand_id
<include refid="where" />
order by cb.update_time desc
</select>
20. 开源框架EasyExcel的使用
目的:实现在页面上下载数据导出至本地文件,从本地文件导入至数据库。
1. 导出实现
1.1 添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
</dependency>
1.2 定义一个实体类来封装excel每一行的数据
1.3 Controller层下载数据需要用到HttpServletResponse:
@GetMapping(value = "/exportData")
public void exportData(HttpServletResponse response) {
categoryService.exportData(response);
}
1.4 Service层必须定义头部信息,通过write写
public void exportData(HttpServletResponse response) {
try {
// 设置响应结果类型
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("分类数据", "UTF-8");
//完全固定格式
response.setHeader("Content-disposition",
"attachment;filename=" + fileName + ".xlsx");
//response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
// 查询数据库中的数据
List<Category> categoryList = categoryMapper.selectAll();
List<CategoryExcelVo> categoryExcelVoList = new ArrayList<>(categoryList.size());
//省略 Category 转 CategoryExcelVo
// 写出数据到浏览器端
// CategoryExcelVo.class 表示excel对应的数据,sheet是表名
EasyExcel.write(response.getOutputStream(), CategoryExcelVo.class)
.sheet("分类数据").doWrite(categoryExcelVoList);
} catch (IOException e) {
e.printStackTrace();
}
}
1.5 mapper层sql语句编写实现查询所有
2. 导入实现
1.1 添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.0</version>
</dependency>
1.2 定义一个实体类来封装excel每一行的数据
1.3 Controller层需要MultipartFile表示上传的文件
@PostMapping("importData")
public Result importData(MultipartFile file) {
categoryService.importData(file);
return Result.build(null , ResultCodeEnum.SUCCESS) ;
}
1.4 需要创建监听器
public class ExcelListener<T> extends AnalysisEventListener<T> {
//存储数据库100条,然后清理list ,方便内存回收,cachedDataList是缓存的数据
private static final int BATCH_COUNT = 100;
private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
//此类不能被spring管理,只能通过构造器!!!
private CategoryMapper categoryMapper;
public ExcelListener(CategoryMapper categoryMapper) {
this.categoryMapper = categoryMapper;
}
// 每解析一行数据就会调用一次该方法
@Override
public void invoke(T o, AnalysisContext analysisContext) {
cachedDataList.add(o);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// excel解析完毕以后需要执行的代码
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
}
private void saveData() {
//这里要强转下,强转成与excel对应的数据
categoryMapper.batchInsert((List<CategoryExcelVo>)cachedDataList);
}
}
1.5 Service层调用read
public void importData(MultipartFile file) {
try {
//创建监听器对象,传递mapper对象
ExcelListener<CategoryExcelVo> excelListener = new ExcelListener<>(categoryMapper);
//调用read方法读取excel数据
EasyExcel.read(file.getInputStream(),
CategoryExcelVo.class,
excelListener).sheet().doRead();
} catch (IOException e) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
}
1.6 mapper层的sql语句编写实现插入数据库数据
21. mybatis与mybatis-plus的分页查询
1. mybatis分页查询
1.1 配置文件
1.1.1 yml文件
# mybatis的配置 mybatis: config-location: classpath:/mybatis-config.xml # 其他配置 mapper-locations: classpath:/mapper/*.xml # 映射文件所在地
1.1.2 xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC
"-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 设置驼峰标识 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 打印SQL语句 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<plugins>
<!-- 分页插件 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>
</configuration>
1.2 具体实现
1.2.1 controller层
@GetMapping("/{page}/{limit}")
@Operation(summary = "查询所有品牌接口")
//page与limit是看前端传递来的名字
public Result<PageInfo<Brand>> findAll(@PathVariable(value = "page") Integer page,
@PathVariable(value = "limit")Integer limit){
return Result.build(brandService.findAll(page, limit), ResultCodeEnum.SUCCESS);
}
1.2.2 service层
public PageInfo<Brand> findAll(Integer page, Integer limit) {
PageHelper.startPage(page , limit);
List<Brand> all = brandMapper.findAll(page, limit);
PageInfo pageInfo = new PageInfo(all) ;
return pageInfo;
}
2. mybatis-plus分页查询
22. 资源类的创建
有些配置文件需要用到一些常量数据,可以定义 properties 包存资源类
1. 编写yml配置文件,写下常量
test: testtwo: testthree: - test - test - test
2. 创建properties包,新建资源类,加入以下注解
注意与配置文件对应关系
@Data
@ConfigurationProperties(prefix = "test.testtwo")
public class UserAuthProperties {
private List<String> testthree;
}
3. 启动类扫描我们定义的资源类
//扫描我们定义的资源类
@EnableConfigurationProperties(value = {UserAuthProperties.class})
public class ManagerApplication {
public static void main(String[] args) {
SpringApplication.run(ManagerApplication.class, args);
}
}