本章对Blog项目的技术进行总结
具体技术实现见相关文章
多模块项目
项目分为前后台,两套系统可能都会用到的代码可以写到一个公共模块中,让前台系统和后台系统分别取依赖公共模块。
① 创建父模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > com.sangeng</groupId > <artifactId > SGBlog</artifactId > <packaging > pom</packaging > <version > 1.0-SNAPSHOT</version > <modules > <module > sangeng-framework</module > <module > sangeng-admin</module > <module > sangeng-blog</module > </modules > <properties > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <java.version > 1.8</java.version > </properties > <dependencyManagement > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-dependencies</artifactId > <version > 2.5.0</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.33</version > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.0</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.3</version > </dependency > <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > 3.10.2</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > easyexcel</artifactId > <version > 3.0.5</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > <version > 2.9.2</version > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > <version > 2.9.2</version > </dependency > </dependencies > </dependencyManagement > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.1</version > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > <encoding > ${project.build.sourceEncoding}</encoding > </configuration > </plugin > </plugins > </build > </project >
②创建公共子模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <parent > <artifactId > SGBlog</artifactId > <groupId > com.sangeng</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > sangeng-framework</artifactId > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > easyexcel</artifactId > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > </dependency > </dependencies > </project >
③创建博客后台模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <parent > <artifactId > SGBlog</artifactId > <groupId > com.sangeng</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > sangeng-admin</artifactId > <dependencies > <dependency > <groupId > com.sangeng</groupId > <artifactId > sangeng-framework</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies > </project >
④创建博客前台模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <parent > <artifactId > SGBlog</artifactId > <groupId > com.sangeng</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > sangeng-blog</artifactId > <dependencies > <dependency > <groupId > com.sangeng</groupId > <artifactId > sangeng-framework</artifactId > <version > 1.0-SNAPSHOT</version > </dependency > </dependencies > </project >
整合MP 1.导入依赖(已在父模块导入)
2.配置yml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 server: port: 7777 spring: redis: host: 192.168 .111 .130 datasource: url: jdbc:mysql://localhost:3306/sg_blog?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 2MB max-request-size: 5MB mybatis-plus: mapper-locations: classpath:mapper/*.xml typeAliasesPackage: com.maple.demo.bean configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true cache-enabled: true global-config: db-config: logic-delete-field: delFlag logic-delete-value: 1 logic-not-delete-value: 0 id-type: auto
字段填充器 MetaObjectHandler
是 MyBatis-Plus 提供的一个用于处理实体对象的元数据的接口,可以用来自动填充一些常用字段,例如创建时间、修改时间等。
1.定义字段处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill (MetaObject metaObject) { Long userId = null ; try { userId = SecurityUtils.getUserId(); } catch (Exception e) { e.printStackTrace(); userId = -1L ; } this .setFieldValByName("createTime" , LocalDateTime.now(), metaObject); this .setFieldValByName("createBy" ,userId , metaObject); this .setFieldValByName("updateTime" , LocalDateTime.now(), metaObject); this .setFieldValByName("updateBy" , userId, metaObject); } @Override public void updateFill (MetaObject metaObject) { this .setFieldValByName("updateTime" , LocalDateTime.now(), metaObject); this .setFieldValByName(" " , SecurityUtils.getUserId(), metaObject); } }
2.使用注解标记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @TableField(fill = FieldFill.INSERT) private Long createBy; @TableField(fill = FieldFill.INSERT) private Date createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private Long updateBy; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime;
模型转换 DO->DTO->VO
Bean拷贝工具类封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class BeanCopyUtils { private BeanCopyUtils () { } public static <V> V copyBean (Object source,Class<V> clazz) { V result = null ; try { result = clazz.newInstance(); BeanUtils.copyProperties(source, result); } catch (Exception e) { e.printStackTrace(); } return result; } public static <O,V> List<V> copyBeanList (List<O> list,Class<V> clazz) { return list.stream() .map(o -> copyBean(o, clazz)) .collect(Collectors.toList()); } }
整合SpringSecurity 统一异常处理 实际我们在开发过程中可能需要做很多的判断校验,如果出现了非法情况我们是期望响应对应的提示的。但是如果我们每次都自己手动去处理就会非常麻烦。我们可以选择直接抛出异常的方式,然后对异常进行统一处理。把异常中的信息封装成ResponseResult响应给前端。
自定义异常父类SystemException
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class SystemException extends RuntimeException { private int code; private String msg; public int getCode () { return code; } public String getMsg () { return msg; } public SystemException (AppHttpCodeEnum httpCodeEnum) { super (httpCodeEnum.getMsg()); this .code = httpCodeEnum.getCode(); this .msg = httpCodeEnum.getMsg(); } }
GlobalExceptionHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(SystemException.class) public ResponseResult systemExceptionHandler (SystemException e) { log.error("出现了异常! {}" ,e); return ResponseResult.errorResult(e.getCode(),e.getMsg()); } @ExceptionHandler(Exception.class) public ResponseResult exceptionHandler (Exception e) { log.error("出现了异常! {}" ,e); return ResponseResult.errorResult(AppHttpCodeEnum.SYSTEM_ERROR.getCode(),e.getMessage()); } }
统一返回结果 定义统一返回结果类,使用泛型封装内部数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 package com.me.dto;import com.fasterxml.jackson.annotation.JsonInclude;import com.me.enums.AppHttpCodeEnum;import java.io.Serializable;@JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult <T> implements Serializable { private Integer code; private String msg; private T data; public ResponseResult () { this .code = AppHttpCodeEnum.SUCCESS.getCode(); this .msg = AppHttpCodeEnum.SUCCESS.getMsg(); } public ResponseResult (Integer code, T data) { this .code = code; this .data = data; } public ResponseResult (Integer code, String msg, T data) { this .code = code; this .msg = msg; this .data = data; } public ResponseResult (Integer code, String msg) { this .code = code; this .msg = msg; } public static ResponseResult errorResult (int code, String msg) { ResponseResult result = new ResponseResult (); return result.error(code, msg); } public static ResponseResult okResult () { ResponseResult result = new ResponseResult (); return result; } public static ResponseResult okResult (int code, String msg) { ResponseResult result = new ResponseResult (); return result.ok(code, null , msg); } public static ResponseResult okResult (Object data) { ResponseResult result = setAppHttpCodeEnum(AppHttpCodeEnum.SUCCESS, AppHttpCodeEnum.SUCCESS.getMsg()); if (data!=null ) { result.setData(data); } return result; } public static ResponseResult errorResult (AppHttpCodeEnum enums) { return setAppHttpCodeEnum(enums,enums.getMsg()); } public static ResponseResult errorResult (AppHttpCodeEnum enums, String msg) { return setAppHttpCodeEnum(enums,msg); } public static ResponseResult setAppHttpCodeEnum (AppHttpCodeEnum enums) { return okResult(enums.getCode(),enums.getMsg()); } private static ResponseResult setAppHttpCodeEnum (AppHttpCodeEnum enums, String msg) { return okResult(enums.getCode(),msg); } public ResponseResult<?> error(Integer code, String msg) { this .code = code; this .msg = msg; return this ; } public ResponseResult<?> ok(Integer code, T data) { this .code = code; this .data = data; return this ; } public ResponseResult<?> ok(Integer code, T data, String msg) { this .code = code; this .data = data; this .msg = msg; return this ; } public ResponseResult<?> ok(T data) { this .data = data; return this ; } public Integer getCode () { return code; } public void setCode (Integer code) { this .code = code; } public String getMsg () { return msg; } public void setMsg (String msg) { this .msg = msg; } public T getData () { return data; } public void setData (T data) { this .data = data; } }
统一返回消息 用枚举类进一步封装code与msg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.me.enums;public enum AppHttpCodeEnum { SUCCESS(200 ,"操作成功" ), NEED_LOGIN(401 ,"需要登录后操作" ), NO_OPERATOR_AUTH(403 ,"无权限操作" ), SYSTEM_ERROR(500 ,"出现错误" ), USERNAME_EXIST(501 ,"用户名已存在" ), PHONENUMBER_EXIST(502 ,"手机号已存在" ), EMAIL_EXIST(503 , "邮箱已存在" ), REQUIRE_USERNAME(504 , "必需填写用户名" ), LOGIN_ERROR(505 ,"用户名或密码错误" ), CONTENT_NOT_NULL(506 , "评论不能为空" ), USERNAME_NOT_NULL(507 , "用户名不能为空" ), PASSWORD_NOT_NULL(508 , "密码不能为空" ), EMAIL_NOT_NULL(509 , "邮箱不能为空" ), NICKNAME_NOT_NULL(510 , "昵称不能为空" ), ID_NOT_NULL(511 , "id不能为空" ), REQUEST_BODY_NOT_NULL(512 , "请求体不能为空" ), PARAM_NOT_NULL(513 , "参数为不能空" ); int code; String msg; AppHttpCodeEnum(int code, String errorMessage){ this .code = code; this .msg = errorMessage; } public int getCode () { return code; } public String getMsg () { return msg; } }
统一常量管理 定义常量类SystemConstants管理常量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.me.constants;public class SystemConstants { public static final int ARTICLE_STATUS_DRAFT = 1 ; public static final int ARTICLE_STATUS_NORMAL = 0 ; public static final int ROLE_STATUS_NORMAL = 0 ; public static final int USER_STATUS_NORMAL = 0 ; public static final String CATEGORY_STATUS_NORMAL = "0" ; public static final String CATEGORY_STATUS_BAN = "0" ; public static final String Link_STATUS_NORMAL = "0" ; public static final String Link_STATUS_BAN = "1" ; public static final int ROOT_COMMENT_ID = -1 ; public static final String COMMENT_TYPE_ARTICLE = "0" ; public static final String COMMENT_TYPE_LINK = "1" ; public static final String FILE_UPLOAD_DOMAIN = "http://rpnj2nc9r.bkt.clouddn.com/" ; public static final String ARTICLE_COUNT_KEY="article:viewCount" ; public static final String ROLE_TYPE_C="C" ; public static final String ROLE_TYPE_F="F" ; }
统一时间响应格式 webConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Bean public HttpMessageConverter fastJsonHttpMessageConverters () { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter (); FastJsonConfig fastJsonConfig = new FastJsonConfig (); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss" ); SerializeConfig.globalInstance.put(Long.class, ToStringSerializer.instance); fastJsonConfig.setSerializeConfig(SerializeConfig.globalInstance); fastConverter.setFastJsonConfig(fastJsonConfig); HttpMessageConverter<?> converter = fastConverter; return converter; } @Override public void configureMessageConverters (List<HttpMessageConverter<?>> converters) { converters.add(fastJsonHttpMessageConverters()); }
统一分页Vo 1 2 3 4 5 6 7 @Data @NoArgsConstructor @AllArgsConstructor public class PageVo { private List rows; private Long total; }
查询多级分类树 1.根据需求查询所有的数据封装进集合(此时它们的子节点都为null)
2.转换为期望的返回结果(Vo)集合
3.定义方法传入顶层父级节点id与所有节点集合,调用设置子节点的方法
4.定义寻找子节点的方法,自行设置子节点,并递归调用
具体代码例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public List<Menu> selectRouterMenuTreeByUserId (Long userId) { List<Menu> menuTree = builderMenuTree(menus,0L ); return menuTree; } private List<MenuVo> buildMenuTree (List<Menu> menus, Long parentId) { List<MenuVo> menuVos = BeanCopyUtils.copyBeanList(menus, MenuVo.class); List<MenuVo> menuTree = menuVos.stream() .filter(menuVo -> menuVo.getParentId().equals(parentId)) .map(menuVo -> menuVo.setChildren(getChildrenMenu(menuVo, menuVos))) .collect(Collectors.toList()); return menuTree; } private List<MenuVo> getChildrenMenu (MenuVo menuVo, List<MenuVo> menuVos) { List<MenuVo> childrenList = menuVos.stream() .filter(menuVo1 -> menuVo1.getParentId().equals(menuVo.getId())) .map(menuVo1 -> menuVo1.setChildren(getChildrenMenu(menuVo1, menuVos))) .collect(Collectors.toList()); return childrenList; }
OSS对象存储(七牛云) 因为如果把图片视频等文件上传到自己的应用的Web服务器,在读取图片的时候会占用比较多的资源。影响应用服务器的性能。
所以我们一般使用OSS(Object Storage Service对象存储服务)存储图片或视频。
①添加依赖
1 2 3 4 5 <dependency > <groupId > com.qiniu</groupId > <artifactId > qiniu-java-sdk</artifactId > <version > [7.7.0, 7.7.99]</version > </dependency >
②配置
application.yml
1 2 3 4 oss: accessKey: xxxx secretKey: xxxx bucket: sg-blog
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Service @Data @ConfigurationProperties(prefix = "oss") public class UploadServiceImpl implements IUploadService { private String accessKey; private String secretKey; private String bucket; @Override public ResponseResult uploadImg (MultipartFile img) { String url=ossUpload(img); return ResponseResult.okResult(url); } private String ossUpload (MultipartFile imgFile) { Configuration cfg = new Configuration (Region.autoRegion()); cfg.resumableUploadAPIVersion = Configuration.ResumableUploadAPIVersion.V2; UploadManager uploadManager = new UploadManager (cfg); InputStream is=null ; try { is=imgFile.getInputStream(); } catch (Exception e) { throw new RuntimeException (e); } String key = PathUtils.generateFilePath(imgFile.getOriginalFilename()); Auth auth = Auth.create(accessKey, secretKey); String upToken = auth.uploadToken(bucket); try { Response response= uploadManager.put(is,key,upToken,null ,null ); DefaultPutRet putRet = new Gson ().fromJson(response.bodyString(), DefaultPutRet.class); System.out.println(putRet.key); System.out.println(putRet.hash); } catch (QiniuException ex) { Response r = ex.response; System.err.println(r.toString()); try { System.err.println(r.bodyString()); } catch (QiniuException ex2) { } } return SystemConstants.FILE_UPLOAD_DOMAIN+key; } }
AOP日志记录 打印格式为
1 2 3 4 5 6 7 8 9 "=======Start=====" "URL : " BusinessName : "HTTP Method : " Class Method : "IP : " Request Args : "Response : " =======End======="
1.自定义注解 自定义注解标识需要打印日志的方法
1 2 3 4 5 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SystemLog { String businessName () ; }
2.定义切面类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 @Component @Aspect @Slf4j public class LogAspect { @Pointcut("@annotation(com.me.annotation.SystemLog)") public void pt () { } @Around("pt()") public Object printLog (ProceedingJoinPoint point) throws Throwable { Object res; try { handleBefore(point); res = point.proceed(); handleAfter(res); } finally { log.info("=======End=======" + System.lineSeparator()); } return res; } private void handleBefore (ProceedingJoinPoint point) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); SystemLog systemLog=getSysLog(point); log.info("=======Start=======" ); log.info("URL : {}" ,request.getRequestURL()); log.info("BusinessName : {}" ,systemLog.businessName() ); log.info("HTTP Method : {}" , request.getMethod()); log.info("Class Method : {}.{}" , point.getSignature().getDeclaringTypeName(),((MethodSignature) point.getSignature()).getName()); log.info("IP : {}" ,request.getRemoteHost()); log.info("Request Args : {}" , JSON.toJSONString(point.getArgs())); } private SystemLog getSysLog (ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); SystemLog systemLog = signature.getMethod().getAnnotation(SystemLog.class); return systemLog; } private void handleAfter (Object res) { log.info("Response : {}" , JSON.toJSONString(res)); } }
AOP+Redis限流 1.自定义注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { String key () default "limiter-" ; int time () default 60 ; int count () default 100 ; LimitType limitType () default LimitType.DEFAULT; String limitMsg () default "访问过于频繁,请稍候再试" ; }
2.编写切面类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @Aspect @Component public class RateLimiterAspect { private final static Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); @Autowired private StringRedisTemplate redisUtils; @Pointcut("@annotation(com.me.annotation.RateLimiter)") public void pt () { } @Before("pt()") public void doBefore (JoinPoint point) throws Throwable { RateLimiter rateLimiter=getRateLimiter(point); int time = rateLimiter.time(); int count = rateLimiter.count(); long total = 1L ; String combineKey = getCombineKey(rateLimiter, point); try { if (redisUtils.hasKey(combineKey)) { total = redisUtils.opsForValue().increment(combineKey); if (total > count) throw new SystemException (AppHttpCodeEnum.REQUEST_FAST); } else { redisUtils.opsForValue().set(combineKey, "1" , time, TimeUnit.SECONDS); } } catch (SystemException e) { throw e; } catch (Exception e) { throw e; } } private RateLimiter getRateLimiter (JoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); RateLimiter rateLimiter = signature.getMethod().getAnnotation(RateLimiter.class); return rateLimiter; } public String getCombineKey (RateLimiter rateLimiter, JoinPoint point) { StringBuffer stringBuffer = new StringBuffer (rateLimiter.key()); if (rateLimiter.limitType() == LimitType.IP) { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); stringBuffer.append(request.getRemoteHost()).append("-" ); } MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); stringBuffer.append(targetClass.getName()).append("-" ).append(method.getName()); return stringBuffer.toString(); } }
项目启动时预处理 如果希望在SpringBoot应用启动时进行一些初始化操作可以选择使用CommandLineRunner来进行处理。
我们只需要实现CommandLineRunner接口,并且把对应的bean注入容器。把相关初始化的代码重新到需要重新的方法中。
这样就会在应用启动的时候执行对应的代码。
1 2 3 4 5 6 7 8 @Component public class TestRunner implements CommandLineRunner { @Override public void run (String... args) throws Exception { System.out.println("程序初始化" ); } }
定时任务 定时任务的实现方式有很多,比如XXL-Job等。但是其实核心功能和概念都是类似的,很多情况下只是调用的API不同而已。
这里就先用SpringBoot为我们提供的定时任务的API来实现一个简单的定时任务
① 使用@EnableScheduling注解开启定时任务功能
我们可以在配置类上加上@EnableScheduling
1 2 3 4 5 6 7 8 @SpringBootApplication @MapperScan("com.sangeng.mapper") @EnableScheduling public class SanGengBlogApplication { public static void main (String[] args) { SpringApplication.run(SanGengBlogApplication.class,args); } }
② 确定定时任务执行代码,并配置任务执行时间
使用@Scheduled注解标识需要定时执行的代码。注解的cron属性相当于是任务的执行时间。目前可以使用 0/5 * * * * ? 进行测试,代表从0秒开始,每隔5秒执行一次。
注意:对应的bean要注入容器,否则不会生效。
1 2 3 4 5 6 7 8 9 10 @Component public class TestJob { @Scheduled(cron = "0/5 * * * * ?") public void testJob () { System.out.println("定时任务执行了" ); } }
Swagger2 简介
Swagger 是一套基于 OpenAPI 规范构建的开源工具,可以帮助我们设计、构建、记录以及使用 Rest API。
传统意义上的文档都是后端开发人员手动编写的,相信大家也都知道这种方式很难保证文档的及时性,这种文档久而久之也就会失去其参考意义,反而还会加大我们的沟通成本。
Swagger 给我们提供了一个全新的维护 API 文档的方式,下面我们就来了解一下它的优点:
1.代码变,文档变 。只需要少量的注解,Swagger 就可以根据代码自动生成 API 文档,很好的保证了文档的时效性。 2.跨语言性,支持 40 多种语言。 3.Swagger UI 呈现出来的是一份可交互式的 API 文档,我们可以直接在文档页面尝试 API 的调用,省去了准备复杂的调用参数的过程。
快速入门 引入依赖 1 2 3 4 5 6 7 8 9 <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger2</artifactId > </dependency > <dependency > <groupId > io.springfox</groupId > <artifactId > springfox-swagger-ui</artifactId > </dependency >
启用Swagger2 在启动类上或者配置类加 @EnableSwagger2 注解
1 2 3 4 5 6 7 8 9 @SpringBootApplication @MapperScan("com.sangeng.mapper") @EnableScheduling @EnableSwagger2 public class SanGengBlogApplication { public static void main (String[] args) { SpringApplication.run(SanGengBlogApplication.class,args); } }
测试 访问:http://localhost:7777/swagger-ui.html
注意其中localhost和7777要调整成实际项目的域名和端口号。
权限控制 见Security文章
RBAC
前端动态路由
easyExcel导出 待加功能 用户点赞,取消点赞实现,同一接口
//缓存在redis
//定时更新
3.点赞/取消点赞
1 POST /article/ likeCount/id
1 2 3 4 5 6 pathvarable形式{ "userId" : "1" , "articleId" : "1" , "like" : 1 /0 }
1 2 3 4 5 6 7 8 9 10 11 12 13 public ResponseResult likeArticle (Long articleId, Integer like) { RedisTemplate template = redisCache.redisTemplate; Long userId = SecurityUtils.getLoginUser().getUser().getId(); String key="blog:like:" +articleId; Boolean result = template.opsForSet().isMember(key, userId); if (Boolean.FALSE.equals(result)){ template.opsForSet().add(key, userId); return ResponseResult.okResult("点赞成功" ); }else { template.opsForSet().remove(key, userId); return ResponseResult.errorResult(201 ,"取消点赞成功" ); } }
通过定时任务写入数据库