系列文章目录
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
提示:这里可以添加本文要记录的大概内容:
本文旨在记录spring应用开发过程中没见过或不熟悉的知识点,学到某个知识点可能为了完全看懂源码就拓展了其他的知识点,比如从CommandLineRunner拓展了责任链模式。
提示:以下是本篇文章正文内容,下面案例可供参考
初始化相关
CommandLineRunner接口
CommandLineRunner 是 Spring Boot 中的一个接口,用于在 Spring Boot 应用程序启动时执行特定的任务或操作。它允许您在 Spring Boot 应用程序启动后,但在应用程序完全启动之前执行一些自定义的初始化操作。这对于执行一些特定的初始化逻辑、数据加载、调度任务等非常有用。
CommandLineRunner 接口定义了一个单一的方法 run(String… args),该方法在 Spring Boot 应用程序启动时被调用。可以实现这个接口,并在 run 方法中编写自定义的初始化逻辑。
实例 责任链模式初始化
AbstractChainContext
public final class AbstractChainContext<T> implements CommandLineRunner {
private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer = new HashMap<>();
/**
* 责任链组件执行
*
* @param mark 责任链组件标识
* @param requestParam 请求参数
*/
public void handler(String mark, T requestParam) {
List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(mark);
if (CollectionUtils.isEmpty(abstractChainHandlers)) {
throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
}
// 执行责任链逻辑,例如 不符合条件的,抛出异常
abstractChainHandlers.forEach(each -> each.handler(requestParam));
}
@Override
public void run(String... args) throws Exception {
// 获取所有的AbstractChainHandler实现类
Map<String, AbstractChainHandler> chainFilterMap = ApplicationContextHolder
.getBeansOfType(AbstractChainHandler.class);
// 把AbstractChainHandler实现类按照mark进行分组,添加到abstractChainHandlerContainer中
chainFilterMap.forEach((beanName, bean) -> {
List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(bean.mark());
if (CollectionUtils.isEmpty(abstractChainHandlers)) {
abstractChainHandlers = new ArrayList();
}
abstractChainHandlers.add(bean);
// 排序
List<AbstractChainHandler> actualAbstractChainHandlers = abstractChainHandlers.stream()
.sorted(Comparator.comparing(Ordered::getOrder))
.collect(Collectors.toList());
abstractChainHandlerContainer.put(bean.mark(), actualAbstractChainHandlers);
});
}
}
AbstractChainHandler
public interface AbstractChainHandler<T> extends Ordered {
/**
* 执行责任链逻辑
*
* @param requestParam 责任链执行入参
*/
void handler(T requestParam);
/**
* @return 责任链组件标识
*/
String mark();
}
TrainPurchaseTicketChainFilter
再定义这个接口是为了直接定义mark,在同一个责任链上的实现类实现同一个Filter
public interface TrainPurchaseTicketChainFilter<T extends PurchaseTicketReqDTO> extends AbstractChainHandler<PurchaseTicketReqDTO> {
@Override
default String mark() {
return TicketChainMarkEnum.TRAIN_PURCHASE_TICKET_FILTER.name();
}
}
TrainPurchaseTicketParamNotNullChainHandler
@Component
public class TrainPurchaseTicketParamNotNullChainHandler implements TrainPurchaseTicketChainFilter<PurchaseTicketReqDTO> {
@Override
public void handler(PurchaseTicketReqDTO requestParam) {
if (StrUtil.isBlank(requestParam.getTrainId())) {
throw new ClientException("列车标识不能为空");
}
if (StrUtil.isBlank(requestParam.getDeparture())) {
throw new ClientException("出发站点不能为空");
}
if (StrUtil.isBlank(requestParam.getArrival())) {
throw new ClientException("到达站点不能为空");
}
if (CollUtil.isEmpty(requestParam.getPassengers())) {
throw new ClientException("乘车人至少选择一位");
}
for (PurchaseTicketPassengerDetailDTO each : requestParam.getPassengers()) {
if (StrUtil.isBlank(each.getPassengerId())) {
throw new ClientException("乘车人不能为空");
}
if (Objects.isNull(each.getSeatType())) {
throw new ClientException("座位类型不能为空");
}
}
}
@Override
public int getOrder() {
return 0;
}
}
bean相关
作用域
如果一个singleton的bean想要在运行时,在每次注入时都能有一个新的prototype的bean生成并注入,这是不行的。因为依赖注入在初始化的时候只会注入一次。如果想要在运行时多次注入新的prototype的bean,请参考spring官方文档
application作用域的bean是每个ServletContext只有一个;而singleton作用域是每个Spring的ApplicationContext只有一个。两者的范围不同。
The request, session, application, and websocket scopes are available only if you use a web-aware Spring ApplicationContext implementation (such as XmlWebApplicationContext). If you use these scopes with regular Spring IoC containers, such as the ClassPathXmlApplicationContext, an IllegalStateException that complains about an unknown bean scope is thrown.
翻译:
仅当您使用支持 Web 的 Spring ApplicationContext 实现(例如 XmlWebApplicationContext)时,请求、会话、应用程序和 websocket 作用域才可用。如果将这些作用域与常规 Spring IoC 容器(例如 ClassPathXmlApplicationContext)一起使用,则会引发 IllegalStateException,that complains about an unknown bean scope is thrown.
WebSocket
WebSocket 是一种支持双向通信的网络协议,它允许客户端和服务器之间建立持久连接,并通过该连接实现实时通信。基于TCP连接。
Spring Boot 提供了对 WebSocket 的支持,可以方便地在应用程序中实现实时通信功能。Spring Boot 中的 WebSocket 依赖于 Spring WebFlux 模块,使用了 Reactor Netty 库来实现底层的 WebSocket 通信。
@Bean
@Bean用于类中的方法,返回一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中;而包含了@Bean方法的类需要有@Component @Repository @Controller @Service @Configration等注解
@ConditionalOnMissingBean
通常写在@Bean下方,使用 @ConditionalOnMissingBean 来定义一个默认配置,只有在没有其他满足条件的 Bean 存在时才会创建。
自动装配相关
@AutoWired
字段注入:
public class MyClass {
@Autowired
private MyDependency myDependency;
// 其他代码...
}
在这个例子中,MyDependency 类型的实例会被自动注入到 MyClass 类中的 myDependency 字段中。
构造函数注入:
public class MyClass {
private MyDependency myDependency;
@Autowired
public MyClass(MyDependency myDependency) {
this.myDependency = myDependency;
}
// 其他代码...
}
在这个例子中,通过构造函数注入的方式,MyDependency 类型的实例会在创建 MyClass 实例时被自动传递并赋值给 myDependency 字段。
方法注入:
public class MyClass {
private MyDependency myDependency;
@Autowired
public void setMyDependency(MyDependency myDependency) {
this.myDependency = myDependency;
}
// 其他代码...
}
contex相关
ApplicationContextAware 接口
ApplicationContextAware 是一个接口,通常实现这样实现它:
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext CONTEXT;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextHolder.CONTEXT = applicationContext;
}
// 编写一些静态方法来封装使用ApplicationContext
public static <T> T getBean(Class<T> clazz) {
return CONTEXT.getBean(clazz);
}
public static <A extends Annotation> A findAnnotationOnBean(String beanName, Class<A> annotationType) {
return CONTEXT.findAnnotationOnBean(beanName, annotationType);
}
// ......
尽管直接使用 ApplicationContext 也可以实现相同的功能,但通过这样的封装,可以提供更高级的、更易于使用的接口,从而改善代码的可读性和可维护性,以及提供一些自定义的功能和扩展性。
aop相关
aspectj实现aop
@Aspect写在一个类上,@Around写在类的方法上,@Around的括号里可以写一些内容,指向被@Pointcut注解标识的方法,而@Pointcut()里的内容说明了被哪些注解标识的方法或哪些包里的方法要被增强。
实例自定义注解@LoginUser实现从header获取信息放入controller方法参数
@Aspect
@Component
@Order(20)
public class AuditAspect {
private final Logger logger = LoggerFactory.getLogger(AuditAspect.class);
/**
* @Audit的Around Advice
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("cn.edu.xmu.javaee.core.aop.CommonPointCuts.auditAnnotation()")
public Object aroundAudit(JoinPoint joinPoint) throws Throwable{
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader(JwtHelper.LOGIN_TOKEN_KEY);
UserToken decryptToken = decryptToken(token);//自定义私有方法
checkDepartId(request, method, decryptToken);//自定义私有方法
Object[] args = joinPoint.getArgs();
Annotation[][] annotations = method.getParameterAnnotations();
putMethodParameter(decryptToken, args, annotations);//自定义私有方法,真正实现标题目的的代码段
Object obj = null;
try {
obj = ((ProceedingJoinPoint) joinPoint).proceed(args);
} catch (Throwable e) {
throw e;
}
return obj;
}
/**
* 检查路径上的departId是否与token里一致
* @param request
* @param method
* @param decryptToken
* @throws BusinessException
*/
private void checkDepartId(HttpServletRequest request, Method method, UserToken decryptToken) throws BusinessException{
//检验/shop的api中传入token是否和departId一致
}
/**
* 将token的值设置到jp的参数上
* @param token
* @param args
* @param annotations
*/
private void putMethodParameter(UserToken token, Object[] args, Annotation[][] annotations) {
for (int i = 0; i < annotations.length; i++) {
Annotation[] paramAnn = annotations[i];
if (paramAnn.length == 0){
continue;
}
for (Annotation annotation : paramAnn) {
//这里判断当前注解是否为LoginUser.class
if (annotation.annotationType().equals(LoginUser.class)) {
//校验该参数,验证一次退出该注解
UserDto user = new UserDto();
user.setName(token.getUserName());
user.setId(token.getUserId());
user.setUserLevel(token.getUserLevel());
user.setDepartId(token.getDepartId());
args[i] = user;
}
}
}
}
/**
* 解密token
* @param tokenString token字符串
* @return 解密的JWTHelper.Token对象
* @throws BusinessException
*/
private UserToken decryptToken(String tokenString) throws BusinessException{
}
@RestControllerAdvice
@RestControllerAdvice是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component。
注解了@RestControllerAdvice的类的方法可以使用@ExceptionHandler、@InitBinder、@ModelAttribute注解到方法上。
@RestControllerAdvice注解将作用在所有注解了@RequestMapping的控制器的方法上。
@ExceptionHandler:用于指定异常处理方法。当与@RestControllerAdvice配合使用时,用于全局处理控制器里的异常。
@InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。
@ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对
@ExceptionHandler
@ExceptionHandler({BindException.class, ValidationException.class, MethodArgumentNotValidException.class})括号里用来指明处理哪些异常类
@ResponseStatus(code = HttpStatus.BAD_REQUEST)和@ExceptionHandler一起用,表明http返回状态
其他
ApplicationListener接口
实例
通过实现ApplicationListener,防止Spring事件被多次执行
/**
* 应用初始化后置处理器,防止Spring事件被多次执行
*
* @公众号:马丁玩编程,回复:加群,添加马哥微信(备注:12306)获取项目资料
*/
@RequiredArgsConstructor
public class ApplicationContentPostProcessor implements ApplicationListener<ApplicationReadyEvent> {
private final ApplicationContext applicationContext;
/**
* 执行标识,确保Spring事件 {@link ApplicationReadyEvent} 有且执行一次
*/
private final AtomicBoolean executeOnlyOnce = new AtomicBoolean(false);
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
if (!executeOnlyOnce.compareAndSet(false, true)) {
return;
}
applicationContext.publishEvent(new ApplicationInitializingEvent(this));
}
}
public class ApplicationBaseAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ApplicationContextHolder congoApplicationContextHolder() {
return new ApplicationContextHolder();
}
@Bean
@ConditionalOnMissingBean
public ApplicationContentPostProcessor congoApplicationContentPostProcessor(ApplicationContext applicationContext) {
return new ApplicationContentPostProcessor(applicationContext);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = "framework.fastjson.safa-mode", havingValue = "true")
public FastJsonSafeMode congoFastJsonSafeMode() {
return new FastJsonSafeMode();
}
}
controller层规范
请求体参数
请求体参数是对象时,应该递归具有getter setter
vo通常成员变量名驼峰命名法,加个注解@JsonProperty(“trade_no”)下划线命名法,这样方便接收json参数。
实际为查询的请求,必须要用请求体时,应该用post还是get?一般get请求不带body。支付宝官方文档也用post。部分浏览器比如chrome是不支持get带请求体的。
响应体
当api正常返回和异常返回的响应体的字段不一样时,怎么办?
@ExceptionHandler()
默认情况下,如果 Service 层抛出异常而没有在 Service 层进行捕获和处理,异常会沿着调用链一直向上抛,直到被 Controller 层的异常处理方法捕获。这种异常处理机制允许你在 Controller 层集中处理所有的异常,为客户端提供一致的错误响应。
Spring的@ExceptionHandler可以用来统一处理方法抛出的异常。
代码示例:
public class AlipayBusinessException extends RuntimeException{
private AlipayReturnNo errno;
public AlipayBusinessException(AlipayReturnNo errno) {
super(errno.getMessage());
this.errno = errno;
}
public AlipayReturnNo getAlipayReturnNo(){
return this.errno;
}
}
// 交易已经关闭
if (existingPayment.getTradeStatus().equals(Payment.TradeStatus.TRADE_CLOSED)) {
throw new AlipayBusinessException(AlipayReturnNo.TRADE_HAS_CLOSE);
}
@ExceptionHandler(AlipayBusinessException.class)
public ResponseEntity<String> handleException(AlipayBusinessException e) {
return ResponseEntity.status(400).body(e.getAlipayReturnNo().toString());
}