Spring 源码(十):Spring AOP 核心 API

栏目: IT技术 · 发布时间: 4年前

概述

Spring 的两大核心: IoCAOPIoC 作为 Spring 的根基,通过大量的扩展点让系统轻而易举的就可以实现良好的扩展性,而 AOPIoC 结合在一起,类似于发生强大化学反应一样,将 Spring 的功能性又提高了一个层次。 Spring 中也有大量使用 AOP 场景,比如 @Configuration 、数据库事务、 mybatis mapper 接口注入等等。

AOP 全称 Aspect Oriented Programming ,即面向切面编程,其并非 Spring 独有,作为一种对 OOP 编程思想的补充,其也有自己的标准规范并有独立的组织进行维护。

根据织入时机的不同, AOP 又可以分为三类:

  • 编译时织入: ApectJ 主要采用的就是编译时织入方式,这种一般使用特定的编译器方式实现;
  • 类加载时织入:这种一般都是依赖 JVM Instruments 技术实现,Spring中也有对这种技术支持,具体可以了解下 LoadTimeWeaver
  • AOP
    Spring
    AOP
    JDK动态代理
    CGLIB动态代理
    

AOP 标准规范是由独立的组织机构进行维护,其涉及到的核心概念主要如下:

  • JoinPoint
    AOP
    AOP
    
  • Pointcut
    Spring
    Pointcut
    
  • 通知( Advice ):在连接点处需要织入的增强代码逻辑封装;
  • Aspect
    Advice
    Pointcut
    Spring
    Advisor
    
  • 织入( Weaving ):织入是在适当的位置将切面插入到应用程序代码中的过程,就是上面说的编译时织入、类加载时织入和动态织入;
  • 目标对象( target ): AOP 代理增强的原生对象;

基础API

Spring AOP 很多人不能很好的理解、使用,一方面是因为 AOP 涉及的概念可能比较抽象,不容易理解;另外一方面你对 Spring AOP 涉及到的一些基础 API 不熟悉。下面我们就对 Spring AOP 中最核心的一些 API ,由底向上,由基础到高级方式一步步分析。

Enhancer

Spring AOP 主要使用的是动态代理方式实现,动态代理实现主要包括两种: jdk动态代理cglib动态代理jdk动态代理 方式比较熟悉,下面就来看下 cglib动态代理 如何实现。

Spring 中提供了一个 工具 类: EnhancerSpring 中主要就是利用该工具类创建 cglib动态代理 。下面我们通过一个案例看下其基本使用:

1、创建 Callback 回调接口类,该接口中就可以实现增强逻辑:

public class MyMethodInterceptor implements MethodInterceptor {

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable
{
try {
before(method);//前置通知
Object ret = methodProxy.invokeSuper(obj, args);//目标方法执行
after(method, ret);//后置通知
return ret;
} catch (Exception e) {
exception();//异常通知
} finally {
afterReturning();//方法返回通知
}
return null;
}
//前置增强
private void before(Method method) {
System.out.printf("before execute:%s\r\n", method.getName());
}
//后置增强
private void after(Method method, Object ret) {
System.out.printf("after execute:%s, ret:%s\r\n", method.getName(), ret);
}
//异常增强
private void exception() {
System.out.println("execute failure");
}
//after返回增强
private void afterReturning() {
System.out.println("execute finish");
}

}

2、编写测试:

//NoOp.INSTANCE:NoOp回调把对方法调用直接委派给这个方法在父类中的实现,即可理解为真实对象直接调用方法,没有任何增强
private static final Callback[] CALLBACKS = new Callback[] {
new MyMethodInterceptor(),
NoOp.INSTANCE
};

public void test() {
//创建Enhancer实例
Enhancer enhancer = new Enhancer();

//cglib是基于继承方式代理,superClass就是基于哪个类型父类进行增强,创建出来的对象就是该类型子类
enhancer.setSuperclass(UserServiceImpl.class);

//CallbackFilter主要用于过滤不同Method使用不同的Callback
enhancer.setCallbackFilter(new CallbackFilter() {
@Override
public int accept(Method method) {
if (method.getDeclaringClass() == Object.class) {
return 1;//使用Callback数组下标是1的
}
return 0;//使用Callback数组下标是0的
}
});
//设置Callback数组,Callback就是封装的增强逻辑
enhancer.setCallbacks(CALLBACKS);
//创建代理对象
UserService proxyObj = (UserService) enhancer.create();
System.out.println(proxyObj.say("zhangsan"));
}

