Spring整理

一、简述Spring框架

​ spring是一个是基于分层的javaEE应用的一站式轻量级开源框架,主要核心是IoC/DI(控制反转/依赖注入) 与 Aop(面向切面)两大技术,帮助我们实现开发过程中的轻松解耦,提高项目的开发效率。

二、Spring的特点、优点

1、方便解耦,简化开发

通过spring提供的ioc容器,将创建对象及对象之间的依赖关系交给spring进行控制(将原本new对象的过程交给spring-ioc),将资源进行集中管理,实现资源的可配置和易管理,避免硬编码造成的过度耦合。

2、AOP编程的支持

spring中的AOP支持将一些通用任务如安全、事务、日志等进行集中式管理,达到更好的服务。

3、声明事务的支持

通过声明的方式灵活的进行事务的管理,提高开发效率和质量。

4、方便程序的测试
5、方便集成各种优秀的框架

spring支持各种优秀的开源框架,并降低了各种框架的使用难度。(struts、Hibernate、Hession、Quartz)

6、降低了很多JAVAEE API的使用难度

比如对JDBC、JavaMail、远程调用等,进行了简易封装,大大降低了这些api的使用难度。

三、Spring的主要模块组成

1、Spring Core

框架的最基础部分,提供IoC容器,对bean进行管理。核心容器的主要组件是BeanFactory。

2、Spring Context

(spring上下文是一个配置文件,向Spring框架提供上下文信息)

基于bean,提供上下文信息,扩展出JNDI、EJB、电子邮件、国际化、校验和调度等功能。

3、Spring DAO

提供了JDBC的抽象层,它可消除冗长的JDBC编码和解析数据库厂商特有的错误代码,还提供了声明性事务管理方法。

4、Spring ORM

提供了常用的“对象/关系”映射APIs的集成层。其中包括JPA、JDO、Hibernate、MyBatis等。

5、Spring AOP

提供了符合AOP Aliance规范的面向切面的编程实现。

6、Spring Web

提供了基础的Web开发的上下文信息,可与其他web进行集成。

7、Spring Web MVC

提供了Web应用的Model-View-Controller全功能实现。

四、spring核心容器(应用上下文)模块

基本的spring模块,提供spring框架的基本功能,BeanFactory是任何以spring为基础的应用的核心。Spring框架建立在此模块之上,它使spring称为一个容器。

五、BeanFactory实现举例?

最常用的BeanFactory实现是xmlBeanFactory类

bean工厂是工厂模式的一个实现,提供了控制反转功能,用来把应用的配置和依赖从真正的应用代码中分离。

六、XMLBeanFactory

最常用的beanfactory实现。他根据xml文件中的定义加载beans,从xml文件读取配置元数据并用它去创建一个完全配置的系统或应用。

七、什么是ApplicationContext,它的通常实现是什么?

ApplicationContext是BeanFactory接口的子接口,它增强了BeanFactory的功能,处于context包下。它是context包的基础。

1、FileSystemXmlApplicationContext:

此容器从一个xml文件中加载bean的定义

//加载单个配置文件,相对路径
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
//加载单个配置文件,绝对路径
ApplicationContext ctx = new FileSystemXmlApplicationContext("D:/project/bean.xml");
//使用字符串数组,加载多个配置文件
String[] locations = {"bean1.xml","bean2.xml","bean3.xml"};
ApplicationContext ctx = new FileSystemXmlApplication(locations);
2、ClassPathXmlApplicationContext

此容器从一个xml文件中加载bean的定义

FileSystemXmlApplicationContext、ClassPathXmlApplicationContext与BeanFactory的xml文件定位方式一样,是基于路径的。
3、XmlWebApplicationContext

此容器加载一个配置文件,此文件定义了一个web应用的所有bean

ServletContext servletContext = request.getSession().getServletContext();
ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

八、bean工厂和ApplicationContext有什么区别?

ApplicationContext是BeanFactory的子接口,拥有BeanFactory的全部功能,并扩展了很多高级特性。beanFactory的特点是在每次获取对象时才会创建对象,而ApplicationContext的特点是每次容器启动就会创建所有对象。(这样做是因为早期的电脑性能低,内存小,所以spring容器的容量不足)。

九、解释jdbc抽象和dao模块

通过使用jdbc抽象和dao模块,保证数据库代码的简洁,并能避免数据库资源错误关闭导致的问题,它在各种不同的数据库的错误信息之上提供了一个统一的异常访问层,还利用spring的aop模块给spring应用中的对象提供事务管理服务。

十、解释对象关系映射集成模块—spring ORM模块

Spring通过提供ORM模块,支持我们直接在jdbc之上使用一个对象关系映射工具,spring支持集成主流的ORM框架,如hibernate、MyBatis、iBatis、EclipseLink、JFinal。

同样的Spring的事务管理同样支持以上所有ORM框架及JDBC

十一、解释web模块

