文档章节

Spring AOP 深入剖析

llahn_v
 llahn_v
发布于 2017/01/13 15:40
字数 3656
阅读 14
收藏 0

        AOP是Spring提供的关键特性之一。AOP即面向切面变成,是OOP变成的有效补充。使用AOP技术,可以将一些关系特性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑--比如:权限管理、事物管理、日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统。从而达到了将不同额关注点分离出来的效果。

本文深入剖析Spring的AOP原理

  1. AOP相关的概念
    • Aspect:切面,切入系统的切面。比如事务管理是一个切面,权限管理是一个切面,日志记录是一个切面;
    • Join point:连接点,也就是可以进行横向切入的位置;
    • Advice:通知,切面在某个连接点执行的操作(分:Before advice、After returning advice、After throwing advice、After【finally】 advice、Around advice);
    • Pointcut:切点,符合切点表达式的连接点,也就是真正被切入的地方;
  2. AOP的实现原理

        AOP分为静态AOP和动态AOP。静态AOP是指AspectJ实现的AOP,它是将切面代码直接编译到Java类文件中。动态AOP是指将切面代码进行动态织入实现的AOP。Spring的AOP为动态AOP,实现的技术为:JDK提供的动态代理技术CGLIB(动态字节码增强技术)。尽管实现技术不一样,但是都是基于代理模式,都是生成一个代理对象。

            2.1    JDK动态代理

        主要使用到 InvocationHandler 接口 和 Proxy.newProxyInstance() 方法。JDK动态代理要求被代理实现一个接口,只有接口中的方法才能被代理。其方法是将被代理对象注入到一个中间对象,而中间对象实现 InvocationHandler 接口,在实现该接口时,可以在被代理对象调用它的方法时,在调用的前后插入一些代码,而 Proxy.newProxyInstance() 能够利用中间对象来生产代理对象。插入的代码就是切面代码。所以使用JDK动态代理可以实现AOP。

例子:

被代理对象实现的接口,只有接口中的方法才能够被代理

public interface UserService {
    public void addUser(User user);
    public User getUser(int id);
}

被代理对象

public class UserServiceImpl implements UserService {
    public void addUser(User user) {
        System.out.println("add user into database.");
    }
    public User getUser(int id) {
        User user = new User();
        user.setId(id);
        System.out.println("getUser from database.");
        return user;
    }
}

代理中间类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class ProxyUtil implements InvocationHandler {
    private Object target;    // 被代理的对象
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("do sth before....");
        Object result =  method.invoke(target, args);
        System.out.println("do sth after....");
        return result;
    }
    ProxyUtil(Object target){
        this.target = target;
    }
    public Object getTarget() {
        return target;
    }
    public void setTarget(Object target) {
        this.target = target;
    }
}

测试

import java.lang.reflect.Proxy;
import net.aazj.pojo.User;
public class ProxyTest {
    public static void main(String[] args){
        Object proxyedObject = new UserServiceImpl();    // 被代理的对象
        ProxyUtil proxyUtils = new ProxyUtil(proxyedObject);
        
        // 生成代理对象,对被代理对象的这些接口进行代理:UserServiceImpl.class.getInterfaces()
        UserService proxyObject = (UserService) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 
                    UserServiceImpl.class.getInterfaces(), proxyUtils);
        proxyObject.getUser(1);
        proxyObject.addUser(new User());
    }
}

执行结果

do sth before....
getUser from database.
do sth after....
do sth before....
add user into database.
do sth after....

        我们看到在UserService接口中的方法 addUser 和 getUser 方法的前面插入了我们自己的代码。这就是JDK动态代理实现AOP的原理。

        我们看到该方式有一个要求,被代理的对象必须实现接口,而且只有接口中的方法才能被代理。

            2.2    CGLIB( code generate libary)

        字节码生成技术实现AOP,起始就是继承被代理对象,然后Override需要被代理的方法,在覆盖该方法时,自然是可以插入我们自己的代码。因为需要Override被代理对象的方法,所以自然CGLIB技术实现AOP时,就必须要求需要被代理的方法不能是final方法,因为final方法不能被子类覆盖。

例子:

package net.aazj.aop;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CGProxy implements MethodInterceptor{
    private Object target;    // 被代理对象
    public CGProxy(Object target){
        this.target = target;
    }
    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy proxy) throws Throwable {
        System.out.println("do sth before....");
        Object result = proxy.invokeSuper(arg0, arg2);
        System.out.println("do sth after....");
        return result;
    }
    public Object getProxyObject() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());    // 设置父类
        // 设置回调
        enhancer.setCallback(this);    // 在调用父类方法时,回调 this.intercept()
        // 创建代理对象
        return enhancer.create();
    }
}
public class CGProxyTest {
    public static void main(String[] args){
        Object proxyedObject = new UserServiceImpl();    // 被代理的对象
        CGProxy cgProxy = new CGProxy(proxyedObject);
        UserService proxyObject = (UserService) cgProxy.getProxyObject();
        proxyObject.getUser(1);
        proxyObject.addUser(new User());
    }
}

输出结果

do sth before....
getUser from database.
do sth after....
do sth before....
add user into database.
do sth after....

        我们看到达到了同样的效果。它的原理是生成一个父类 enhancer.setSuperclass(this.target.getClass())的子类 enhancer.create(),然后对父类的方法进行拦截 enhancer.setCallback(this),对父类的方法进行覆盖,所以父类方法不能是final的。

            2.3    Spring实现AOP的相关源码

@SuppressWarnings("serial")
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                        "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface()) {
                return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }

从以上代码中我们可以看到:

if (targetClass.isInterface()) {
    return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);

        如果被代理对象实现了接口,那么就使用JDK的动态代理技术,反之则使用CGLIB来实现AOP,所以Spring默认是使用JDK的动态代理技术实现AOP的。

JdkDynamicAopProxy的实现其实很简单:

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {    
    @Override
    public Object getProxy(ClassLoader classLoader) {
        if (logger.isDebugEnabled()) {
            logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
        }
        Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised);
        findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
        return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
    }

    3. Spring AOP的配置

        Spring中AOP的配置一般有两种方法,一种是使用<aop:config>标签在xml中进行配置,一种是使用注解以及@Aspect 风格的配置。

        3.1. 基于<aop:config>的AOP配置

<tx:advice id="transactionAdvice" transaction-manager="transactionManager"?>
	<tx:attributes >
		<tx:method name="add*" propagation="REQUIRED" />
		<tx:method name="append*" propagation="REQUIRED" />
		<tx:method name="insert*" propagation="REQUIRED" />
		<tx:method name="save*" propagation="REQUIRED" />
		<tx:method name="update*" propagation="REQUIRED" />

		<tx:method name="get*" propagation="SUPPORTS" />
		<tx:method name="find*" propagation="SUPPORTS" />
		<tx:method name="load*" propagation="SUPPORTS" />
		<tx:method name="search*" propagation="SUPPORTS" />

		<tx:method name="*" propagation="SUPPORTS" />
	</tx:attributes>
</tx:advice>
<aop:config>
	<aop:pointcut id="transactionPointcut" expression="execution(* net.aazj.service..*Impl.*(..))" />
	<aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
</aop:config>

在看一个例子:

<bean id="aspectBean" class="net.aazj.aop.DataSourceInterceptor"/>
<aop:config>
	<aop:aspect id="dataSourceAspect" ref="aspectBean">
		<aop:pointcut id="dataSourcePoint" expression="execution(public * net.aazj.service..*.getUser(..))" />
		<aop:pointcut expression="" id=""/>
		<aop:before method="before" pointcut-ref="dataSourcePoint"/>
		<aop:after method=""/>
		<aop:around method=""/>
	</aop:aspect>
	
	<aop:aspect></aop:aspect>
</aop:config>

<aop:aspect>配置一个切面;<aop:pointcut>配置一个切点,基本切点表达式;<aop:before>,<aop:after>,<aop:around>是定义不同类型的advise,aspectBean是切面的处理Bean;

public class DataSourceInterceptor {
    public void before(JoinPoint jp) {
        DataSourceTypeManager.set(DataSources.SLAVE);
    }
}

        3.2. 基于注解和@Aspect风格的AOP配置

我们以事务配置为例:首先我们启用基于注解的事物配置:

<!-- 使用annotation定义事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />

然后扫描Service包:

<context:component-scan base-package="net.aazj.service,net.aazj.aop" />

最后在service上进行注解:

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userMapper;
    
    @Transactional (readOnly=true)
    public User getUser(int userId) {
        System.out.println("in UserServiceImpl getUser");
        System.out.println(DataSourceTypeManager.get());
        return userMapper.getUser(userId);
    }
    
    public void addUser(String username){
        userMapper.addUser(username);
//        int i = 1/0;    // 测试事物的回滚
    }
    
    public void deleteUser(int id){
        userMapper.deleteByPrimaryKey(id);
//        int i = 1/0;    // 测试事物的回滚
    }
    
    @Transactional (rollbackFor = BaseBusinessException.class)
    public void addAndDeleteUser(String username, int id) throws BaseBusinessException{
        userMapper.addUser(username);
        this.m1();
        userMapper.deleteByPrimaryKey(id);
    }
    
    private void m1() throws BaseBusinessException {
        throw new BaseBusinessException("xxx");
    }

    public int insertUser(User user) {
        return this.userMapper.insert(user);
    }
}