通过上面 enhancer.create() 这条语句,就可以为目标类创建一个 cglib动态代理 ,通过 Callback 回调方式将各种增强逻辑织入到代理实例中。

还可以使用 Enhancer.createClass() 方法只创建出代理类型,然后自己通过反射方式创建对象。这时,需要注意:

1、这时就不能使用 setCallbacks() 设置 Callback 数组,而是使用 setCallbackTypes() 设置 Callback 对应的类型;

2、 Enhancer.createClass() 执行完成后,再通过 Enhancer.registerStaticCallbacks(clz, CALLBACKS) 方式设置 Callback 数组;

enhancer.setInterfaces() 可用于设置生成的代理类必须实现的接口,比如你可以不设置 superclass ,只设置 interfaces ,这时也是可以正常创建出基于这个接口的动态代理实例,但是这时就要注意不能触发目标对象方法执行,如 methodProxy.invokeSuper 执行会报错,如下:

public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable
{
try {
before(method);//前置通知
//Object ret = methodProxy.invokeSuper(obj, args);//目标方法执行
after(method, ret);//后置通知
return ret;
} catch (Exception e) {
exception();//异常通知
} finally {
afterReturning();//方法返回通知
}
return null;
}

基于接口创建的代理实例还是非常有用的,比如 mybatis mapper 就是一个没有实现类的接口,但是在 spring 中却可以依赖注入到 service bean 中,其中就是利用到上面基于接口创建动态代理的思想,注入进来的其实就是基于接口的动态代理,然后调用接口中方法时就可以进行拦截,获取到具体调用方法签名信息以及参数信息,基于这些数据进行业务逻辑处理。

invoke和invokeSuper方法区别

Callback#intercept() 回调方法中执行 methodProxy.invokeSuper()methodProxy.invoke() 是有很大区别的,而且看不到执行流程,所以这里涉及的逻辑非常绕。

public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable
{
Object ret = methodProxy.invokeSuper(obj, args);
Object ret = methodProxy.invoke(delete, args);
}

大致区别如下图:

Spring 源码(十):Spring AOP 核心 API

客户端触发代理对象 say 方法调用,首先进入代理对象中的同名方法,然后进入方法拦截对象 MethodInterceptor ,这里会出现两种情况:

  • invokeSuper
    super
    super.say()
    
  • invoke
    target
    target.say
    

invokeSuper()invoke() 方法都可以调用到目标对象方法,但是它们之间存在的一个本质区别:上下文环境不一样;或者更直接说:目标对象中 this 指向不一样。通过 super.say() 方式调用的目标对象, this 指向的是代理对象;而通过 target.say() 方式调用的,目标对象中 this 指向的就是目标对象本身。这会导致什么差异呢?

假如目标对象类型如下定义,然后使用 Enhancer 创建一个代理对象:

public class Target {

public void a() {
System.out.println(" a 方法");
b();
}

public void b() {
System.out.println(" b 方法");
}
}

客户端触发代理对象 a() 方法执行,如果拦截器中使用 invoke 方式调用目标对象:直接调用目标对象 a() 方法,这个方法中又会通过 this.b() 调用 方法b ,由于是目标对象本身内部调用,所以 b() 方法不会被拦截的。

客户端触发代理对象 a() 方法执行,如果拦截器中使用 invokeSuper() 方式调用目标对象:这里是通过 super.a() 方式调用目标对象中的 a() 方法,然后 a() 方法又会通过 this.b() 调用 方法b ,注意这时的 this 不是目标对象本身,而是代理对象,因为代理对象继承目标对象,代理对象会有重名方法覆写了目标对象方法。所以, this.b() 实际上会触发代理对象中 方法b 的执行,这时是会触发拦截器的。

所以, methodProxy.invokeSuper(obj, args) 这个 obj 是代理对象;而 methodProxy.invoke(obj, args) 这个入参 obj 是目标对象。搞清楚这些基本理解清楚应该使用 invoke 还是 invokeSuper

ProxyFactory

Enhancer 只能用于创建 cglib动态代理Spring 中还有一个更上层点的类 ProxyFactory ,可以用于创建 JDK动态代理cglib动态代理

public void test02() {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(new UserServiceImpl());
/*
调用ProxyFactory.addInterface()或setInterfaces()表示对接口进行代理,一般会使用jdk动态代理,
除非setOptimize(true)或setProxyTargetClass(true)表示使用cglib代理
cglib代理类名称格式大致为:ServiceImpl$$EnhancerBySpringCGLIB$$f2952b94
*/

//setInterfaces设置这个,会基于接口代理,使用jdk动态代理方式
proxyFactory.setInterfaces(UserService.class);
//proxyFactory.setOptimize(true);
//proxyTargetClass=true:直接代理目标类,而不是接口,使用cglib
proxyFactory.setProxyTargetClass(true);

// 添加增强
proxyFactory.addAdvice(new MyBeforeAdvice02());

//内部使用了缓存,target不变时,getProxy()获取到的都是同一个
//只有target变化时,才会重新创建新的代理对象
Object proxy = proxyFactory.getProxy();
}