Spring的web模块是构建在 applicationcontext模块基础之上,提供一个适合web应用的上下文。这个模块也包括支持多种面向web的任务,比如透明的处理多个文件上传请求和程序级请求参数的绑定到你的业务对象。

十二、为什么说Spring是一个容器

这是形容它使用来存储单例的bean对象的

十三、spring配置文件

Spring配置文件是xml文件,这个文件包含了类信息,描述了如何配置它们以及如何相互调用。

十四、ioc的优点

将创建对象和对象之间的依赖关系交由spring来控制,将资源集中管理到IoC容器中,实现资源的可配置易管理,避免硬编码造成的过渡耦合。

十五、一个spring的应用看起来像什么?

一个定义了一些功能的接口

十六、IoC实例化bean的3种方式

1、构造器(默认)(80%)
<bean id="userServiceImpl" class="com.wz.service.impl.UserServiceImpl"></bean>

​ 通过默认构造器创建,(bean对应的类的)空构造方法必须存在,否则创建失败。

2、静态工厂(10%)

可以控制bean的初始化 (默认空构造器的方法不能进行bean的初始化操作)

​ 通过工厂的静态方法创建一个对象,做了初始化的操作

<bean id="personService" class="com.wz.factory.StaticFactory" factory-method="getPersonService"></bean>
//bean对应的类
public class PersonService{
	private String tag;
	
	public PersonService(Strin tag){
		this.tag = tag;
	}
	public void show(){
		System.out.println("Person:"+tag);
	}
}
//静态工厂
public class StaticFactory{
	
	public static PersonService getPersonService(){
		return new PersonService("sxt");
	}
	
}
//测试
public class PersonServiceTest{
	ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
	PersonService personService = context.getBean("personService");
    personService.show();	//输出结果为   Person:sxt
}
3、实例化工厂(10%)

利用实例化factory方法创建,即将factory方法也作为业务bean来控制。

1、可用于集成其他框架的bean创建管理方法,2、能够使bean和factory的角色互换

<bean id="initFactory" class="com.wz.factory.InitFactory"></bean>
<bean id="accountSercice" factory-bean="initFactory" factory-method="getAccountService"></bean>

依赖注入

一、什么是spring的依赖注入?

通常情况下我们如果要在一个类中使用另一个类,需要使用new来初始化这个类的实例再使用。而依赖注入是为了方便我们不用每次使用一个类而去new这个类,而是通过注入的方式将类的实例注入到这个类中,从而进行使用。

二、IoC(依赖注入)的方式?

1、set注入

2、构造器注入

3、静态工厂注入

4、实例化工厂注入

Spring Beans

一、什么是Spring Beans?

Spring beans是那些形成Spring应用的主干的java对象。它们被Spring IoC容器初始化、装配和管理。这些beans通过容器中配置的元数据创建。 比如:以xml文件中的形式定义。

(Spring框架中定义的beans都是单件beans。在bean tag中有个属性"singleton",默认true,为单间beans)

二、一个spring bean定义包含什么?

一个Spring Bean的定义包含容器必知的所有配置元数据(描述数据的数据),包含如何创建一个bean,它的生命周期详情及它的依赖。

三、怎么给spring容器配置元数据(bean)?

1、XML配置文件

2、基于注解的配置

3、基于java的配置(@Configuration+@Bean(name=""))

四、你怎么定义类的作用域?

在定义标签的时候,有一个scope属性。在spring要在需要的时候每次产生一个 新的bean实例,bean的scope属性被指定为prototype。当一个bean每次使用时必须返回同一个实例,这个bean的scope属性必须设为singleton。

五、解释Spring支持的几种bean的作用域

Spring框架支持一下五种bean的作用域:

1、singleton:bean在每个Spring ioc容器中只有一个实例

2、prototype:一个bean的定义可以有多个实例

3、request:每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效。

4、session:在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。

5、global-session:在一个全局的HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。

缺省的Spring bean的作用域是Singleton。

六、Spring框架中的单例bean是线程安全的吗?

不,Spring框架中的单例bean不是线程安全的。

七、 Spring中bean的生命周期过程

img

1、Spring对bean的实例化

2、Spring将值和bean的引用注入到bean对应的属性中

3、如果bean实现了BeanNameAware接口,Spring将Bean的ID传递给setBeanName()方法