搞定。这种事物配置方式,不需要我们书写pointcut表达式,而是我们在需要事物的类上进行注解。但是如果我们自己来写切面的代码时,还是要写pointcut表达式,下面卡一个例子(自己写切面逻辑):

首先去扫描@Aspect 注解定义的 切面:

<context:component-scan base-package="net.aazj.aop" />

启用@AspectJ风格的注解:

<aop:aspectj-autoproxy />

这里有两个属性,<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>,proxy-target-class="true" 这个最好不要随便使用,它是指定只能使用CGLIB代理,那么对final方法时会抛出错误,所以还是让spring自己选择是使用JDK动态代理还是CGLIB,expose-proxy="true" 的作用后面会讲到。

切面代码:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect    // for aop
@Component // for auto scan
@Order(0)  // execute before @Transactional
public class DataSourceInterceptor {
    @Pointcut("execution(public * net.aazj.service..*.get*(..))")
    public void dataSourceSlave(){};
    
    @Before("dataSourceSlave()")
    public void before(JoinPoint jp) {
        DataSourceTypeManager.set(DataSources.SLAVE);
    }
}

我们使用到了

  • @Aspect 来定义一个切面;
  • @Component是配合<context:component-scan/>,不然扫描不到;
  • @Order 定义了该切面切入的顺序,因为在同一个切点,可能同时存在多个切面,那么在这多个切面之间就存在一个执行的顺序问题,该例子是一个切换数据源的切面,那么它应该在事务处理之前执行,所以我们使用的@Order(0)来确保先切换数据源,然后加入事务处理。@Order的擦书越小,优先级越高,默认的优先级最低;
/**
 * Annotation that defines ordering. The value is optional, and represents order value
 * as defined in the {@link Ordered} interface. Lower values have higher priority.
 * The default value is {@code Ordered.LOWEST_PRECEDENCE}, indicating
 * lowest priority (losing to any other specified order value).
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface Order {
    /**
     * The order value. Default is {@link Ordered#LOWEST_PRECEDENCE}.
     * @see Ordered#getOrder()
     */
    int value() default Ordered.LOWEST_PRECEDENCE;
}

        3.3. 切点表达式(pointcut)

    通过上边的描述我们可以看到,无论是<aop:config>风格的配置,还是@Aspect风格的配置,切点表达式都是重点,都是我们必须要掌握的。

a>pointcut语法形式(execution):

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

带有?号的部分是可选的,所以可以简化成:ret-type-pattern name-pattern(param_pattern) 返回类型,方法名称,参数三部分来匹配。

配置起来起始也时分简单:

  •     * 星号表示任意返回类型,任意方法名,任意一个参数类型;
  •     .. 连续两个点表示0或者多个包路径,还有0个或多个参数;

例子:

execution(* net.aazj.service..*.get*(..))

这个例子表示 net.aazj.service包或者子包下的以get开头的方法,参数可以是0个或者多个(参数不限);

execution(* net.aazj.service.AccountService.*(..))

这个例子表示 net.aazj.service.AccountService接口下的任何方法,参数不限;

注意这里,将类名和包路径是一起来处理的,并没有进行区分,因为类名也是包路径的一部分。

参数 param-pattern 部分比较复杂;()表示没有参数,(..)表示不限参数,(*,String)表示第一个参数不限,第二个参数为String类型。

b>within()语法:

within() 只能指定(限定)包路径(类名也可以看做是包路径),表示某个包下或者子包下的所有方法;

within(net.aazj.service.*)
within(net.aazj.service..*)
within(net.aazj.service.UserServiceImpl.*)

c>this() 与 target():

this 是指代理对象,target 是指被代理对象(目标对象)。所以 this() 和 target() 分别限定代理对象的类型和被代理对象的类型;

this(net.aazj.service.UserService) 实现UserService的单利对象(中的所有方法);

target(net.aazj.service.UserService) 被代理对象实现了UserService(中的所有方法);

d>args():

限定方法的参数的类型;

args(net.aazj.pojo.User) 参数为User类型的方法。

e>@target(),@within(),@annotation(),args():