ProxyFactory 类是 AOP 底层实现中非常重要的一个类,另外 AnnotationAwareAspectJAutoProxyCreatorBeanNameAutoProxyCreatorDefaultAdvisorAutoProxyCreator 等一些高级 AOP 实现工具类都是通过在其父类 AbstractAutoProxyCreator 中借助 ProxyFactory 实现 AOP 逻辑织入的。所以,理解 ProxyFactory 的使用对理解 Spring AOP 至关重要。

ProxyFactory 类控制代理的创建过程,其内部委托给 DefaultAopProxyFactory 的一个实例,该实例又转而委托给 Cglib2AopProxyJdkDynamicAopProxy ,用于创建基于 cglib 代理还是 jdk 代理,想了解这两种动态代理区别可以分析下这个类源码。

ProxyFactoryaddAdvice() 方法将传入的通知封装到 DefaultPointcutAdvisor ( DefaultPointcutAdvisorPointcutAdvisor 的标准实现)的一个实例中,并使用默认包含所有方法的切入点对其进行配置。为更加灵活细粒度的控制在哪些连接点上拦截通知,可以使用 addAdVisor() 方法添加一个带有切入点消息的 Advisor

可以使用相同的 ProxyFactory 实例来创建多个代理,每个代理都有不同的切面。为了帮助实现该过程, ProxyFactory 提供了 removeAdvice()removeAdvisor() 方法,这些方法允许从 ProxyFactory 中删除之前传入的任何通知或切面,同时可以使用 boolean adviceIncluded(@Nullable Advice advice) 检查 ProxyFactory 是否附有特定的通知对象。

Advice

ProxyFactoryaddAdvice()addAdvisor() 两个方法分别引入了两个重要的类: AdviceAdvisor 。首先,我们来看下 Advice 这个接口类,其可以看成需要织入增强的代码逻辑封装。 AdviceSpringAPI 结构如下:

Spring 源码(十):Spring AOP 核心 API

大致描述:

  • BeforeAdvice
    MethodBeforeAdvice
    @Before
    
  • AfterAdvice
    AfterReturningAdvice
    ThrowsAdvice
    @AfterReturning
    @AfterThrowing
    
  • MethodInterceptor :可以实现环绕通知,对应注解 @Around

Advisor

AOP 规范中有切面概念,在 Spring 中大概对应就是 AdvisorAdvisor 有两个子接口: PointcutAdvisorIntroductionAdvisor

Spring 源码(十):Spring AOP 核心 API

其实真正使用比较多的是它的子类 PointcutAdvisor ,该接口关键就是如下两个方法:

public interface PointcutAdvisor {
Advice getAdvice();
Pointcut getPointcut();
}

PointcutAdvisor 从接口定义大概就可以看出,其就是对 AdvicePointcut 的封装, Advice 代表的是横切面需要织入的代码,而 Pointcut 定义了如何去切的问题。从之前分析来看, Advice 也可以看出一种非常简单的切面,是对指定的类所有方法都进行切入,横切面太宽泛,灵活性不够, PointAdvisor 引入了 Pointcut 后显然比 Advice 更加灵活、强大。

PointcutAdvisor 主要有6个具体的实现类,分别介绍如下:

  • DefaultPointcutAdvisor
    Pointcut
    Advice
    
  • NameMatchMethodPointcutAdvisor :通过该类可以定义按方法名定义切点的切面;
  • RegexpMethodPointcutAdvisor :使用正则表达式模式定义切点,其内部通过 JdkRegexpMethodPointcut 构造出正则表达式方法名切点;
  • StaticMethodMatcherPointcutAdvisor :静态方法匹配器切点定义的切面,默认情况下,匹配所有的目标类;
  • AspecJExpressionPointcutAdvisor :用于 Aspecj 切点表达式定义切点的切面;
  • AspecJPointcutAdvisor :用于 AspecJ 语法定义切点的切面;

其实,这些 Advisor 主要区别还是基于其内部封装的 Pointcut 实现类体现的,在实际工作中这些类使用的可能不多,这里的核心在于 Pointcut 如何定义切入点,所以实际开发中更多的可能会去定制 Pointcut 实现类,然后使用 DefaultPointcutAdvisor 将其包装成 Advisor 使用。