​ (实现BeanNameAware主要是为了通过bean的引用来获得bean的ID,一般业务中是很少用到bean的ID

4、如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory(BeanFactory bf)方法,并把BeanFactory容器实例作为参数传入。

​ (实现beanFactoryAware主要目的是为了获取Spring容器,如Bean通过Spring容器发布事件等)

5、如果bean实现了ApplicationContextAware接口,Spring容器将调用setApplicationContext(Application ctx)方法,将应用上下文当参数传入。

(作用与实现beanFactoryAware类似)

6、如果bean实现了beanPostProcess接口,Spring将调用它们的postProcessBeforeInitialization(与初始化)方法。

​ (作用是在bean实例创建成功后进行增强处理,如对bean进行修改、增加某个功能)

7、如果bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet方法,作用与在配置文件中对bean使用init-method声明初始化的作用一样,都是在bean的全部属性设置成功后执行的初始化方法

8、如果Bean实现了BeanPostProcess接口,Spring将调用它们的postProcessAfterInitialization(后初始化)方法。

​ (作用与6的一样,只不过6是在bean初始化前执行的,而这个是在bean初始化后执行的,时机不同

9、经过以上的工作,Bean将一直驻留在应用上下文中给应用使用,知道应用上下文被销毁。

10、如果bean实现了DispostbleBean接口,Spring将调用它的destory方法,作用与在配置文件中对bean使用destory-method属性的作用一样,都是在bean实例销毁钱执行的方法

八、那些是bean最重要的生命周期方法?

bean生命周期中两个重要的方法:

1、在容器加载bean的时候调用的setup

2、在容器卸载类的时候调用的teardown

the bean标签有两个重要的属性(init-method和destory-method)。用他们你可以自己定制初始化和注销方法。他们也有相应的注解(@PostConstruct和@PreDestory)。

九、什么是Spring的内部bean?

当一个bean仅仅被当做另一个bean的属性时,它能被声明为一个内部bean。内部bean通常是匿名的,它们的scope一般是prototype。

(在Spring的基于XML的配置元数据中,可以在或元素内使用元素)

十、在Spring中如何注入一个java集合?

Spring提供一下几种集合的配置元素:

​ 1、类型用于注入一列值,允许有相同的值。

​ 2、类型用于注入一组值,不允许有相同的值。

​ 3、类型用于注入一组键值对,键和值都可以为任意类型。

​ 4、类型用于注入一组键值对,键和值都只能为String类型。

十一、什么是bean装配?

装配,或bean装配是指在Spring容器中把bean组装到一起,前提是容器需要知道bean的依赖关系。

十二、什么是bean的自动装配?

Spring容器能够自动装配相互合作的bean,这意味着容器不需要和配置,能通过Bean工厂自动处理bean之间的协作。

十三、解释不同方式的自动装配

十四、自动装配有哪些局限性?

1、重写:任需用和装配来定义依赖,意味着总要重写自动装配。

2、基本数据类型: 你不能自动装配简单的属性,如基本数据类型,String字符串 ,和类。

3、模糊特性:自动装配不如显式装配精确,如果可能,建议使用显式装配。

Spring注解:

一、什么是基于Java的Spring注解配置?给一些注解的例子。

基于java的配置,允许你在少量的java注解的帮助下,进行你的大部分Spring配置而非通过xml文件。

以@Configuration注解为例,它用来标记类可以当做一个bean的定义,被Spring IoC容器使用。另一个例子是@Bean注解,它表示此方法将要返回一个对象,作为一个bean注册进Spring应用上下文。

二、什么是基于注解的容器配置?

通过在响应的类,方法或属性上使用注解的方式,直接在组件类中进行配置,而不是通过使用xml表述bean的装配关系。

三、怎样开启注解装配?

注解装配在默认情况下是不开启的,为了使用注解,我们必须在Spring配置文件中配置< context:annotation-config/ > 的配置。

四、@Configuration

加了@Configuration注解的类,等价于在xml中配置beans,相当于IoC容器,它的某个方法头上如果使用了@Bean,该类就会作为这个Spring容器中的bean,与xml中配置的bean意思一样。

@Configuration  
public class MainConfig {  

    //在properties文件里配置  
    @Value(“${wx_appid})  
    public String appid;  

    protected MainConfig(){}  

    @Bean  
    public WxMpService wxMpService() {  
        WxMpService wxMpService = new WxMpServiceImpl();  
        wxMpService.setWxMpConfigStorage(wxMpConfigStorage());  
        return wxMpService;  
    }  
}  

五、@Bean

用于一个类的方法上,表该方法用于产生该类的bean

六、@Value

@Value("#{}")与@Value("${}")的作用及区别:

1、@Value("#{}")、@Value("")

@Value("#{}")通常用来表示常量,或者获取bean的属性,当然还可以调用bean的某个方法,

2、@Value("${}")

通过@Value("${}") 可以获取对应属性文件中定义的属性值。比如获取properties文件中的值。

需在spring.xml中配置:

<context:property-placeholder location="classpath:resources/resource.properties" />

七、@Controller,@Service,@Repository,@Component

目前4种注解意思是一样的,并没有什么区别。使用方法:

1、使用<context:component-scanbase-package=“XXX” /> 扫描被注解的类

2、@Controller用于controller类,@Service用于serviceImpl类,@Repository用于dao层,@Component用于组件

八、@Autowired

@Autowired默认先按type注入,如果发现多个bean,则用**@Qualifier**来按name注入

//1.通过此注解完成从spring配置文件中 查找满足Fruit的bean,然后按//@Qualifier指定pean
@Autowired
@Qualifier(“pean”)
public Fruit fruit;

//2.如果要允许null 值,可以设置它的required属性为false,如:
@Autowired(required=false) 
public Fruit fruit;

九、@Resource

默认按name注入,如果找不到再按 type找 bean,如果还找不到则抛异常。不管按哪种方式,如果找到多个bean,则抛出异常。

//可以手动指定bean,它有2个属性分别是name和type,使用name属性,则使用byName的自动注入,而使用type属性时则使用byType自动注入。

@Resource(name=”bean名字”)
或
@Resource(type=”bean的class”)

//这个注解是属于J2EE的,减少了与spring的耦合。

AOP 面向切面:

一、AOP的概念及作用

1、解释下什么叫aop?

AOP是spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。

2、aop有什么作用或优势?以及常用的使用场景

使用aop技术,可以将一些系统性相关的编程工作,独立的提取出来,独立实现,然后通过切面切入金系统。从而避免了在业务逻辑代码中混入很多的系统相关的逻辑—比如***权限管理***,***事务管理,日志记录***等。

3、aop相关概念

1)、Aspect:切面,切入系统的一个切面。比如事务管理是一个切面,权限管理也是一个切面;

2)、Join point:连接点,也就是可以进行横向切入的位置。