这些语法形式都是针对注解的,比如带有某个注解的类,带有某个注解的方法,参数的类型带有某个注解;

@within(org.springframework.transaction.annotation.Transactional)
@target(org.springframework.transaction.annotation.Transactional)
//两者都是指被代理对象类上有@Transactional注解的(类的所有方法),(两者似乎没有区别???)
@annotation(org.springframework.transaction.annotation.Transactional) //方法带有@Transactional 注解的所有方法
@args(org.springframework.transaction.annotation.Transactional) //参数的类型带有@Transactional 注解的所有方法

f>bean():指定某个bean的名称

bean(userService): bean的id为 "userService" 的所有方法;
bean(*Service): bean的id为 "Service"字符串结尾的所有方法;


另外注意上面这些表达式是可以利用 ||, &&, ! 进行自由组合的。
比如:execution(public * net.aazj.service..*.getUser(..)) && args(Integer,..)

    4. 向注解处理方法传递参数

        有时我们在写注解处理方法时,需要访问被拦截的方法的参数。此时我们可以使用 args() 来传递参数,下面看一个例子:

@Aspect
@Component // for auto scan
//@Order(2)
public class LogInterceptor {    
    @Pointcut("execution(public * net.aazj.service..*.getUser(..))")
    public void myMethod(){};

    @Before("myMethod()")
    public void before() {
        System.out.println("method start");
    } 
    
    @After("myMethod()")
    public void after() {
        System.out.println("method after");
    } 
    
    @AfterReturning("execution(public * net.aazj.mapper..*.*(..))")
    public void AfterReturning() {
        System.out.println("method AfterReturning");
    } 
    
    @AfterThrowing("execution(public * net.aazj.mapper..*.*(..))")
//  @Around("execution(public * net.aazj.mapper..*.*(..))")
    public void AfterThrowing() {
        System.out.println("method AfterThrowing");
    } 
    
    @Around("execution(public * net.aazj.mapper..*.*(..))")
    public Object Around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("method Around");
        SourceLocation sl = jp.getSourceLocation();
        Object ret = jp.proceed();
        System.out.println(jp.getTarget());
        return ret;
    } 
    
    @Before("execution(public * net.aazj.service..*.getUser(..)) && args(userId,..)")
    public void before3(int userId) {
        System.out.println("userId-----" + userId);
    }  
    
    @Before("myMethod()")
    public void before2(JoinPoint jp) {
        Object[] args = jp.getArgs();
        System.out.println("userId11111: " + (Integer)args[0]);
        System.out.println(jp.getTarget());
        System.out.println(jp.getThis());
        System.out.println(jp.getSignature());
        System.out.println("method start");
    }    
}

其中方法:

@Before("execution(public * net.aazj.service..*.getUser(..)) && args(userId,..)")
public void before3(int userId) {
    System.out.println("userId-----" + userId);
}

它会拦截 net.aazj.service 包下或者子包下的getUser方法,并且该方法的第一个参数必须是int型的,那么使用切点表达式args(userId,..)就可以使我们在切面中的处理方法before3中可以访问这个参数。

before2方法也让我们知道也可以通过 JoinPoint 参数来获得被拦截方法的参数数组。JoinPoint 是每一个切面处理方法都具有的参数,@Around类型的具有的参数类型为ProceedingJoinPoint。通过JoinPoint或者ProceedingJoinPoint参数可以访问到被拦截对象的一些信息(参见上面的before2方法)。

    5. Spring AOP的缺陷

        因为Spring AOP是基于动态代理对象的,那么如果target中的方法不是被代理对象调用的,那么就不会织入切面代码,看个例子:

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userMapper;
    
    @Transactional (readOnly=true)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }
    
    public void addUser(String username){
        getUser(2);
        userMapper.addUser(username);
    }
}

看到上面的 addUser() 方法中,我们调用了 getUser() 方法,而getUser() 方法是谁调用的呢?是UserServiceImpl的实例,不是代理对象,那么getUser()方法就不会被织入切面代码。

切面代码如下:

@Aspect
@Component
public class AOPTest {
    @Before("execution(public * net.aazj.service..*.getUser(..))")
    public void m1(){
        System.out.println("in m1...");
    }    
    @Before("execution(public * net.aazj.service..*.addUser(..))")
    public void m2(){
        System.out.println("in m2...");
    }
}

执行如下代码:

public class Test {
	public static void main(String[] args){        
		ApplicationContext context = new ClassPathXmlApplicationContext(
						new String[]{"config/spring-mvc.xml","config/applicationContext2.xml"});

		UserService us = context.getBean("userService", UserService.class);
		if(us != null){
			us.addUser("aaa");
		}
	}
}