下面通过 RegexpMethodPointcutAdvisor 案例简单了解即可:

public void test1(){
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext("aop.demo03");

UserServiceImpl target = new UserServiceImpl();
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(target);
proxyFactoryBean.setProxyTargetClass(true);

RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor();
//设置advisor的advice
advisor.setAdvice(new MyBeforeAdvice02());
//设置advisor的pointcut,aop.demo03包下所有类中已say开头的方法才会织入
advisor.setPattern("aop.demo03..*.say*");
proxyFactoryBean.addAdvisor(advisor);

proxyFactoryBean.setBeanFactory(context);

Object obj = proxyFactoryBean.getObject();

System.out.println(obj.getClass().getName());
UserServiceImpl userService = (UserServiceImpl) obj;
System.out.println(userService.say("haha"));
}

RegexpMethodPointcutAdvisor 表示通过正则表达式进行切点描述的切面,它有一个 pattern 属性用来指定增强要应用到哪些类的哪些方法,也可以通过 patterns 属性指定多个表达式进行匹配。有一个 advice 属性用来表示要应用的增强,这样就能表示一个完整的切面了。

Pointcut

Advisor 引入了一个核心接口 Pointcut ,其描述了对哪些类的哪些方法进行切入。 Pointcut 从其定义可以看出其由 ClassFilterMethodMatcher 构成。 ClassFilter 用于定位哪些类可以进行切入,然后再通过 MethodMatcher 定位类上的哪些方法可以进行切入,这样 Pointcut 就拥有了识别哪些类的哪些方法能被进行切入的能力。

public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}

Pointcut 接口 API 结构见下:

Spring 源码(十):Spring AOP 核心 API

其中这里比较常用的 AnnotationMatchingPointcut 基于注解进行切入,之前分析【Spring源码】- 09 扩展点之@Import注解一节就使用到该实现类,将标记有 @MyAsync 注解的方法都进行增强就是利用这个实现类:

AnnotationMatchingPointcut pointcut = AnnotationMatchingPointcut.forMethodAnnotation(MyAsync.class);
Advice advice = new AsyncAnnotationAdvice(executor);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(pointcut);
advisor.setAdvice(advice);

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(bean);
if(!this.isProxyTargetClass()){
proxyFactory.setInterfaces(bean.getClass().getInterfaces());
}

proxyFactory.addAdvisor(advisor);
proxyFactory.copyFrom(this);
return proxyFactory.getProxy();

ProxyFactoryBean

ProxyFactoryBeanProxyFactory 功能和使用其实差不多,底层逻辑也基本一致, ProxyFactoryBean 主要是融合了 IOC 功能。一方面 ProxyFactoryBean 类是 FactoryBean 的一个实现,更加方便注入 IoC 中,参照 mybatisspring 整合一节,其中关键一步就是将扫描的 BeanDefinitionbeanClass 由接口类替换成 FactoryBean 类型;另一点就是切面可以使用 IoC 容器 bean

下面通过一个案例简单看下 ProxyFactoryBean 使用:

package org.simon.ioc.demo1;

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class MyBeforeAdvice implements MethodBeforeAdvice {
public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
System.out.println("-----洗手-----");
}
}
@Test
public void test1(){
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext("org.simon.ioc.demo1");

UserService target = new UserServiceImpl();
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(target);
proxyFactoryBean.setInterceptorNames("myBeforeAdvice", "otherAdvice*");
proxyFactoryBean.setBeanFactory(context);

Object obj = proxyFactoryBean.getObject();
System.out.println(obj.getClass().getName());
UserService userService = (UserService) obj;
System.out.println(userService.say("haha"));
}

其中关键一步在: proxyFactoryBean.setInterceptorNames("myBeforeAdvice", "otherAdvice*"); ,可以使用 IoC 中的 beanName ,同时还支持 通配符*IoC 中查找对应 Bean

高级API

前面介绍的类、接口等都是 Spring AOP 中一些底层 API ,使用起来不太方便,感觉功能不太强大,不论是 ProxyFactory 还是 ProxyFactoryBean 创建织入切面的代理,每次只能硬编码一个具体的 Bean ,假如我想将某个包路径下符合一定规则的类的特定方法都进行织入代理怎么办?

使用前面那些 API 好像都不能实现这个需求,但是结合之前分析的 Spring 扩展点,很容易想到可以结合 BeanPostProcessor 扩展点实现这个需求, postProcessAfterInitialization() 这个方法回调时 Bean 依赖注入、初始化等都已经完成,这时就可以在这个方法中过滤出符合一定条件的 Bean 进行代理增强处理。

