多模块 SpringBoot 种子工程搭建 (二)
上一次, 将我们项目的项目结构大体建立起来, 这次我们来完善一些功能.
- LOG4J2 配置
- AOP 日志
- 通用响应包装
- Hibernate validation 以及异常处理
- 全局异常处理
- OKHTTP 配置以及拦截器配置
- JWT 实现与配置
- XSS 和 Trim 拦截器
- 登录用户注入到 IOC
- JWT 用户
- 后台管理用户
1. LOG4J2 配置
添加到父模块和子模块
<!--<editor-fold desc="LOG4J2">-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${springboot.version}</version>
<!-- exclude掉spring-boot的默认log配置 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入log4j2依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>${springboot.version}</version>
</dependency>
<!-- 加上这个才能辨认到log4j2.yml文件 -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${yaml.version}</version>
</dependency>
<!--</editor-fold>-->
config/(profile)/ 中放入 log4j2.yml
application.properties 设置 log42 配置文件路径
# log4j conf
logging.config=${spring.config.location}log4j2.yml
2. AOP 日志
framework-support 中添加ServiceLoggerAop, 因为 bizz-support 自动生成已生成 log,所以只需要 api 和 manage 包.
Aspect
@Component
public class ServiceLoggerAop {
private static final String HEAD = "##########| ";
public static final Logger logger = LoggerFactory.getLogger(ServiceLoggerAop.class);
@Pointcut("execution(* com.logictech.api.service..*(..)) || " +
"execution(* com.logictech.manage.service..*(..))")
public void servicePointCut() {
}
/**
* Advice that logs methods throwing exceptions.
*
* @param joinPoint join point for advice
* @param e exception
*/
@AfterThrowing(pointcut = "servicePointCut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
logger.error(HEAD + "发生异常 在 {}.{}() with cause = '{}' 异常为: '{}'", joinPoint.getSignature()
.getDeclaringTypeName(),
joinPoint.getSignature().getName(), e.getCause() != null ? e.getCause() : "NULL", e.getMessage(), e);
}
@Around("servicePointCut()")
public Object interceptor(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
if (logger.isDebugEnabled()) {
logger.debug(HEAD + "进入: {}.{}() 参数为: {}", proceedingJoinPoint.getSignature()
.getDeclaringTypeName(),
proceedingJoinPoint.getSignature().getName(),
proceedingJoinPoint.getArgs().length == 1 ?
JSON.toJSON(proceedingJoinPoint.getArgs()) : Arrays.toString
(proceedingJoinPoint.getArgs()));
}
try {
Object result = proceedingJoinPoint.proceed();
if (logger.isDebugEnabled()) {
logger.debug(HEAD + "完成: {}.{}() 结果为 = {}", proceedingJoinPoint.getSignature()
.getDeclaringTypeName(),
proceedingJoinPoint.getSignature().getName(), result);
}
return result;
} catch (IllegalArgumentException e) {
logger.error(HEAD + "参数出错: {} 在 {}.{}()", Arrays.toString(proceedingJoinPoint.getArgs()),
proceedingJoinPoint.getSignature().getDeclaringTypeName(), proceedingJoinPoint.getSignature().getName());
throw e;
}
}
}
3. 通用响应包装
对于返回数据的 Controller, 我们会包装成一个通用的响应
{
"code":0,
"message":"成功啦!",
"data":"This Active Profile is: [prod], Project Name: [MULTI_MODULE_SPRING_BOOT]"
}
但是对于开发来说, 如果每次都是去新建一个 Map, 然后 put,put,难免会出现低级错误
于是我们去新建一个 ResultEntity, 并添加两个常用的构造方法, 那边可以避免这样的问题
public class ResultEntity<T> implements Serializable {
private static final long serialVersionUID = -123894718371L;
/**
* 成功: 0
*/
public static final int SUCCESS = 0;
/**
* 失败: 1
*/
public static final int FAIL = 1;
/**
* 无权限: 2
*/
public static final int NO_PERMISSION = 2;
/**
* 信息
*/
private String msg = "成功";
/**
* 编码
*/
private int code = SUCCESS;
/**
* 数据
*/
private T data;
/**
* setter getter
*/
*****
此时正常的流程就已经完成, 为了更简化开发( 亦或是偷懒??? ), 添加 ResponseBodyAdvice
Allows customizing the response after the execution of an {@code @ResponseBody}
or an {@code ResponseEntity} controller method but before the body is written
with an {@code HttpMessageConverter}.
@ControllerAdvice
public class ResponseConfig implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
Type type = returnType.getGenericParameterType();
// 是否有IgnoreCommonResponse 注解
boolean isIgnore = returnType.getMethod().getAnnotation(IgnoreCommonResponse.class) != null ||
returnType.getMethod().getAnnotation(ResponseBody.class) != null ||
returnType.getClass().getAnnotation(RestController.class) != null;
// 不进行包装的
boolean noAware =
ResultEntity.class.equals(type) || String.class.equals(type) || PageInfo.class.equals(type) || isIgnore;
return !noAware;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
ResultEntity result = new ResultEntity(body);
return result;
}
}
其中重写 support 方法, 去判断哪些需要包装哪些不需要, 这里就可以可以按照自己的需求进行梳理, 例如我觉得个别不需要进行包装, 所以我添加了个 @IgnoreCommonResponse
注解, 添加上了这个便不进行包装.
由于我们返回的类型已经是那样结构的, 所以要么直接返回 message, 要么添加 @IgnoreCommonResponse
注解:
$ curl http://localhost:8766/api/index\?_\=abc
{
"msg":"成功",
"code":0,
"data":{
"message":"成功啦!",
"data":"This Active Profile is: [prod], Project Name: [MULTI_MODULE_SPRING_BOOT]",
"code":0
}
}%
4. Hibernate validation 以及异常处理 (水,详情见代码...)
Hibernate validation 是个非常好用的插件, 很完美的后台验证, 但是一般情况下没要求就懒得写了, 这边就作为一个预备在这.
添加依赖:
<!-- validate hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
添加错误信息Bean: FieldError
public class FieldError implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 字段
*/
private String name;
/**
* 消息
*/
private String message;
添加异常类: ParamValidException
略略略略略略略略略略略略略略略略略略略略略略略略略略略略...
AOP 添加异常捕获
略略略略略略略略略略略略略略略略略略略略略略略略略略略略...
测试:
@GetMapping({"/", "index", "index.html"})
public String adminIndex(@NotBlank String name) throws Exception {
return "This Active Profile is: [" + env + "], Project Name: [" + PROJECT_NAME + "]";
}
因为这边没有加全局异常捕捉
所以返回的结果还是成功, 暂时放一下, 但是我们的验证已经对了,然后试试正确的
$ curl http://localhost:8766/api/index
{"msg":"成功","code":0,"data":{"timestamp":1530774243574,"status":500,"error":"Internal Server Error","message":"[name:不能为空]","path":"/api/index"}}
$ curl http://localhost:8766/api/index\?name\=hahah
{"msg":"成功","code":0,"data":true}
5. 全局异常处理
@ExceptionHandler 了解一下
通用的异常处理
return new ResultEntity("发生未知错误, 请联系管理员", 1, new HashMap() {{
put("ex", ex.getClass().getName());
put("orgEXMessage", ex.getMessage());
put("clazz", stackTraceElements.length == 0 ? null : stackTraceElements[0].getClassName());
put("method", stackTraceElements.length == 0 ? null : stackTraceElements[0].getMethodName());
put("lineNumber", stackTraceElements.length == 0 ? null : stackTraceElements[0].getLineNumber());
}});
/**
* Hibernate-Validator 异常处理
* ExceptionHandler配置异常处理
* ResponseStatus 配置响应码
* @param ex
* @return
*/
@ExceptionHandler(ParamValidException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ResultEntity paramValidExceptionHandler(ParamValidException ex) {
ResultEntity result = new ResultEntity(ex);
result.setData(ex.getFieldErrors());
return result;
}
6. OKHTTP 配置以及拦截器配置
这个也是项目中用的比较顺手的一个 Http Client 工具, 自带配套的拦截器功能, 可以共同化请求日志
添加依赖:
<!--okhttp3-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>${okhttp.version}</version>
</dependency>
新增OkHttpLoggerInterceptor 继承 okhttp3.Interceptor, 可通过重写 intercept 方法获得 request 和 response
Request request = chain.request();
RequestBody requestBody = request.body();
新建OkHttpSender, 封装通用的方法
测试
public String getMessage(String message) throws IOException {
OkHttpSender.post("http://localhost:8766/api/index", "asd", OkHttpSender.JSON);
return message;
}
╔═════════════════════════════════
║ 请求方式: POST 请求地址: http://localhost:8766/api/index
║ 请求时间: 2018-07-05 18:19:44
║ Content-Type: application/json;charset=utf-8
║ 请求参数: asd
║ 响应编码: 400 响应消息:
║ 请求地址: http://localhost:8766/api/index
║ 响应体: {"code":1,"data":[{"name":"name","message":"不能为空"}],"msg":"[name:不能为空]"}
╚═════════════════════════════════
7. JWT 实现与配置
添加依赖
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
/**
* 生成 token
* @param map 需要放入 body 的数据
* @return
* @throws UnsupportedEncodingException
*/
public static String createToken(Map<String, Object> map) throws UnsupportedEncodingException {
// jwt 生成
String compactJws = Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRES_SECOND))
.signWith(SignatureAlgorithm.HS512, BASE64_SECRET).compact();
return compactJws;
}
/**
* 获得 body 的数据, 抛出异常则 token 无效
* @param jwtStr token 字符串
* @return
*/
public static Map<String, Object> getBodyByToken(String jwtStr) throws UnknownTokenException {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(BASE64_SECRET))
.parseClaimsJws(jwtStr).getBody();
} catch (ExpiredJwtException e) {
throw new UnknownTokenException("用户登录已过期, 请重新登录!");
}
return claims;
}
8. XSS 和 Trim 拦截器
9. 登录用户注入到 IOC
9.1 JWT 用户
思路: 由注解去区分哪些需要 JWT 认证的 API 方法, 验证完 JWT 后,将 body 中的值设置到 IOC 作用域为 Request 的 Bean
新建 注解 @Authorization
检查用户是否登录,未登录返回401错误
/**
* 在Controller的方法上使用此注解,该方法在映射时会检查用户是否登录,未登录返回401错误
*
* @author ScienJus
* @date 2015/7/31.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorization {
}
添加令牌拦截器 TokenInterceptor
public class TokenInterceptor extends HandlerInterceptorAdapter {
public static final Logger logger = LoggerFactory.getLogger(TokenInterceptor.class);
@Resource
protected TokenInterceptorService tokenInterceptorService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if (method.getAnnotation(Authorization.class) == null) {
return true;
}
// 从header中得到token
String authorization = request.getHeader("x-lt-token");
logger.debug("拦截器到的token为: [{}]", authorization);
if ("".equals(authorization) || authorization == null) {
throw new UnknownTokenException();
}
// 验证 token
boolean isTokenPassed = tokenInterceptorService.checkToken(authorization);
// 如果验证token失败,并且方法注明了Authorization,返回401错误
if (!isTokenPassed) {
logger.debug("验证token失败");
throw new UnknownTokenException();
}
tokenInterceptorService.setTokenUser(authorization);
return true;
}
}
其中 TokenInterceptorService 是注入到 IOC 的服务, 具体到 api 模块去实现, 否则会报错.
9.2 后台管理用户
2019-04-04 继续写完这个系列, 思路有点短路了, 可能不太流畅, 也把第一次的东西写写完整吧
思路: 登录成功后,将登录后的用户设置到 IOC 作用域为 session 的 Bean
AdminUserSession.java
import com.logictech.bizz.entity.AdminUserInfo;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import java.io.Serializable;
/**
* @author JG.Hannibal
* @since 2018/7/9 14:57
*
* 这里注意@Scope注解:
* @Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE)这个是说在每次注入的时候回自动创建一个新的bean实例
* @Scope(value=ConfigurableBeanFactory.SCOPE_SINGLETON)单例模式,在整个应用中只能创建一个实例
* @Scope(value=WebApplicationContext.SCOPE_GLOBAL_SESSION)全局session中的一般不常用
* @Scope(value=WebApplicationContext.SCOPE_APPLICATION)在一个web应用中只创建一个实例
* @Scope(value=WebApplicationContext.SCOPE_REQUEST)在一个请求中创建一个实例
* @Scope(value=WebApplicationContext.SCOPE_SESSION)每次创建一个会话中创建一个实例
* 里面还有个属性
* proxyMode=ScopedProxyMode.INTERFACES创建一个JDK代理模式
* proxyMode=ScopedProxyMode.TARGET_CLASS基于类的代理模式
* proxyMode=ScopedProxyMode.NO(默认)不进行代理
作者:LvFang
链接:https://www.jianshu.com/p/5f06e18ed520
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
*/
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AdminUserSession extends AdminUserInfo implements Serializable {
private static final long serialVersionUID = 9120765714832970813L;
}
AdminUserController.java
@Resource
private AdminUserSession adminUserSession;
@PostMapping({"/login"})
public String login(AdminUserInfo adminUserInfo, ModelMap mode) throws Exception {
// 验证登录
List<AdminUserInfo> adminUserInfos = adminUserInfoService.allData(params);
if (adminUserInfos.size() != 1) {
mode.addAttribute("error", "* 账号或密码错误");
return "login";
} else if (adminUserInfos.size() == 1){
// 注册到 Session IOC BeanUtils.copyProperties(adminUserInfos.get(0), adminUserSession);
return "redirect:/user/check";
}
return "login";
}
LoginInterceptor.java
该处不做多述, 拦截 AdminUserSession 是否有值, 否则没有登录
9.3 这样的好处
在使用时, 会有检察字段 createTime,creatUser,updateTime,updateUser,
如果使用注入到 IOC 容器, 那么我们进行更新creatUser,updateUser就会变得相对优雅,类似以下代码
@Resource
private AdminUserSession adminUserSession;
@ResponseBody
@PostMapping({"/add"})
public int add(@RequestBody AdminUserInfo adminUserInfo) throws Exception {
adminUserInfo.withCreateTime(new Date())
.withCreateUser(adminUserSession.getName())
.withUpdateTime(new Date())
.withUpdateUser(adminUserSession.getName());
return adminUserInfoService.insert(adminUserInfo);
}
对于 API 模块也是如此, 这样对于管理侧和 API 侧的开发体验保持一致