输出结果如下:

in m2...

虽然 getUser()方法 被调用了,但是因为不是代理对象调用的,所以 AOPTest.m1() 方法并没有执行。这就是Spring aop的缺陷。解决方法如下:

首先: 将 <aop:aspectj-autoproxy /> 改为:

<aop:aspectj-autoproxy expose-proxy="true"/>

然后,修改UserServiceImpl中的 addUser() 方法:

@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private UserMapper userMapper;
    
    @Transactional (readOnly=true)
    public User getUser(int userId) {
        return userMapper.getUser(userId);
    }
    
    public void addUser(String username){
        ((UserService)AopContext.currentProxy()).getUser(2);
        userMapper.addUser(username);
    }
}

((UserService)AopContext.currentProxy()).getUser(2); 先获得当前的代理对象,然后在调用 getUser() 方法,就行了。

expose-proxy="true" 表示将当前代理对象暴露出去,不然 AopContext.currentProxy() 或得的是 null .

修改之后的运行结果:

in m2...
in m1...

 

 

 

 

 

本文转载自:http://www.cnblogs.com/digdeep/p/4528353.html?utm_source=tuicool&utm_medium=referral

llahn_v
粉丝 0
博文 5
码字总数 951
作品 0
朝阳
程序员
私信 提问
Java系列文章(全)

JVM JVM系列:类装载器的体系结构 JVM系列:Class文件检验器 JVM系列:安全管理器 JVM系列:策略文件 Java垃圾回收机制 深入剖析Classloader(一)--类的主动使用与被动使用 深入剖析Classloader(二...

www19
2017/07/04
0
0
深入理解-Spring-之源码剖析IOC(一)

引言 作为Java程序员,Spirng我们再熟悉不过,可以说比自己的女朋友还要亲密,每天都会和他在一起,然而我们真的了解spring吗? 我们都知道,Spring的核心是IOC和AOP,但楼主认为,如果从这两...

Mr_zebra
2018/09/10
41
0
深入 Spring Boot : 快速集成 Dubbo + Hystrix

背景 Hystrix 旨在通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备拥有回退机制和断路器功能的线程和信号隔离,请求缓存和请求打包,以...

小致dad
2018/07/02
349
0
Spring 事务管理高级应用难点剖析: 第 2 部分

Spring 抽象的 DAO 体系兼容多种数据访问技术,它们各有特色,各有千秋。像 Hibernate 是非常优秀的 ORM 实现方案,但对底层 SQL 的控制不太方便;而 iBatis 则通过模板化技术让您方便地控制...

红薯
2010/03/28
801
1
Spring源码剖析开篇:Spring概述

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a724888/article/details/73033273 欢迎阅读完整的专栏博客内容:Spring源码解析 本文首发于我的个人公众号:...

你的猫大哥
2017/06/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

计算机实现原理专题--二进制减法器(二)

在计算机实现原理专题--二进制减法器(一)中说明了基本原理,现准备说明如何来实现。 首先第一步255-b运算相当于对b进行按位取反,因此可将8个非门组成如下图的形式: 由于每次做减法时,我...

FAT_mt
今天
5
0
好程序员大数据学习路线分享函数+map映射+元祖

好程序员大数据学习路线分享函数+map映射+元祖,大数据各个平台上的语言实现 hadoop 由java实现,2003年至今,三大块:数据处理,数据存储,数据计算 存储: hbase --> 数据成表 处理: hive --> 数...

好程序员官方
今天
7
0
tabel 中含有复选框的列 数据理解

1、el-ui中实现某一列为复选框 实现多选非常简单: 手动添加一个el-table-column,设type属性为selction即可; 2、@selection-change事件:选项发生勾选状态变化时触发该事件 <el-table @sel...

everthing
今天
6
0
【技术分享】TestFlight测试的流程文档

上架基本需求资料 1、苹果开发者账号(如还没账号先申请-苹果开发者账号申请教程) 2、开发好的APP 通过本篇教程,可以学习到ios证书申请和打包ipa上传到appstoreconnect.apple.com进行TestF...

qtb999
今天
10
0
再见 Spring Boot 1.X,Spring Boot 2.X 走向舞台中心

2019年8月6日,Spring 官方在其博客宣布,Spring Boot 1.x 停止维护,Spring Boot 1.x 生命周期正式结束。 其实早在2018年7月30号,Spring 官方就已经在博客进行过预告,Spring Boot 1.X 将维...

Java技术剑
今天
18
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部