3)、Advice:通知,切面在某个连接点执行的操作。

4)、Pointcut:切点,符合切点表达式的连接点,是真正被切入的地方。

二、aop的实现方式

通知类型说明
前置通知(MethodBeforeAdvice)目前方法执行之前调用
后置通知(AfterReturningAdivce)目标方法执行完成之后调用
环绕通知(MethodInterceptor)目前方法执行前后都会调用方法,且能增强结果
异常处理通知 (ThrowsAdvice)目标方法出现异常调用

三、aop的实现原理

AOP分为静态aop和动态aop。

静态aop:是指AspectJ实现的AOP,他是将切面代码直接编译到java类文件中。

动态aop:是指将切面代码进行动态织入实现的aop。

Spring的 aop为动态Aop,实现的技术为:JDK提供的动态代理技术(有接口)与CGLIB(无接口)。

SpringBoot的 aop的默认代理是什么? CGLIB(动态字节码增强技术)。

1、JDK动态代理

主要使用到 InvocationHandler接口 和 Proxy.newProxyInstance()方法。

JDK动态代理要求被代理实现一个接口,只有接口中的方法才能够被代理。

其方法是将被代理的对象注入到一个中间对象中,而中间对象实现InvocatioHandler接口。

在实现该接口时,可以在被代理对象调用它方法时,在调用前后插入一些代码。

而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;    // 被代理的对象
    
    P roxyUtil(Object target){
        this.target = target;
    }
    
    //被代理类的任何方法的执行,此方法都会执行
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("do sth before....");
        
        //method指的是当前正在调用的方法  .invoke表示执行该方法
        //args表示该方法的参数
        Object result =  method.invoke(target, args);
        
        System.out.println("do sth after....");
        return result;
    }

    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....
2、CGLIB (code generate libary)

字节码生成技术实现AOP,其实就是继承被代理对象,然后Override需要被代理的方法,在覆盖该方法时,自然是可以插入我们的代码的。

它的原理是生成一个父类 enhancer.setSuperclass( this.target.getClass()) 的子类 enhancer.create() ,然后对父类的方法进行拦截enhancer.setCallback( this) . 对父类的方法进行覆盖,所以父类方法不能是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());
    }
}

四、Spring Aop的配置(以事务管理为例)

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

1、基于<aop:config >的aop配置

下面是一个典型的事务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。

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表达式。

Spring中整合jdbc

一、Spring对jdbc的支持

Spring为了提供对jdbc的支持,在JDBC API的基础上封装了一套实现,以此建立一个JDBC存取框架。

作为Spring JDBC框架的核心,JDBC模板的设计目的是为了不同类型的JDBC操作 提供模板方法。每个模板方法都能控制整个过程,并允许覆盖过程中的特定任务。通过这种方式,可以在尽可能保留灵活性的情况下,将数据存取的工作量降到最低。

二、传统JDBC实现

public class JdbcTest {