其实,在 Spring 中基于这种思想,已经为我们提供了三个实现类:

  • BeanNameAutoProxyCreator
    beanName
    setBeanNames(String... beanNames)
    beanName
    
  • DefaultAdvisorAutoProxyCreator
    Advisor
    Advisor
    Bean
    
  • AnnotationAwareAspectjAutoProxyCreator
    Bean
    AspectJ
    @Aspect
    @Before
    @Around
    AspectJ
    

下面我们就来通过 DefaultAdvisorAutoProxyCreator 了解下使用场景:

1、定义一个目标类,后续就是基于该类进行增强:

@Component
public class UserServiceImpl{

public String say(String name){
System.out.println("执行:==UserService#say===");
return "hello,"+name;
}

}

2、定义一个配置类:

@Configuration
@ComponentScan(basePackageClasses = AopConfig.class)
public class AopConfig {

@Bean
public RegexpMethodPointcutAdvisor regexpMethodPointcutAdvisor(){
RegexpMethodPointcutAdvisor advisor = new RegexpMethodPointcutAdvisor();
advisor.setAdvice(new MyBeforeAdvice02());
//aop.demo03包下所有类中带有say开头方法进行增强
advisor.setPattern("aop.demo03..*.say*");
return advisor;
}

@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
return new DefaultAdvisorAutoProxyCreator();
}
}

这个类有两个关键,一个是向 IoC 中注入 Advisor ,之前分析过 Advisor 包含两个功能:

  • 通过 Pointcut 定位哪些类的哪些方法需求切入;
  • 通过关联的 Advice 指定切入增强逻辑;

另一个关键就是注入 DefaultAdvisorAutoProxyCreator ,这个就是一个 Spring 内置的实现 BeanPostProcessor 扩展类,其在 postProcessAfterInitialization() 方法中对 Bean 进行切入增强。

3、测试:

public void test1(){
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AopConfig.class);
System.out.println(context.getBean(UserServiceImpl.class).getClass());
}

context.getBean(UserServiceImpl.class)IoC 容器中获取的就是使用 cglib 代理后的实例。

下面我们再来分析下 AnnotationAwareAspectjAutoProxyCreator ,平时如果项目中需要开启 AOP 功能,使用 @EnableAspectJAutoProxy 注解方式开启,我们来看下该注解干了什么?

1、 @EnableAspectJAutoProxy 注解使用 @Import 注解将 AspectJAutoProxyRegistrar 引入:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
boolean proxyTargetClass() default false;
boolean exposeProxy() default false;
}

2、 AspectJAutoProxyRegistrarImportBeanDefinitionRegistrar 接口实现类:

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)
{

//注册一个SmartInstantiationAwareBeanPostProcessor类型的实现类:AnnotationAwareAspectJAutoProxyCreator
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

AnnotationAttributes enableAspectJAutoProxy =
AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
if (enableAspectJAutoProxy != null) {
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}

}

其中最关键一句 AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); ,就是向IoC容器中注入 AnnotationAwareAspectJAutoProxyCreator

这样我们就明白了 @EnableAspectJAutoProxy 注解方式开启 AOP 的本质就像向 IoC 中注入 AnnotationAwareAspectJAutoProxyCreator ,它利用 BeanPostProcessor 扩展点功能实现织入增强逻辑。

总结

首先,对 Spring AOP 底层一些最基础、最核心的 API 的分析梳理,相信你会对 Spring AOP 底层实现逻辑有了一个更加深入的理解。然后通过 Spring AOP 提供的高级 API ,理解了如何将 IoCAOP 集成到一起实现强大功能,对 SpringAOP 的整体实现思路也有了比较清晰的认识。

             长按识别关注, 持续输出原创

Spring 源码(十):Spring AOP 核心 API


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Tomcat与Java Web开发技术详解

Tomcat与Java Web开发技术详解

孙卫琴 / 电子工业出版社 / 2004-4-1 / 45.00元

《Tomcat与Java Web开发技术详解》编辑推荐:Jakarta Tomcat服务器是在SUN公司的JSWDK(JavaServer Web DevelopmentKit,SUN公司推出的小型Servlet/JSP调试工具)的基础上发展起来的一个优秀的Java Web应用容器,它是Apache-Jakarta的一个子项目。Tomcat被JavaWorld杂志的编辑选为2001年度最具创新的J......一起来看看 《Tomcat与Java Web开发技术详解》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具