    /**
     * 1.数据库连接,使用时就创建,不适用就立即释放,对数据库进行频繁的开启和关闭,造成数据库资源浪费,影像数据库性能。
     *      解决方案:使用数据库连接池来管理数据库连接
     *
     * 2.将sql语句硬编码到java代码中,若需sql修改,则需要重新编译java代码,不利于系统维护。(只要出现【硬编译】三个字都会不利于系统维护)
     *      解决方案:将sql语句配置在xml文件中,即使sql变化,也不需要重新编译。
     *
     * 3.向ps设置参数,对占位符的位置和参数赋值,硬编码在java代码中,不利于系统文化
     *      解决方案:将sql语句、占位符、参数全部配置在xml中。
     *
     * 4.取结果集数据(从rs遍历结果集数据时,存在硬编码,将要获取的字段进行硬编码),不利于系统维护。
     *      解决方案:
     *          设想:将查询到的结果集,自动映射成java对象。
     *
     */
    public static void main(String[] args) {
        // 数据库连接
        Connection conn = null;
        // 预编译的statement,使用预编译的statement可以提高数据库性能
        PreparedStatement ps = null;
        // 结果集对象
        ResultSet rs = null;

        try{
            // 加载数据库驱动
            Class.forName("oracle.jdbc.OracleDriver");
            // 加载数据库链接
            conn = DriverManager.getConnection("数据库url", "数据库用户名", "数据库密码");
            // 定义sql语句  ?表示占位符
            String sql = "select * from pf_customer where name=?";
            // 获取预处理statement
            ps = conn.prepareStatement(sql);
            // 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值
            ps.setString(1, "18620990000");
            // 进行sql查询,返回结果集
            rs = ps.executeQuery();
            
            while (rs.next()) {
                System.out.println(rs.getString("id") + "   " + rs.getString("sex"));
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally{
            // 释放资源
            if(rs != null){
                try {
                    rs.close();
                } catch (SQLException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            if(ps != null){
                try {
                    ps.close();
                } catch (SQLException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            if(conn != null){
                try {
                    conn.close();
                } catch (SQLException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

}

三、Spring+JDBC实现(核心是JdbcTemplate类,只需要创建一次,这是一个线程安全类)

相比于传统的jdbc实现,在jdbc API的基础上封装了一套实现JdbcTemplate,JdbcTemplate的优点如下:

1)、配置基于模板设置

2)、完成了资源的创建和释放的工作

3)、完成了对JDBC的核心流程的工作,包括sql语句的创建和执行,简化了对JDBC的操作

4)、仅需要传递DataSource就可以把它实例化

5)、JdbcTemplate只需要创建一次,减少了代码复用的烦恼

6)、JdbcTemplate是线程安全类

步骤1:配置Spring+JDBC配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!-- 名称空间 -->
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:util="http://www.springframework.org/schema/util"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        ">
        
        <!-- 1、声明数据源对象:C3P0连接池 -->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <!-- 加载jdbc驱动 -->
            <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
            <!-- jdbc连接地址 -->
            <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/kk"></property>
            <!-- 连接数据库的用户名 -->
            <property name="user" value="root"></property>
            <!-- 连接数据库的密码 -->
            <property name="password" value="root"></property>
            <!-- 数据库的初始化连接数 -->
            <property name="initialPoolSize" value="3"></property>
            <!-- 数据库的最大连接数 -->
            <property name="maxPoolSize" value="10"></property>
            <!-- 数据库最多执行的事务 -->
            <property name="maxStatements" value="100"></property>
            <!-- 连接数量不够时每次的增量 -->
            <property name="acquireIncrement" value="2"></property>           
        </bean>
        
        <!--  创建jdbcTemplate对象 -->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource">
            </property>
        </bean>
        
        <bean id="jdbcUser" class="com.jyk.spring.jdbc.JdbcUser">
            <property name="jdbcTemplate" ref="jdbcTemplate">
            </property>
        </bean>
</beans>
步骤2:通过jdbcTemplate编写对业务的增删改查操作
package com.jyk.spring.jdbc;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

public class JdbcUser {
    
    private JdbcTemplate jdbcTemplate;    

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void save()
    {
        String sql = "insert into aa(id,name) values ('4','admin4')";
        //使用jdbc模板工具类来简化数据库执行前的操作
        jdbcTemplate.update(sql);
    }
    
    public person queryById(String id)
    {
        String sql = "select s.name from aa s where s.id=?";
        Map<String, Object> map = jdbcTemplate.queryForMap(sql, id);
        System.out.println(map);
        return null;
    }
    
    public List<person> queryAll()
    {
        String sql = "select * from aa";
        
      //RowMapper的使用:在Jdbc的操作中,有很多情况下是要将ResultSet里的数据封装在一个持久化Bean里,再把持久化Bean封装到集合中。这样会造成大量的代码的重复,不利于代码的复用。而RowMapper正好解决了这个问题。
        return jdbcTemplate.query(sql, new RowMapper<person>(){

            @Override
            public person mapRow(ResultSet rs, int index) throws SQLException {
                person p = new person();
                p.setId(rs.getInt("id"));
                p.setName(rs.getString("name"));
                return p;
            }});
    }
}

四、数据库连接池/数据源 (C3P0、dbcp和JNDI)

​ 由于建立数据库连接是一个非常耗时耗资源的行为,所以通过连接池预先同一个数据库建立一些连接,放在内存中,应用程序需要建立数据库连接时直接到连接池中申请一个就行,用完后再放回去。

Spring内置数据源: 只是新建连接,根本没有连接池的作用,不推荐使用

  • Spring内置数据源配置
    Class:DriverManagerDataSource
    全限定名:org.springframework.jdbc.datasource.DriverManagerDataSource
    需要的jar包:spring-jdbc.jar

  • ```xml





    
    
    
    

DBCP(DataBase connection pool),数据库连接池。是apache上的一个java连接池项目,也是tomcat使用过的连接池组件。没有自动回收空闲连接功能

  • Class:BasicDataSource
    全限定名:org.apache.commons.dbcp.BasicDataSource
    需要添加:com.springsource.org.apache.commons.dbcp-1.2.2.osgi.jar
    com.springsource.org.apache.commons.pool-1.5.3.jar

  • ```xml











    
    
    
    

C3P0是一个开源的JDBC连接池,它实现了数据源,支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate、Spring等。有自动回收空闲连接功能。

  • Class:ComboPooledDataSource
    全限定名:com.mchange.v2.c3p0.ComboPooledDataSource
    需要添加:com.springsource.com.mchange.v2.c3p0-0.9.1.2.jar

  • jdbc.properties

    driver=com.mysql.jdbc.Driver  
    url=jdbc:mysql://127.0.0.1:3306/steamedbun?useUnicode=true&characterEncoding=UTF-8  
    username=root  
    password=root  
      
    #连接池初始化时创建的连接数
    c3p0.initialPoolSize=3  
    #连接池中保留的最小连接数  
    c3p0.minPoolSize=5 
    #连接池中保留的最大连接数
    c3p0.maxPoolSize=100 
    #连接池无空闲连接可用时,一次性创建的新连接数
    c3p0.acquireIncrement=3 
    #最大空闲时间 
    c3p0.maxIdleTime=60 
    #每60秒检查所有连接池中的空闲连接 
    c3p0.idleConnectionTestPeriod=60
    #连接池在获得新连接失败时重试的次数
    c3p0.acquireRetryAttempts=30 
    #用以控制数据源内加载的PreparedStatements数量   
    c3p0.maxStatements=100  
    #c3p0是异步的,可以提升性能,通过多线程实现多个操作同时被执行 
    c3p0.numHelperThreads=10  
    

    数据源的配置:

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    		<!-- 1.2设置连接池的属性 -->
    		<property name="driverClass" value="com.mysql/jdbc.Driver"></property>
    		<property name="jdbcUrl" value="jdbc:mysql:///test-ssm"></property>
    		<property name="user" value="root"></property>
    		<property name="password" value="admin"></property>
    	</bean>
    

JNDI:需要在web server中配置数据源,不方便部署,如果使用高性能服务器WebLogic或Websphere,可以配置。

(jedis专门给redis提供了连接池管理)

另外redis的jedis集成了redis的一些命令操作,是封装了redis的java客户端,提供了连接池管理。一般不直接使用jedis,而是在其上再封装一层,作为业务的使用。 如果是spring的话,可以看看spring封装的redis— Spring Data Redis

注意:


以上spring整合JDBC并加入了数据源,但是,如果操作数据库的过程中出现了异常,jdbc需要我们手动的去控制事务,无法满足生产的需求。


Spring中的事务控制(声明式事务管理)

一、事务相关概念

1、什么是事务?

事务是正确执行一系列的操作(或行为),使得数据库从一种状态转换成另一种状态,且保证操作全部成功,或者全部失败。

2、事务的四大特性或原则?

原子性:不可分割性,事务要么全部被执行,要么全部不执行

一致性:事务执行前后数据的完整性必须保持一致。

隔离性:多个并发事务的数据相互隔离。

持久性:事务一旦被提交后,数据就会被持久化到数据库中。

二、Spring事务管理接口

1、PlatformTransactionManager 事务管理器

2、TransactionDefinition 事务定义信息(隔离,传播,超时,只读)

3、TransactionStatus 事务具体运行状态


1、PlatformTransactionManager(接口) 事务管理器

Spring为不同的持久层框架提供PlatformTransactionManager接口不同的实现类。

事务说明
org.springframework.jdbc.datasource.DataSourceTransactionManager使用Spring JDBC或iBatis进行持久化数据时使用
org.springframework.orm.hibernate3.HibernateTransactionManager使用Hibernate3.0版本进行持久化数据时使用
org.springframework.orm.jpa.JpaTransactionManager使用JPA进行持久化时使用
org.springframework.jdo.JdoTransactionManager当持久化机制是Jdo时使用
org.springframework.transaction.jta.JtaTransactionManager使用一个JTA实现来管理事务的,在一个事务跨越多个资源时必须使用
2、TransactionDefinition(接口)事务定义信息

事务定义信息接口中主要涉及 事务隔离级别事务传播行为 两个内容的设置。下面就具体分别来讲。

1)、事务隔离级别

事务隔离级别的设置是为了解决脏读,不可重复读,幻读等问题的。

***脏读:**一个事务a读取了另一个事务b改写还未提交的数据,如果另一个事务b回滚,事务a读取到的数据是无效的。 (侧重读取的数据是无效的)

***不可重复读:**在同一个事务中,多次读取同一数据读取到的结果不同。比如事务A多次读取同一数据,事务B在数据A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。

​ (侧重于修改)

***幻读:**一个事务读取了几行数据后,另一个事务插入了新的数据后,幻读就发生了。在后来的查询中,第一个事务就会发现一些原来没有的数据。

​ (侧重于新增或删除)

img

其中Spring中默认设置的default隔离级别的值就是数据库的默认隔离级别。比如mysql的默认隔离级别就是repeatable_read级别。

2)、事务传播行为

一般事务机制添加在service层上,service层中的多个方法之间的调用就涉及到事务传播机制,当事务方法被另一个事务方法调用时,必须指定事务是如何传播的。

img

三、spring中过的声明式事务管理

编程式事务管理将数据层提交事务的代码加入逻辑层,与spring无侵入式编程思想的主思想冲突,实际开发过程中,往往采用声明式事务管理形式。

声明式事务的核心机制:在业务逻辑层对应的业务上,通过SpringAop的思想,将公共的代码加入,即可完成整体事务的操作,这就是声明式事务管理的核心机制。

1、基于xml的声明式事务管理

a、导入spring事务支持的jar包spring-tx-3.2.0.RELEASE.jar

b、引入spring事务的名称空间;怕

这里写图片描述

c、配置spring的事务支持

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xmlns:aop="http://www.springframework.org/schema/aop"  
       xmlns:tx="http://www.springframework.org/schema/tx"  
       xsi:schemaLocation="http://www.springframework.org/schema/beans   
                           http://www.springframework.org/schema/beans/spring-beans.xsd   
                           http://www.springframework.org/schema/tx   
                           http://www.springframework.org/schema/tx/spring-tx.xsd  
                           http://www.springframework.org/schema/aop   
                           http://www.springframework.org/schema/aop/spring-aop.xsd">  
    <!-- 把Dao交给Spring管理 -->  
    <bean id="accountDao" class="springTX.dao.impl.AccountDaoImpl">  
        <!-- 为dao的父类JdbcDaoSupport注入一个数据源 -->  
        <property name="dataSource" ref="driverManagerDataSource"></property>  
    </bean>  

    <!-- 把业务层也交给Spring -->  
    <bean id="accountService" class="springTX.service.impl.AccountServiceImpl">  
        <property name="accountDao" ref="accountDao"></property>  
    </bean>  

    <!-- 事务控制:声明式事务,重点。基于XML的。  
         前期准备:  
            1.导入aop的jar包  
            2.确保spring-tx-xxxx.jar在你的工作空间中  
         步骤:  
            1.配置一个事务管理器,并为其注入数据源  
            2.配置事务的通知.id是为其指定一个唯一标识。transaction-manager指定事务通知使用的管理器  
            3.配置AOP,指定切面使用的通知。<aop:advisor advice-ref="" pointcut="切入点表达式"/>它就是指定已经配置好的通知类型。  
    -->  
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="driverManagerDataSource"></property>  
    </bean>  
    <tx:advice id="txAdivce" transaction-manager="transactionManager">  
        <!-- 配置事务的相关属性 -->  
        <tx:attributes>  
            <!-- tx:method 是用于指定方法名称和事务属性的关系。  
                 name:指定方法的名称。可以使用通配符*,也可以部分通配  
                 isolation:事务的隔离级别。默认值是:DEFAULT。当是default时是看数据库的隔离级别。   
                 rollback-for:该属性用于配置一个异常,当产生该异常时回滚。  
                 no-rollback-for:该属性用于配置一个异常,当产生该异常时不回滚。  
                 propagation:指定事务的传播行为。 默认值是:REQUIRED  
                 read-only:指定当前事务是否是只读事务。默认值是false,不是只读的。  
                 timeout:指定超时时间。默认值是-1。以秒为单位  
             -->  
            <tx:method name="*" />  
            <tx:method name="find*" read-only="true"/>  
        </tx:attributes>  
    </tx:advice>  
    <aop:config>  
        <aop:advisor advice-ref="txAdivce" pointcut="execution(* springTX..*.*(..))"/>  
    </aop:config>  
    
    <!-- 配置Spring的内置数据源 -->  
    <bean id="driverManagerDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
        <!-- 给数据源注入参数 -->  
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>  
        <property name="url" value="jdbc:mysql://localhost:3306/spring_tx"></property>  
        <property name="username" value="root"></property>  
        <property name="password" value="1234"></property>  
    </bean>  
</beans>
2、基于注解的声明式事务配置

a、把spring容器管理改为注解配置的方式(开启要扫描的包)

b、开启注解事务的支持

配置如下:

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xmlns:aop="http://www.springframework.org/schema/aop"  
       xmlns:tx="http://www.springframework.org/schema/tx"  
       xmlns:context="http://www.springframework.org/schema/context"  
       xsi:schemaLocation="http://www.springframework.org/schema/beans   
                           http://www.springframework.org/schema/beans/spring-beans.xsd   
                           http://www.springframework.org/schema/tx   
                           http://www.springframework.org/schema/tx/spring-tx.xsd  
                           http://www.springframework.org/schema/aop   
                           http://www.springframework.org/schema/aop/spring-aop.xsd   
                           http://www.springframework.org/schema/context   
                           http://www.springframework.org/schema/context/spring-context.xsd">  
    <!-- 事务控制:声明式事务,重点。基于注解的。  
         前期准备:  
            1.导入context的名称空间  
            2.确保spring-tx-xxxx.jar在你的工作空间中  
         步骤:  
            1.设置Spring要扫描的包  
            2.开始Spring对注解式事务的支持。并且指定 事务管理器类  
            3.把Dao和service都交给Spring管理  
            4.由于我们使用了注解,所以不能用JdbcDaoSupport,这个时候我们需要定义JdbcTemplate,为其注入数据源  
    -->  
    <context:component-scan base-package="springTX"></context:component-scan>  

    <tx:annotation-driven transaction-manager="transactionManager"/>  

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="driverManagerDataSource"></property>  
    </bean>  
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">  
        <property name="dataSource" ref="driverManagerDataSource"></property>  
    </bean>  

    <!-- 配置Spring的内置数据源 -->  
    <bean id="driverManagerDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
        <!-- 给数据源注入参数 -->  
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>  
        <property name="url" value="jdbc:mysql://localhost:3306/srping_tx"></property>  
        <property name="username" value="root"></property>  
        <property name="password" value="root"></property>  
    </bean>  
</beans>  

c、在需要事务支持的地方加入@Transactional注解(一般在service层控制事务)

d、@Transactional注解配置的位置:

​ a)、用在业务实现上:该类所有的方法都在事务的控制范围

@Component("accountService")
@Transaction
public class AccountServiceImpl implements AccountServiceI {

​ b)、用在业务接口上:该接口的所有实现类都起作用

@Transactional
public interface AccountServiceI {}

​ c)、通过注解指定事务的定义信息

@Transactional(readOnly=true)
public Object query(){
	return null;
}

Spring定时任务:

一、基于xml方式定时任务的配置

1、定时任务命名空间的添加

xmlns:task="http://www.springframework.org/schema/task" http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd

2、定时任务方法代码

package com.shsxt.task; 

import java.text.SimpleDateFormat;
import java.util.Date; 
import org.springframework.stereotype.Component;

@Component 
public class TaskSchedule {
    public void job1(){ 
        System.out.println("任务 1:"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
    }
    
    public void job2(){ 
        System.out.println("任务 2:"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
                                                                                             }
}

3、定时任务配置

<task:scheduled-tasks> 
<!-- 每个两秒执行一次任务 --> 
    <task:scheduled ref="taskSchedule" method="job1" cron="0/2 * * * * ?"/>
    <!-- 每隔五秒执行一次任务 -->
    <task:scheduled ref="taskSchedule" method="job2" cron="0/5 * * * * ?"/> 
    <!-- 多个定时任务 在这里配置 --> 
    
</task:scheduled-tasks>

二、基于注解的方式配置定时任务

1、在配置文件中添加命名空间

xmlns:task=http://www.springframework.org/schema/task http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd

2、配置定时任务驱动

<task:annotation-driven />

3、定时任务代码

package com.shsxt.task; 

import java.text.SimpleDateFormat;
import java.util.Date; import org.springframework.scheduling.annotation.Scheduled; 
import org.springframework.stereotype.Component;

@Component
public class TaskSchedule { 
	@Scheduled(cron="0/2 * * * * ?")
	public void job1(){
		System.out.println("任务1:"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
	}
    
    @Scheduled(cron="0/2 * * * * ?")
    public void job2(){ 
        System.out.println("任务 2:"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
    }

println(“任务 1:”+new SimpleDateFormat(“yyyy-MM-dd hh:mm:ss”).format(new Date()));
}

public void job2(){ 
    System.out.println("任务 2:"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
                                                                                         }

}


3、定时任务配置

```xml
<task:scheduled-tasks> 
<!-- 每个两秒执行一次任务 --> 
    <task:scheduled ref="taskSchedule" method="job1" cron="0/2 * * * * ?"/>
    <!-- 每隔五秒执行一次任务 -->
    <task:scheduled ref="taskSchedule" method="job2" cron="0/5 * * * * ?"/> 
    <!-- 多个定时任务 在这里配置 --> 
    
</task:scheduled-tasks>

二、基于注解的方式配置定时任务

1、在配置文件中添加命名空间

xmlns:task=http://www.springframework.org/schema/task http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd

2、配置定时任务驱动

<task:annotation-driven />

3、定时任务代码

package com.shsxt.task; 

import java.text.SimpleDateFormat;
import java.util.Date; import org.springframework.scheduling.annotation.Scheduled; 
import org.springframework.stereotype.Component;

@Component
public class TaskSchedule { 
	@Scheduled(cron="0/2 * * * * ?")
	public void job1(){
		System.out.println("任务1:"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
	}
    
    @Scheduled(cron="0/2 * * * * ?")
    public void job2(){ 
        System.out.println("任务 2:"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值