spring

Spring

*     很重要
#     不重要

spring framework

spring框架家族的学习路线

spring framework–>springWebMVC–>springboot–>springdata–>springSession–>springCloud

自己有兴趣可以看看spring serurity

spring的概念:spring是一个控制反转和面向切面的容器,用来管理bean对象,同时根据bean对象的依赖关系进行动态注入bean。其他各种组件可以集成到spring这个平台(DruidDatasource,mybatis)。

IOC:bean对象的维护由应用自身维护交由spring第三方容器来维护对象(创建,关系维护,生命周期,销毁)

DI:程序运行期间,根据bean的依赖关系,从容器中找到对应的对象注入。

AOP:面向切面编程。

1.搭建spring环境

​ 最新版本5.2.8

​ spring 框架中学习路线:

	核心的:IOC和AOP
	测试:与junit集成
	持久层:事务管理
	web:springMVC
	组件集成:远程接口调用,任务调度
操作步骤:
- 引入spring的核心jar包:4个核心jar,1个commons-logging日志包
- 创建spring的配置文件,spring.xml
- 创建服务层的类
- 把自定义的服务类配置到spring.xml中
- 测试类来加载spring.xml文件,创建spring的容器(保存对象)
	ApplicationContext ac = new ClasspathXMLApplicationContext("xml文件")
	Object obj = ac.getBean("bean的id")

注意: 1. ac.getBean()	几个方法都是获取spring容器管理的对象 
        在初始化的时候都已经实例化对象完成 getBean()都是获取同一个对象(默认单例) 
     2. 如果接口有两个及以上子类 那么不能通过他ClassName获得对象  找不到   
     3. spring在程序运行期间,动态的给成员变量赋值。  <property name="us" ref="userService"></property>
//接口
public interface Case1 {
    public User login(String name ,String pass);
}
//实现类
public class Case2Impl implements Case1 {
    private User us;
    @Override
    public User login(String name, String pass) {
        System.out.println("登录服务层");
        return null;
    }

    public void setUs(User us) {
        this.us = us;
    }

    public void getUs(User us) {
        this.us = us;
    }
}
//配置spring.xml
    <bean id="login" class="com.javasm.service.Case1Impl">
        <property name="us" ref="User" ></property>     //DI注入   ref只能放有<bean/>反转id的  
        //通过set方法动态给成员变量us赋值 
    </bean>
    <bean id="User" class="com.javasm.enity.User"/>
        
//
public class Case1Tset {

    @Test
    public void TestLogin(){
        //ClassPathXmlApplicationContext内部解析spring.xml,得到className
        //反射实例化出bean,吧bean保存到Map<String,Object>
        //已经把xml中配置的bean标签对应的类,实例化管理了。
        ApplicationContext ac = new ClassPathXmlApplicationContext("Case.xml");    
        Case1 s = ac.getBean(Case2Impl.class);          
        Case2Impl l = ac.getBean("login2", Case2Impl.class);
        Object c = ac.getBean("login2");
        System.out.println(s==c);
        Case1 x =(Case1)c;
        User z = x.login("zz","zz");
    }
}        
概念:
IOC:控制反转,原来对象管理由应用自身来管理,交由第三方的组件来管理对象。比如spring框架,本质就是由spring来new对象。
DI:依赖注入,在spring容器实例化期间,动态的给某个bean对象需要依赖的其他bean注入引用。

2.spring的作用

​ spring是一个开源控制反转和面向切面的容器框架,也称它为一个大的bean工厂,用来管理项目中mvc相关的bean对象。同时spring也是一个框架的平台,整合web开发中使用各种技术栈。(redis,mybatis,springmvc,quartz,hibernate,mybatis-plus等)。

  • 把对象交给spring后,对象默认单例,程序中需要用到该对象,不用到处new,产生一些垃圾对象。 ( IOC )
  • 实现MVC的真正解耦,控制层只依赖抽象的服务层,服务层只依赖抽象的dao层。( DI )

3.spring的容器原理

DOM4j加载xml,解析xml的bean标签,反射得到bean对象,把对象保存集合容器。

4.spring的ioc方式总结:

  • 配置bean标签

    用来为jar包中的类配置到spring容器,比如DruidDataSource等…

<bean id="" class="">
scope:singleton,表示该类单例,默认值
           prototype,原型,对象不再是单例,不常用,除了struts2的控制层对象会用到.
init-method:当bean实例化完成后,执行的初始化方法
destroy-method:当spring容器销毁时,执行bean的destroy-method方法
lazy-init:bean对象延迟初始化,但并不对象的单例状态

例: <bean id="userService" class="com.javasm.sys.service.SysuserServiceImpl" lazy-init="true"></bean>
  • 工厂实例化bean

把mybatis的sqlsessionFactory对象放容器

静态工厂实例化bean:
<bean id="sqlSessionFactory" class="com.javasm.factroy.SqlSessionFactoryBean" factory-method="getSqlSessionFactory"></bean>

实例工厂实例化bean:
<bean id="fb" class="com.javasm.factroy.SqlSessionFactoryBean"></bean>
<bean id="sqlSessionFactory" factory-bean="fb" factory-method="getSqlSessionFactory"></bean>
//例子
/**
 * 本类即工厂类
 */
public class SqlSessionFactoryBean {
    public  SqlSessionFactory getSqlSessionFactory(){
        InputStream in = null;
        try {
            in = Resources.getResourceAsStream("mybatis-config.xml");
        } catch (IOException e) {
            e.printStackTrace();
        }
        SqlSessionFactory s = new SqlSessionFactoryBuilder().build(in);
        return s;
    }
}

// <!--实例工厂实例化bean-->
    <bean id="fb" class="com.javasm.factroy.SqlSessionFactoryBean"></bean>
    <bean id="SqlSessionFactory" factory-bean="fb" factory-method="getSqlSessionFactory"></bean>
        
//运行
  public class FactoryBeanTest {
    @Test  //普通方法
    public void test1_createSqlSessionFactory(){
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        SqlSessionFactory sqlSessionFactory = fb.getSqlSessionFactory();
        System.out.println(sqlSessionFactory);
    }

    //把SqlSessionFactory交给spring容器管理。对于复杂对象的创建需要引入工厂实例化bean的方式
    //就是先实例化对象 然后调用对象里面的方法
    @Test
    public void test2_getSqlSessionFactoryFromSpringContainer(){
        ApplicationContext ac = new ClassPathXmlApplicationContext("Case.xml");
        SqlSessionFactory bean = ac.getBean(SqlSessionFactory.class);
        System.out.println(bean);
    }
}




//静态工厂实例化bean:
<bean id="sqlSessionFactory" class="com.javasm.factroy.SqlSessionFactoryBean" factory-method="getSqlSessionFactory"></bean>
//静态工厂类: 就是改为静态方法
public static SqlSessionFactory getSqlSessionFactory(){}
  • 包扫描bean注解

用来在自定义的类上加注解来配置到spring容器,

扫描注解:
@Controller,@Service,@Repository,@Component把类交给spring管理,成为springBean
@Autowired,@Resource用来做对象装配
@Scope指定bean的单例多例
@PostConstruct初始化方法
@PreDestroy销毁方法
<context:component-scan base-package="com.javasm"></context:component-scan>
通过注解配置的bean,id为类名首字母小写。
//spring.xml
<?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: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">

    <!--开启spring的包扫描,扫描指定包及子包下的所有类,反射检测类上是否带有指定的注解-->
    <!--这些注解不能加到接口上。-->
    <!--@Controller,@Service,@Repository,@Component,@Autowired,@Resource,@Scope-->
    <context:component-scan base-package="com.javasm"></context:component-scan>

</beans>

5.spring的di方式总结:

  • set注入
<property name="属性名" ref="注入的bean的id" value="值">
  • 自动装配
两个注解注解到成员变量上。
Autowired:先按照形参的类型进行注入,再按照名称查找
Resource:先按照形参的名称进行注入,找不到再按类型查找bean注入(常用)
  • 构造器注入

基于xml配置bean标签使用

<constructor-arg index|name ref|value></constructor-arg>
//实现类
public class SysuserController {
    private ISysuserService us;//getBean("us")
    private String str;
    private DataSource ds;
    
    public SysuserController(ISysuserService us,String str){
        this.us = us;
        this.str = str;
   }
    public void login(){
        System.out.println("SysuserController的login方法"+us+"--"+str);
        us.login("aa","aa");
    }
}

//spring.xml
    <bean id="userController" class="com.javasm.sys.controller.SysuserController">
        <constructor-arg index="0" ref="userService"></constructor-arg>    
        //index-索引   ref-引入bean对象
        <constructor-arg name="str" value="认真认真"></constructor-arg>     
        //name=名字     value-简单数据类型(就是java自带的那些)
    </bean>
  • 集合注入

基于xml配置bean标签使用

property标签,给属性赋值的时候,属性类型是Array,List,Map的时候,使用<array><list><map>
public class SysuserServiceImpl implements ISysuserService {
    private List<String> strList;
    private Integer[] intArray;
    private Map<String,Integer> mapCollection;

    public SysuserServiceImpl() {
        System.out.println("实例化SysuserServiceImpl");
    }

    public List<String> getStrList() {
        return strList;
    }

    public void setStrList(List<String> strList) {
        this.strList = strList;
    }

    public Integer[] getIntArray() {
        return intArray;
    }

    public void setIntArray(Integer[] intArray) {
        this.intArray = intArray;
    }

    public Map<String, Integer> getMapCollection() {
        return mapCollection;
    }

    public void setMapCollection(Map<String, Integer> mapCollection) {
        this.mapCollection = mapCollection;
    }
}
   <bean id="userService" class="com.javasm.sys.service.SysuserServiceImpl">
        <property name="intArray" >       //数组
            <array>
                <value>1</value>
                <value>3</value>
                <value>2</value>
            </array>
        </property>
        <property name="strList">        //List
            <list>
                <value>aaa</value>
                <value>bbb</value>
                <value>是的</value>
            </list>
        </property>
        <property name="mapCollection">   //map
            <map>
                <entry key="a" value="1"></entry>
                <entry key="b" value="2"></entry>
                <entry key="c" value="3"></entry>
            </map>
        </property>
    </bean>
  • 内部bean注入

基于xml配置bean标签使用

某个bean只会被引用一次,可以使用内部bean来注入。
//实现类
public class SysuserServiceImpl implements ISysuserService {
    private DataSource ds;

    public DataSource getDs() {
        return ds;
    }

    public void setDs(DataSource ds) {
        this.ds = ds;
    }

    public SysuserServiceImpl() {
        System.out.println("实例化SysuserServiceImpl");
    }
}


//spring,xml
<bean id="userService" class="com.javasm.sys.service.SysuserServiceImpl">
        <property name="ds">   
            //某个bean只会被引用一次,可以使用内部bean来注入。
            <bean class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
                <property name="url" value="jdbc:mysql://localhost:3306/720a"></property>
                <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
                <property name="username" value="root"></property>
                <property name="password" value="root"></property>
                <property name="initialSize" value="3"></property>
            </bean>
        </property>
    </bean>

6.xml补充标签

  • 加载XXX.properties
ignore-unresolvable:忽略暂时不可解析的配置${asdfa},应用在有多个.properties文件的时候

<?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: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
       https://www.springframework.org/schema/context/spring-context.xsd">

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

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="initialSize" value="${jdbc.initialSize}"></property>
    </bean>

</beans>

  • xml文件的嵌套
// 1.<import resource="dao.xml"></import>
//有dao.xml spring.xml两个  把dao.xml插入spring.xml中 到时候只加载spring.xml就可以了

<?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: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 https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.javasm"></context:component-scan>

    <import resource="dao.xml"></import>
</beans>


//2
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml","dao.xml");

7.spring的类配置环境

相对于xml配置,通过XXX.java一个配置类来配置bean,配置包扫描.

类配置在ssm项目中不在使用,是为了后面的springboot做准备。springboot提倡的零xml配置,通过类配置来实现。

@Configuration

@ComponentScan

@PropertySource

@Value

@Bean

//等价于xml配置文件
@Configuration
@ComponentScan("com.javasm")
// 把properties配置数据加载的spring容器,通过@Value来获取数据
//即别的类里也可以通过@Value来获取数据(就是把properties数据对象放进了容器里)
@PropertySource("jdbc.properties")
public class MyApplcationConfig {
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.driver}")
    private String driverClassName;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;
    @Value("${jdbc.initialSize}")
    private Integer initSize;

    @Override
    public String toString() {
        return "MyApplcationConfig{" +
                "url='" + url + '\'' +
                ", driverClassName='" + driverClassName + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", initSize='" + initSize + '\'' +
                '}';
    }

   //导入的DataSource类型  import javax.sql.DataSource;
   //方法名相当于"id"
    @Bean(initMethod = "init",destroyMethod = "close")
    public DataSource createDateSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(url);
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setUsername(username);
        druidDataSource.setPassword(password);
        druidDataSource.setInitialSize(initSize);
        return druidDataSource;
    }

    @Bean  //ds必须是这个bean(即传入的参数)    
    public IsDateService createUserService(DataSource ds){  对象
        IsDateService us = new IsDateServiceImpl();
        //这里是通过set方法注入  
        //也可以直接通过@Resource注解   //注意放对象进容器 注解跟这个bean二选一 
        ((IsDateServiceImpl) us).setDs(ds);
        return us;
    }
}
//@Service  二选一
public class IsDateServiceImpl implements IsDateService {
 //这里就可以直接通过@Value来获取数据   
//    @Value("${jdbc.url}")
//    private String url;
//    @Value("${jdbc.driver}")
//    private String driverClassName;
//    @Value("${jdbc.username}")
//    private String username;
//    @Value("${jdbc.password}")
//    private String password;
//    @Value("${jdbc.initialSize}")
//    private String initSize;
    
    @Resource   //注解方式
    private DataSource ds;

    @Override
    public String toString() {
        return "IsDateServiceImpl{" +
                "ds=" + ds +
                '}';
    }

    public DataSource getDs() {
        return ds;
    }
    //注入方式之一
    public void setDs(DataSource ds) {
        this.ds = ds;
    }
}
public class leiTest {
    @Test
    public void test1_getSysuserController(){
        //配置类对象  AnnotationConfigApplicationContext(配置类.class)
        ApplicationContext ann = new AnnotationConfigApplicationContext(MyApplcationConfig.class);
//        MyApplcationConfig bean = ann.getBean(MyApplcationConfig.class);
//        System.out.println(bean);

//        IsDateServiceImpl bean = ann.getBean(IsDateServiceImpl.class);
//        System.out.println(bean);

        DataSource bean = ann.getBean(DataSource.class);
        System.out.println(bean);

    }
}

8.spring的测试整合

通过注解来引入配置文件,容器初始化,同时把测试类也放容器,这样需要测试哪个bean,注入哪个bean
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
/**
 * 通过注解来引入配置文件,容器初始化,同时把测试类也放容器,这样需要测试哪个bean,注入哪个bean
 */
@RunWith(SpringJUnit4ClassRunner.class)          //初始化容器
@ContextConfiguration("classpath:spring.xml")    //加载配置文件
public class Test2_annotationConfig {
    @Resource                               //通过注解拿到数据
    private ISysuserService bean;                       
    @Resource
    private DataSource ds;
   
    @Test
    public void test1_initApplicaitonContext(){
        System.out.println(bean);                   //测试哪个需要注解拿到数据                                                
    }

    @Test
    public void test2_dataSource(){
        System.out.println(ds);
    }
}

9.aop面向切面

AOP:不是spring提出的概念,AOP是面向切面编程,是对OOP编程的一种补充。

Aspect(切面):指横切性关注点的抽象即为切面,它与类相似,只是两者的关注点不一样,类是对物体特征的抽象,而切面是横切性关注点的抽象.
Advice(通知):所谓通知是指拦截到joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知。
Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点,实际上joinpoint还可以是field或类构造器)
Pointcut(切入点):所谓切入点是指我们要对那些joinpoint进行拦截的定义。
Target(目标对象):代理的目标对象。
Weave(织入):指将aspects应用到target对象并导致proxy对象创建的过程称为织入。
切面aspect:抽取出来的非核心业务类。TimerService

通知advice:即切面类中的一个方法(前置通知,返回通知,异常通知,最终通知,环绕通知),start前置通知,end最终通知.
连接点方法joinPoint:即原核心业务方法。WechatPayServiceImp中的pay方法.
切入点pointcut:描述连接点的集合。
目标对象target:即连接点方法所在的对象,本质就是原生被代理对象。
织入weave:通过动态代理把切面中的通知方法动态的添加到连接点方法各个位置。

总结:把一个切面中的通知方法    织入    到     一个目标对象中的一个连接点方法的各个位置。
原生的动态代理的方式,dao层用jdbc的方式,去实现一套统一事务管理切面

10.spring中的aop使用

加入spring的aop的依赖jar包:

spring-aop,spring-aspect

cglib动态代理jar包

aspectj的三个核心包,独立aop组件jar包。spring使用该组件中的注解。

10.1基于注解

@Apsect.@Pointcut,@Before,@After,@AfterReturing,@AfterThrowing,@Around

开启aop注解识别。

环绕通知的方法返回值必须是Object,形参必须是ProceedingJoinpoint,该对象的proceed方法表示执行连接点方法。

//@Before  前置通知能够得到连接点基本信息类名,方法名,实参
//@After  最终通知不能得到 (啥也没有)
//@AfterReturning  能得到连接点方法返回值
//@AfterThrowing  异常通知中能得到异常信息   
//1 编写核心业务
//1.1编写接口
public interface IPayService {
    public boolean pay(String payUser,String payee,Double money);
}
//1.2编写方法
@Service("aliPay")
public class AliPayServiceImpl implements IPayService {
    //连接点方法
    @MyPointcut(aa = "阿里付款")   //用于
    @Override
    public boolean pay(String payUser, String payee, Double money) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(money<0)
            return false;
        System.out.println("支付宝付款实现:付款人:"+payUser);
        return true;
    }
}

@Service("wePay")
public class WechatPayServiceImpl implements IPayService {
    @MyPointcut(aa = "微信付款")
    @Override
    public boolean pay(String payUser, String payee, Double money) {
        try {
            Thread.sleep(1400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("微信付款实现:付款人:"+payUser);
        return true;
    }
}
//2  开启注解配置
<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--@Configuration,@Value,@PropertyResource,@Bean,@Controller,@Service,@Repository,@Autowried,@Resource-->
    <context:component-scan base-package="com.javasm"></context:component-scan>

    <!--aop相关注解的识别@Aspect.@Pointcut,@Before,@After,@AfterReturing,@AfterThrowing@Around-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
//3.写切面bean
//切面bean
@Component       //把这个放入容器       1
@Aspect          //告诉他是切面bean     2
@Order(1)        //如果有两个切面 按照这个顺序进行   
public class TimeService {
    private long start;

    //切入点(切入点表达式,两种写法:execution(),@annotation())    3
    @Pointcut("execution(boolean pay(..))")
    public void payPointcut(){}         //仅用于下面方法识别

    //前置通知                                                    4
    @Before("payPointcut()")
    public void begin() {
        System.out.println("------qian----");
        start = System.currentTimeMillis();
    }

    //最终通知                                                    5
    @After("payPointcut()")
    public void end() {
        System.out.println("-----hou-----");
        long l = System.currentTimeMillis();
        System.out.println("执行耗时:"+(l-start)+"mills");
    }
}
//4.补充
//annotation切入方法
@Retention(RetentionPolicy.RUNTIME)     //运行时注解
@Target({ElementType.METHOD})           
public @interface MyPointcut {
    String aa() default "";             //跟上面 @MyPointcut(aa = "")相对应
   // String cc() default "";             //跟上面 @MyPointcut(cc = "")相对应
}

//切面bean
@Component       //把这个放入容器       1
@Aspect          //告诉他是切面bean     2
@Order(2)
public class AllAdvice {
    //表示带MyPointcut的方法切入   更具体
    @Pointcut("@annotation(com.javasm.service.MyPointcut)") 
    public void pc(){}

    
//1.每个方法备注 
    //前置通知能够得到连接点基本信息类名,方法名,实参
    @Before("pc()")
    public void beforeAdvice(JoinPoint jp){
        System.out.println("al-----qian");
        Object[] args = jp.getArgs();//实参
        Object target = jp.getTarget();//目标对象
        String name = jp.getSignature().getName();//连接点方法名
        System.out.println("aLLAdvice前置通知"+target.getClass().getName()+"."+name+"-"+ Arrays.toString(args));
    }
    //最终通知不能得到
    @After("pc()")
    public void after(JoinPoint jp){
        System.out.println("最终通知");
    }
    //能得到连接点方法返回值
    @AfterReturning(pointcut = "pc()",returning = "o")
    public void afterReturning(JoinPoint jp,Object o){
        Object[] args = jp.getArgs();//实参
        Object target = jp.getTarget();//目标对象
        String name = jp.getSignature().getName();//连接点方法名

        System.out.println("返回通知"+target.getClass().getName()+"."+name+"-"+ Arrays.toString(args)+"-返回值:-"+o);
    }

    //异常通知中能得到异常信息
    @AfterThrowing(pointcut = "pc()",throwing = "e")
    public void afterThrowing(JoinPoint jp,Exception e){
        System.out.println("异常通知"+e.getMessage());
    }

    
    
//2.
//    ProceedingJoinPoint封装了连接点Method对象
    @Around("pc()")
    public Object aroundAdvice(ProceedingJoinPoint jp){
        Object proceed = null;
        try {
            Object[] args = jp.getArgs();//实参
            Object target = jp.getTarget();//目标对象
            String name = jp.getSignature().getName();//连接点方法名
            System.out.println("前置通知"+target.getClass().getName()+"."+name+"-"+ Arrays.toString(args));
            proceed = jp.proceed();//执行连接点方法
            System.out.println("返回"+proceed);
        } catch (Throwable throwable) {
            System.out.println("异常"+throwable.getMessage());
        }finally {
            System.out.println("最终");
        }
        return proceed;
    }
}
10.2基于xml配置.
//用这个  aop注解需要都去掉   要不然走两遍

<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="com.javasm"></context:component-scan>

    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    <!--配置bean-->
    <bean id="allAdvice" class="com.javasm.service.AllAdvice"></bean>
    <aop:config>
        <!--配置bean切面-->
        <aop:aspect ref="allAdvice">
            <!--配置表达式-->
            <aop:pointcut id="pc" expression="execution(* com.javasm.service.*.*(..))"></aop:pointcut>
            <!--配置通知-->
            <aop:before method="beforeAdvice" pointcut-ref="pc"></aop:before>
            <aop:after-returning method="afterReturning" pointcut-ref="pc" returning="o"></aop:after-returning>
            <aop:after-throwing method="afterThrowing" pointcut-ref="pc" throwing="e"></aop:after-throwing>
            <aop:after method="after" pointcut-ref="pc"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>

spring MVC

1.web开发常见模式:

  • 同步开发:每次请求客户端发起url,服务端解析并渲染页面(jsp),输出流把渲染的静态html返回客户端。

  • 半分离的异步开发:

    ​ 前端html代码与后端java代码仍然在一个项目中,一起部署到tomcat,用户先请求tomcat获取html页,客户端拿到html后,再发起ajax异步请求获取数据,客户端拿到数据后再通过前端技术来渲染动态html页(DOM操作)。

  • 全分离异步开发(前后端分离):

    ​ 用户先请求nginx获取html页,再发起针对tomcat服务器的异步请求,拿到数据,客户端动态渲染页面。

    ​ 前端html代码独立项目,独立部署nginx静态服务器;

    ​ 后端java代码独立项目,独立部署tomcat动态服务器(处理数据);

http协议:

请求头:content-type:application/x-www-form-urlencoded:前端数据格式key=value&key=value

​ content-type:application/json:前端数据格式:{key:value,key:value,key:value}

​ content-type:multipart-formdata:文件上传场景:字节流

​ http-method:请求方法:get、post、put、delete,options

​ use-agent:客户端信息(浏览器,移动端)

响应头:content-type:text/html

​ application/json

​ ​

第二阶段的servlet中request.getParameter()不能获取json数据,只能获取key=value&key=value

​ (这个处理前端发来json字符串数据)request.getInputStream(),读取输入流中的数据为String,后端得到json字符串,再通过fastjson或gson组件把json字符串转对象。

2.springmvc作用

springMVC的spring框架中的一个子框架,是web模块中的一个小组件。

对servlet进行封装,解决以下一些servlet开发中繁琐的代码。

  • 一个servlet只能够处理一件事情;
  • 繁琐的getParameter,繁琐的转型;
  • 返回json数据麻烦。response.getWriter().print(JSON.toJsonString(Object obj));

3.springMVC初了解

搭建springMVC环境
  • 引入spring核心jar,引入spring-web核心jar(spring-web;spring-webMVC),引入springMVC内部依赖的json组件包jackson(三个:core;databind;annotation)
  • 创建springMVC的xml配置文件.
<context:component-scan base-package="com.javasm"></context:component-scan>

    <!--开启mvc注解识别:@RequestMapping,@GetMapping,@PostMapping,@PutMaping,@DateTimeFormated.@RequestBody,@RequestParam,@ResponseBody-->
    <!--@JSONFormated-->
    <mvc:annotation-driven></mvc:annotation-driven>

    <!--jsp视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/page/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>

  • 在web.xml配置springmvc的前端控制器DispatcherServlet。

    两个作用:tomcat启动时加载spring的xml文件初始化容器;

    ​ 用户请求时,解析请求url,根据url去springmvc容器中找到对应的处理器对象。

  • 写springmvc风格的控制层对象controller(处理器对象)

例子:
//1.创建springMVC的xml配置文件
<?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:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="com.javasm"></context:component-scan>

    <!--开启mvc注解识别:
@RequestMapping,@GetMapping,@PostMapping,@PutMaping,@DateTimeFormated.@RequestBody,@RequestParam,@ResponseBody-->
    <!--@JSONFormated-->
     <!--选择描述最末尾是MVC的那个 别问为啥写这句  问就是我卡了一个晚上-->
    <mvc:annotation-driven></mvc:annotation-driven>   

    <!--jsp视图解析器   一般同步开发才用-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/page/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>
</beans>
//2.在web.xml配置springmvc的前端控制器DispatcherServlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--init.service,doGet,dopost-->
    <!--任何请求全部进入DispatcherServlet,解析url分发请求,加载指定的spring风格的xml配置文件,初始化spring容器-->
    <!--servlet什么时候实例化,用户第一次请求才实例化这个servlet,希望在tomcat启动时实例化DispatcherServlet-->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
         <!--初始化spring容器-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springMVC.xml</param-value>
        </init-param>
        <!--在tomcat启动时实例化DispatcherServle-->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!--"/"---任何请求全部进入DispatcherServlet --> 
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
//Tomcat现在是设置的deployment 那个项目名简化成"/"
//3.写springmvc风格的控制层对象controller
@Controller
public class SysuserHandler {
    //用来做url映射,把一个url字符串映射到一个处理器方法上
    //http://localhost:8080/hc 走这个方法
    @RequestMapping("hc")  
    public String hellomvc(){
        System.out.println("进入了hellomvc方法");
        //跳转到"/page/hello.jsp"
        return "hello";
    }
}
springMVC执行流程
  • tomcat启动:DispatcherServlet加载springmvc.xml,初始化XmlWebApplicationContext容器对象;

    解析@Controller注解的类里面的@RequestMapping注解,进行url映射(url字符串/hm–>HandlerMethod)

    –>Map<String,HandlerMethod>.put("/hm",hellomvc)

  • 用户请求:

    • dispatcherServlet接收请求,解析url字符串(/hm或/login),RequestMappingHandlerMapping查找处理器方法,把找到的方法返回dispatcherServlet
    • 在dispatcherServlet中,调用RequestMappingHandlerAdapter对象执行处理器方法,获取前端参数,解析封装,调用处理器方法,调用service,调用doa,得到方法的返回值,把返回值返回给dispatcherServlet;
    • 在dispatcherServlet中,调用视图解析器InternalResourceViewResolver得到视图路径。后端进行页面渲染,返回给前端。

4.接收前端参数

4.1 key=value格式

接收key=value&key=value格式的参数,content-type:application/x-www-form-urlencoded

加形参,底层通过反射,获取处理器方法的形参数组,根据形参名去getParameter获取数据。

可以通过@DateTimeFormated指定日期格式

可以通过@RequestParam注解指定参数默认值,该注解只能用在简单类型上,获取key=value数据.

@Controller
public class SysuserHandler {
    
    //@DateTimeFormat(pattern = "yyyy-MM-dd")
    //简单类型,springMVC底层根据形参名去getParameter,习惯写包装类对象。
    //http://localhost:8080/feiqi_addUser?uname=111&upwd=222&ubirthday=2019-11-11
    @RequestMapping("feiqi_addUser")
    public String feiqi_addUser(String uname,String upwd,Integer uscore,@DateTimeFormat(pattern = "yyyy-MM-dd") Date ubirthday){
        System.out.println("进入了feiqi_addUser方法"+uname+"---"+upwd+"---"+ubirthday);
        return "hello";
    }

    //底层判断形参不是简单类型(String,date.包装对象),反射得到对象的成员变量列表,根据成员变量名去getParameter
    //实体类对象如果有时间类型date 可以在实体类里直接注解
    //    @DateTimeFormat(pattern = "yyyy-MM-dd")
    //    private String createTime;

    //http://localhost:8080/add?loginUserr=111&uname=222&createTime=2019-11-11
    @RequestMapping("add")
    public String addUser(Sysuser suser){
        System.out.println("进入了add方法"+suser);
        System.out.println("进入了add方法"+suser.getUname()+"---"+suser.getCreateTime());
        System.out.println(loginUser);
        return "hello";
    }

    //直接给两个对象的成员变量赋值
    //http://localhost:8080/addUserAndRole?uname=fyt&upwd=123&rname=users
    //Sysuser{uid=null, uname='fyt', upwd='123'}--Sysrole{rid=null, rname='users'}
    //如果两个对象有相同的成员变量 这个变量赋值的时候两个对象都会被赋值
    //http://localhost:8080/addUserAndRole?uname=fyt&upwd=123&rid=333&rname=users
    //Sysuser{uid=null, uname='fyt', upwd='123', rid=333}--Sysrole{rid=333, rname='users'}
    //如果是对象.成员变量=?? 这样值给不上
    //http://localhost:8080/addUserAndRole?uname=fyt&upwd=123&srole.rname=users
    //Sysuser{uid=null, uname='fyt', upwd='123'}--Sysrole{rid=null, rname='null'}
    @RequestMapping("addUserAndRole")
    public String addUserAndRole(Sysuser suser, Sysrole srole){
        System.out.println("进入了addUserAndRole方法"+suser+"--"+srole);
        return "hello";
    }

    //Sysuser实体类里持有Srole
    //http://localhost:8080/addUserAndRole2?uname=fyt&upwd=123&srole.rname=users
    //Sysuser{uid=null, uname='fyt', upwd='123', srole=Sysrole{rid=null, rname='users'}}
    @RequestMapping("addUserAndRole2")
    public String addUserAndRole2(Sysuser suser){
        System.out.println("进入了addUserAndRole2方法"+suser);
        return "hello";
    }

    //给传入的参数默认值
    @RequestMapping("usersList")
    public String getUserList(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue ="10") Integer pageSize){
//        if(pageNum==null)pageNum=1;
//        if(pageSize==null)pageSize=10;
        System.out.println("进入了usersList方法"+pageNum+"--"+pageSize);
        //进入了usersList方法1--10
        return "hello";
    }
}
4.2 json格式

接收前端{key:value,key:value}json格式的参数,即请求头content-type:application/json

处理器方法的形参加注解@RequestBody注解

百分百异步开发

@Controller
public class SysuserHandler {
    //一个实体类对象往前端传   @RequestBody不能注解简单数据类型
    @PostMapping("json")
    public ResponseEntity addRole(@RequestBody Sysrole role){  //在这里直接注解
        System.out.println("addRole:"+role);
        role.setRid(100);
        return ResponseEntity.ok(role);
    }
  
    //多个对象往前端传
    //参数以json传输,不能在通过@RequestBody直接注解两个对象,需要注解到map
    //再通过BeanUtils来分数据到对象。
    @PostMapping("addUserAndRole")
    public ResponseEntity addRole(@RequestBody Map map){
        System.out.println("addRole:"+map);
        //数据全部在map中,有时需要把map中的数据分到不同的对象中,apache:commons-beanUtils组件
        Sysuser user = new Sysuser();  //其中一个实体类对象
        Sysrole role = new Sysrole();   //另一个实体类对象
        try {
            BeanUtils.populate(user,map);
            BeanUtils.populate(role,map);
        } catch (Exception e) {
            e.printStackTrace();
        }
        map.put("aa","dd");
        return ResponseEntity.ok(map);
    }
    
//    {
//        "uname": "fyt",
//        "upwd": "111",
//        "rname": "sss",
//          "aa": "dd"
//    }
}

5.同步开发中返回数据给前端

同步开发中:(服务器端渲染视图)request.setAttribute(key,value),在jsp页jstl+el。

​ springMVC中使用Model或者ModelAndView对象来把数据传输到视图层。

    //控制层 public class SysuserHandler代码
	//第一种:通过Mode
	@RequestMapping("login")
    public String loginPage(String uname, String upwd, Model m){
        System.out.println("进入了loginPage方法"+uname+"---"+upwd);
        m.addAttribute("modelDatas","model对象中的数据");//request.setAttribute()
        return "user/login";//   /page/user/login.jsp
    }

	//第二种:通过ModelAndView
    @RequestMapping("login")
    public ModelAndView loginPage(String uname, String upwd){
        System.out.println("进入了loginPage方法"+uname+"---"+upwd);
        ModelAndView mv = new ModelAndView();
        mv.setViewName("user/login");//视图名
        mv.addObject("modelDatas","modelAndView对象中的数据");//数据
        return mv;//   /page/user/login.jsp
    }


//login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
登录页面
<hr>
${modelDatas}   //使用后端传来的数据
</body>
</html>

6.异步请求返回json数据给前端

springMVC内部使用的jackson进行json处理。

  • 在处理器方法上加@ResponseBody注解,springMVC对方法返回值走json转换器(MappingJackson2HttpMessageConverter)进行转json字符串
  • 处理器方法的返回值类型改为ResponseEntity对象.
    • ResponseEntity.ok(数据)
    • new ResponseEntity(数据,响应头,状态码)主要用在下载,刷新token
@Controller
@RequestMapping("role")
public class SysroleHandler {

    //第一种方法
    @RequestMapping("add")
    @ResponseBody   //带这个注解,表示返回值不在走视图解析器,而是走json转换器,把对象转 json字符串
    public Sysrole addRole(Sysrole role){
        System.out.println("addRole:"+role);
        role.setRid(100);
        return role;
        //{"rid":100,"rname":null,"rdate":null}
    }

	//第二种方法(必须推荐使用)
    @RequestMapping("add2")
    public ResponseEntity addRole2( Sysrole role){
        System.out.println("addRole2:"+role);
        role.setRid(100);
        return ResponseEntity.ok(role);//该对象可以指定响应头,响应码,响应体

//        HttpHeaders headers = new HttpHeaders();
//        headers.add("token","adfadfasfd");//下载文件,前后端分离开发给前端返回token
//        return new ResponseEntity(role,null,HttpStatus.OK);
    }
}

7.如何限定请求方法:

  • 在RequestMapping注解进行url映射时,通过注解的属性method限定请求方法
@RequestMapping(path = "add",method = RequestMethod.POST) //注解的属性method限定请求方法
public ResponseEntity addRole(@RequestBody Sysrole role){
    .....................
}
  • 使用GetMapping,PostMapping,PutMapping,DeleteMapping进行url映射
@PostMapping("add")

8.乱码解决

get:tomcat8及以上版本不需要在server.xml中配置URIEncoding=UTF-8,useDefaultEncdoing=true。

post:加编码过滤器,CharacterEncodingFilter

//web,xml  post方法解决乱码加编码过滤器
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

9.如何获取servlet的核心对象

在处理器方法的形参上添加HttpServletRequest,HttpServletResponse,HttpSession。

如果需要获取ServletContext对象的话,通过request或session对象间接获取。

@Controller
public class SysuserHandler {
    //前端vue默认数据格式json,直接传后端
    @PostMapping("login2")
    public ResponseEntity loginPage2(@RequestBody Sysuser user, HttpSession session){
        //先将数值传进session
        session.setAttribute("loginUser",user);
        return ResponseEntity.ok("loginSuccess");
    }
    
    @RequestMapping("add")
    public String addUser(Sysuser suser,HttpSession session){
        System.out.println("进入了loginPage方法"+suser);
        //从session里取数值
        Object loginUser = session.getAttribute("loginUser");
        System.out.println(loginUser);
        return "user/login";//   /page/user/login.jsp
    }
}

11.springMVC的几个概念

  • 前端控制器(中央处理器):DispatcherServlet作用:
作用:初始化springMVC子容器;
接收请求,响应结果,相当于转发器,是springMVC的中央处理器。

处理器映射器:RequestMappingHandlerMapping:解析带有@Controller的类中的RequestMapping注解,进行url映射注册。当用户请求时,根据url得到对应的映射的处理器方法。

处理器适配器:RequestMappingHandlerAdapter:用来封装表单参数,执行处理器方法。

作用:找到Handler处理器方法对象后,由DispatcherServlet前端控制器调用HandlerAdapter来执行HandlerMethod,Handler处理器方法返回给适配器ModelAndView对象。

视图解析器:ViewResolver:设置视图的公共前缀和后缀,通过返回的ModelAndView对象得到完整的视图路径。

json转换器:Converter,MappingJackson2HttpMessageConverter,把对象转json字符串。

12. springMVC请求处理流程图

在这里插入图片描述

12.与DateTimeFormat结合使用的JsonFormated

通过DateTimeFormat注解把获取的日期字符串指定日期格式,转成Date对象

@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")   //把毫秒格式转换成yyyy-MM-dd HH:mm:ss
//jackson在序列化对象的时候,把日期序列化这种格式字符串,timezone = "GMT+8"
private Date rdate;

小总结:

  • 做url映射:

​ @RequestMapping注解,用来做url映射,把url映射一个处理器方法上。可以限定客户端提交请求的方法。

​ @GetMapping,@PostMapping,@PutMapping,@DeleteMapping

  • 用于接收参数:

​ @RequestParam,针对key=value格式的参数,注解到简单类型的形参上(String,Date,包装类,基本类型),可以设置默认值。

​ @RequestBody,针对{key:value}json格式的参数,注解到对象类型的形参上。也可以注解到Map上。

  • 用于返回数据:

​ @ResponseBody,表示处理器方法返回值不执行视图解析器,而是执行jackson消息转换器把返回值对象转 json字符串。

​ ResponseEntity:处理器方法的返回值类型,灵活的设置响应体,响应头,状态码.

​ ResponseEntity.ok(响应体数据)

​ new ResponseEntity(Obejct,HttpHeaders,HttpStatus)

# 13. 转发与重定向:不重要

同步开发中,处理器方法之间的转发与重定向。

在返回的String以forward:或redirect:开头.

@Controller
@RequestMapping("role")
public class SysroleHandler {

    @GetMapping("select")
    public String select(){
//        int a=1/0;
        System.out.println("查询角色列表");
        return "roles";
    }

    @GetMapping("add")
    public String add(){
        System.out.println("添加角色");
        return "redirect:select";      //return "forward:select";
        //forward是转发  页面还是http://localhost:8080/role/add
        //redirect是重定向 服务器发起二次请求 http://localhost:8080/role/select
    }
}                       

14. resturl:重要

同步开发中不能使用,前后端分离的项目中使用最多

rest是对url的写法,描述性状态转移,url中不含动词

注意: 1. 前端以post方法提交---- 对应 —>add方法

​ 2. 每个类里只能有一个post/get/put/delete方法提交的 resturl

url (以前)RequestMethoddataresturl(现在)RequestMethod
/user/addpost请求体:{k:v,k,v}/userPOST添加
/user/selectById?uid=1GET/user/1GET查询
/user/updateByIdPOST请求体:{k:v,k:v}/userPUT修改
/user/deleteById?uid=1GET/user/1DELETE删除
   //四种注解对应四种请求方式
   //请求方式:GET负责查询、POST负责添加、DELETE负责删除、PUT负责更新
   //注解后面没有"id" 就代表这四个方法

	@GetMapping("{uid}")
	//例: "/user/1"
	//@PathVariable("uid")可以在 通过@GetMapping("{uid}") 取得值 
    public ResponseEntity selectUserById(@PathVariable("uid") Integer uid){
        System.out.println("selectUserById:"+uid);
        Sysuser user = us.selectUserById(uid);
        return ResponseEntity.ok(user);
    }

    // /user
    @PostMapping
    public ResponseEntity addUser(@RequestBody Sysuser user){
        //添加用户,暂时不维护用户的角色
        boolean isok = us.addUser(user);
        return ResponseEntity.ok("suc");
    }

    @PutMapping
    public ResponseEntity updateUserById(Sysuser user){
        System.out.println("updateUserByID:"+user);
        boolean isok = us.addUser(user);
        return ResponseEntity.ok("suc");
    }

//    /user/1
    @DeleteMapping("{uid}")
    public ResponseEntity delUserById(@PathVariable("uid") Integer uid,@RequestBody Sysuser user){
        System.out.println("delUserById:"+uid);
        return ResponseEntity.ok("suc");
    }

注意点:

put和delete请求:如果请求体中的数据是key=value&key=value格式的话,收不到数据。可以通过配置FormContentFilter过滤器来接收数据。

   //web.xml中配置
	<filter>
        <filter-name>formContentFilter</filter-name>
        <filter-class>org.springframework.web.filter.FormContentFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>formContentFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

15. 静态资源处理

同步开发,半分离异步开发。(前端资源和后端代码在一个项目中)

在web目录下的前端资源无法直接访问。

为什么jsp能访问,也是因为tomcat,tomcat中有默认的serlvet:default,jsp,当访问XXX.jsp进入JspServlet中处理。当我们访问XXX.html,XXX.css等非jsp后缀的时候,进入DispatcherServlet,前端控制器解析url,找处理器对象,找不到,则404.

  • 解决方法1:在web.xml中配置defaultServlet
<servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.html</url-pattern>
        <url-pattern>*.css</url-pattern>
        <url-pattern>*.jpg</url-pattern>
        <url-pattern>*.png</url-pattern>
        <url-pattern>*.js</url-pattern>
    </servlet-mapping>

  • 解决方式2:springMVC中有标签自动配置defaultServlet,建议使用这种。
<!--所有的静态资源都走DefaultServlet, default-servlet-name="default"-->
<mvc:default-servlet-handler></mvc:default-servlet-handler>
  • 解决方式3:springMVC中通过静态资源映射器,对静态资源进行处理。
<!--mapping:uri以/static/ /page/开头的请求,都进入静态资源处理器。
该处理器对/static/css/a.css-->
<!--如果mapping="/a/**" (可随意更改) 地址就是/a/css/a.css -->
<mvc:resources mapping="/static/**" location="/static/"></mvc:resources>
<mvc:resources mapping="/page/**" location="/page/"></mvc:resources>

16. 异常处理:重要

把服务器端处理器方法内部产生的各种信息进行处理。

  • 在web.xml中对不同的错误码进行错误页面配置(只适合前后端未分离)。
	<error-page>
        <error-code>404</error-code>
        <location>/404.html</location>
    </error-page>
    <error-page>
        <error-code>500</error-code>
        <location>/500.html</location>
    </error-page>

缺点:提示信息无法动态定义。只能够返回页面,在前端分离的情况下,不能使用。

  • 局部异常处理:只对一个处理器类中的异常生效。
//只对该类写了这个的有效
//也可以返回页面 返回ModelAndView
@ExceptionHandler(Exception.class)
    public ResponseEntity doException(Exception e){
        String message = e.getMessage();
        Map<String,String> datas = new HashMap<>();
        datas.put("msg",message);
        datas.put("date",System.currentTimeMillis()+"");
        return ResponseEntity.ok(datas);
    }

缺点:范围太小,只对一个类有效

  • 全局异常处理:对所有处理器类中的异常生效。
//异常处理类
public class MyExceptionHandler implements HandlerExceptionResolver {

    //适合于同步开发,当出现异常,转到一个自定义的异常页。
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        HandlerMethod hm = (HandlerMethod)o;
        Method method = hm.getMethod();
        String clzName = method.getDeclaringClass().getName();
        String methodName = method.getName();
        ModelAndView mv = new ModelAndView();
        mv.setViewName("error");                   //转到error页面
        mv.addObject("msg",e.getMessage());
        mv.addObject("date",System.currentTimeMillis()+"");

        return mv;
    }

缺点:只能够同步开发中使用。异步开发中不支持。

  • 全局统一异常处理(真正使用):

    项目中使用,在局部异常处理的基础上结合着@ControllerAdvice注解一起使用

前端程序员调用后端接口,后端需要把执行结果返回前端(数字状态码,中文文字信息,数据)。

状态码枚举类;StatusBean;ResponseBean;自定义异常;定义全局的异常处理类;

//1.创建枚举
public enum StatusCode {
    OPS_SUC(20000,"操作成功"),
    OPS_ERROR(50001,"操作失败"),
    PWD_ERROR(50002,"密码错误"),
    UNAME_ERROR(50003,"用户名未注册"),
    ;
    private Integer code;
    private String msg;

    StatusCode(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public Integer getCode() {return code;}
    public void setCode(Integer code) {this.code = code;}
    public String getMsg() {return msg;}
    public void setMsg(String msg) {this.msg = msg;}
}

//2.用于返回不带数据的异常信息类
public class StatusBean {
    private Integer code;
    private String msg;

    public StatusBean(StatusCode sc) {
        this.code =sc.getCode();
        this.msg = sc.getMsg();
    }

    public Integer getCode() {return code;}
    public void setCode(Integer code) {this.code = code;}
    public String getMsg() {return msg;}
    public void setMsg(String msg) {this.msg = msg;}
}

//3.用于返回带数据的异常信息类
public class ResponseBean extends StatusBean{
    private Object data;

    public ResponseBean(StatusCode sc,Object data) {
        super(sc);
        this.data = data;
    }

    public Object getData() { return data; }
    public void setData(Object data) {this.data = data;}
}
//5.定义一个每个项目都会创建的属于项目的异常
//用于一些非正确操作来抛出异常 返回枚举数据
public class MvcException extends RuntimeException {
    private StatusCode sc;
    
    public MvcException(StatusCode sc) {this.sc=sc;}
    public StatusCode getSc() {return sc;}
    public void setSc(StatusCode sc) {this.sc = sc;}
}

//6.配置全局的异常发生后的处理
//处理器增强注解,对所有的RequestMapping生效。
@ControllerAdvice
public class MyGlobExceptionHandler {
    //Exception发生类型的异常处理
    @ExceptionHandler(Exception.class)
    public ResponseEntity doException(Exception e){
        return ResponseEntity.ok(new StatusBean(StatusCode.OPS_ERROR));
    }
	//MvcException发生类型的异常处理
    @ExceptionHandler(MvcException.class)
    public ResponseEntity doException(MvcException e){
        return ResponseEntity.ok(new StatusBean(e.getSc()));
    }
}
@Controller
@RequestMapping("user")
public class SysuserHandler {
    @Resource
    private ISysuserService us;

    @GetMapping("login")
    public ResponseEntity login(Sysuser user){
        //判断用户名是否已注册
        Sysuser isRegisuser = us.isRegisUser(user.getUname());
        if(isRegisuser==null){
            //非正确操作
            throw new MvcException(StatusCode.UNAME_ERROR);
        }
        //密码校验执行登录
        boolean isLogin = isRegisuser.getUpwd().equals(user.getUpwd());
        if(!isLogin){
            //非正确操作
            throw new MvcException(StatusCode.PWD_ERROR);
        }
        return ResponseEntity.ok(new ResponseBean(StatusCode.OPS_SUC,isRegisuser));
    }
}

17. 文件上传:重要

springMVC的文件上传仍然基于apache common-fileupload。

  • 添加fileupload;io两个文件上传的jar包;
  • 在springmvc.xml中配置文件上传解析对象。
 <!--文件上传解析器-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="defaultEncoding" value="UTF-8"></property><!--上传中文文件名-->
        <property name="maxUploadSize" value="50000000"></property><!--上传文件最大大小-->
        <property name="maxInMemorySize" value="10000000"></property><!--临时文件的存储域-->
        <property name="uploadTempDir" value="/upload/tmp"></property><!--临时文件存储目录-->
    </bean>
  • 写文件上传处理器
@Controller
public class FileHandler {
    //文件上传
    @PostMapping("upload")
    //形参传前端表单参数名  MultipartFile-文件名 大小 输入流都在这里放着
    public ResponseEntity doupload(MultipartFile ifile, HttpServletRequest req){
        String name = ifile.getName();//参数名
        String fileName = ifile.getOriginalFilename();//文件名
        long size = ifile.getSize();//文件大小
        String realPath = req.getServletContext().getRealPath("/");   //得到根路径
        String savePath = "/upload/"+fileName;
        String saveFile = realPath+savePath;        //项目最终上传文件的路径位置

        try {
            byte[] bytes = ifile.getBytes();//文件内容,适合于小文件
//            InputStream inputStream = ifile.getInputStream();//文件内容,适合于大文件
            FileUtils.writeByteArrayToFile(new File(saveFile),bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        Map<String,String> map = new HashMap<>();
        map.put("SAVE_PATH",savePath);// /upload/aaaa.md
        map.put("REAL_NAME",fileName);
        map.put("UPLOAD_TIME",DateUtils.getCurrentTime());   //传给前端上传时间
        return ResponseEntity.ok(new ResponseBean(StatusCode.OPS_SUC,map));
        //上传成功到out里  E:\code\Java\javaSpringa\out\artifacts\day0915mvc_war_exploded\upload
    }

    
    //同步文件下载,前端不能使用ajax或axios异步调用。
    @GetMapping("down")
    public ResponseEntity download(String path,String realName,HttpServletRequest req){
        System.out.println(path);
        String realPath = req.getServletContext().getRealPath("/");
        String absPath = realPath+path;
        //想把这个文件读取到内存,byte[]
        byte[] bytes = null;
        HttpHeaders headers = new HttpHeaders();
        try {
            bytes = FileUtils.readFileToByteArray(new File(absPath));
            //需要指定响应头  可百度header("Content-Disposition: attachment; filename=abc.txt"); //指定文件名
            //Content-type自动会找不需要设置
            headers.add("Content-Disposition","attachment;filename="+URLEncoder.encode(realName,"UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ResponseEntity(bytes,headers,HttpStatus.OK);
    }
}


//时间工具类
public class DateUtils {
    public static String getCurrentTime(){
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return df.format(new Date());
    }
}

可用postman测试

在这里插入图片描述

18. 拦截器:interceptor

​ 类似于过滤器filter,拦截器是框架中的概念。

1.登陆拦截器是个项目中都会使用。2.权限拦截器

  • 编写拦截器类
public class LoginInterceptor implements HandlerInterceptor {

    //前拦截  只重写这个就行了
    @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      
    	//一旦跨域了 拦截器里需要 对预检请求放行
        String method = request.getMethod();
        if("OPTIONS".equals(method))
            return true;

        HttpSession session = request.getSession();
        Object login_user = session.getAttribute("LOGIN_USER");
        if (login_user != null) {
            return true;
        }
        throw new MvcException(StatusEnum.NO_LOGIN);
    }
}
  • 配置拦截器
  <!--springmvc.xml-->
	<mvc:interceptors>
        <mvc:interceptor>
            <!--拦截路径-->
            <mvc:mapping path="/**"/>
            <!--忽略路径-->
            <mvc:exclude-mapping path="/u/login"></mvc:exclude-mapping>
            <!--拦截器类-->
            <bean class="com.javasm.common.interceptor.LoginInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>

19. 跨域处理:

发起的请求与当前页面所在服务, 协议,ip,端口三者只要有一个不同,违反了浏览器的同源策略,造成跨域问题。

需要在服务器端配置响应头,告诉浏览器服务端允许这个客户端的请求:

响应头需要配置如下信息:

​ 允许的域:http://127.0.0.1:8848

​ 允许的请求头:

​ 允许的请求方法:GET/POST/PUT/DELETE/OPTIONS预检.

​ 是否允许请求携带cookie信息:

​ 可以暴露给客户端的响应头:

跨域产生的问题:

问题1:

请求不能正常响应。因为浏览器的同源保护(推荐 springmvc.xml中配置全局跨域)

方法1:@CrossOrigin注解

该注解添加到在处理器方法或类上。

在这里插入图片描述

​ 缺点:只能够针对当个类处理。

* 方法2:springMVC的全局配置
   //spring,xml
	<mvc:cors>
        <mvc:mapping path="/**" allowed-origins="http://127.0.0.1:8848" allowed-methods="*" allowed-headers="*"/>
    </mvc:cors>
方法3:CrosFilter过滤器
<!-- 这个配置需要放到Spring的配置文件中,不能放到SpringMVC的配置文件,因为SpringMVC的加载是基于Servlet,它是晚于Filter的 -->
<bean id="corsFilter" class="org.springframework.web.filter.CorsFilter">
    <constructor-arg name="configSource">
        <bean class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
            <property name="corsConfigurations">
                <map>
                    <entry key="/**">
                        <bean class="org.springframework.web.cors.CorsConfiguration">
                            <property name="allowCredentials" value="true"/>
                            <property name="allowedMethods">
                                <list>
                                    <value>GET</value>
                                    <value>POST</value>
                                    <value>HEAD</value>
                                    <value>PUT</value>
                                     <value>Delete</value>
                                    <value>OPTIONS</value>
                                </list>
                            </property>
                            <property name="allowedHeaders" value="*"/>
                            <property name="allowedOrigins" value="*"/>
                        </bean>
                    </entry>
                </map>
            </property>
        </bean>
        </constructor-arg>
</bean>
<!-- web.xml
由于CorsFilter跟通常的Filter不一样,Spring对其做了很多改造,所以加载的方式要使用DelegatingFilterProxy,通过Spring的方式把它放到容器中
-->
<filter>
    <filter-name>myCorsFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetBeanName</param-name>
        <param-value>corsFilter</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>myCorsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
方法4:自定义跨域处理过滤器
public class MyCrosFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
         try {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;

            // 预检请求,请求头Origin是客户端地址,要求跨域头不能是*
            String origin = httpRequest.getHeader("Origin");
            if (origin == null) {
                httpResponse.addHeader("Access-Control-Allow-Origin", "*");
            } else {
                httpResponse.addHeader("Access-Control-Allow-Origin", origin);
            }
            httpResponse.addHeader("Access-Control-Allow-Headers",
                    "Origin, x-requested-with, Content-Type, Accept,X-Cookie,token");
//httpResponse.addHeader("Access-Control-Expose-Headers", "token");

            httpResponse.addHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.addHeader("Access-Control-Allow-Methods", "GET,POST,PUT,OPTIONS,DELETE");
            //预检请求,直接通过。
            if (httpRequest.getMethod().equals("OPTIONS")) {
                httpResponse.setStatus(HttpServletResponse.SC_OK);
                return;
            }
            chain.doFilter(request, response);
        } catch (Exception e) {
            throw e;
        }
    }
    @Override
        public void destroy() {
    }
}
方法5:springboot的解决方式:
@Configuration
public class MyConfiguration {

 //springmvc中 没有FilterRegistrationBean这个类
	@Bean
	public FilterRegistrationBean这个类 corsFilter() {
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		CorsConfiguration config = new CorsConfiguration();
		config.setAllowCredentials(true);
		config.addAllowedOrigin("http://domain1.com");
		config.addAllowedHeader("*");
		config.addAllowedMethod("*");
		source.registerCorsConfiguration("/**", config);
		FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
		bean.setOrder(0);
		return bean;
	}
}
方法6:其他方案(了解)

​ 基于nginx做url代理;未学习nginx,暂不学习。

​ 基于node的一个包http-proxy-middleware,基于node组件启动一个后端程序做代理,把所有程序转发到服务器。(属于前端技能)。

问题2:

httpSession保存用户会话信息:每次请求服务器都会创建新的HttpSession对象,无法做到多次请求之间共享session中数据。

​ 解决方法:前端axios请求配置withCredentials=true,表示请求时携带cookie

服务端跨越配置中允许接收客户端cookie,allow-credentials="true"
如果使用chrome发现配置后仍然不行,开发中暂时先chrome://flags/,把samesite禁用了

仍然存在的问题:
tomcat分布式部署的时候,多服务器之间的session共享问题(通过spring-session,结合redis使用)。
chrome浏览器的samesite同站点默认配置lax,不保存第三方的cookie问题。		
//vue
//base.js
axios.defaults.baseURL = 'http://localhost:8080';  //这里省略axios网址的书写
axios.defaults.withCredentials = true;      //客户端请求仍然携带cookie信息。

//axios前拦截
axios.interceptors.request.use(function (config) {
	//前端进行登录校验,前端存储用户的登录信息。
    return config;
  }, function (error) {
    return Promise.reject(error);
});

//axios后拦截
axios.interceptors.response.use(function (response) {
	//服务端返回的错误状态码,统一处理
	//token的刷新重置。
	//统一获取数据
    return response.data;
  }, function (error) {
    return Promise.reject(error);
  });

//
methods: {
            doLogin() {
                //这里axios网址前缀就是http://localhost:8080;了
                axios.post('/u/login',this.loginForm).then(resp => {
                    // debugger;
                    if(resp.code==50004){
                        this.showLoginMsg = true;
                    }else{
                        debugger;
                        this.showLoginMsg = false;
                        localStorage.setItem("loginuser",resp.data);
                        location.href="main.html";
                    }
                });
            }
}
//后端部分
//跨域这里加一句 allow-credentials="true" 允许请求携带cookie
	<mvc:cors>
        <mvc:mapping path="/**" allowed-origins="http://127.0.0.1:8848" allowed-methods="*" allowed-headers="*" allow-credentials="true"/>
    </mvc:cors>

​ 解决方法2:JWT组件,token工具包

	服务端不保存会话状态信息,当用户登陆,服务端生成token字符串,返回客户端保存localStorage或cookie。每次请求都携带token信息在请求头中,发送服务端,服务端校验是否合法请求。
	 依赖组件:auto0或jjwt,两者都可以用来生成token。
	 缺点:token过长,效率低;服务端无法控制token强制失效,不安全;需要自己实现token刷新。

注意点:

在跨域的情况下,浏览器对POST,PUT,DELETE三种请求,会先向服务器发送预检请求,(检查服务端是否支持客户端的本次请求).

20. aop事务切面

定义出事务管理器对象。

定义事务切面,切入点表达式使用注解方式。

//service
public interface ISysuserService {
    public boolean addUsers() throws Exception;
    public boolean delUserByID(Integer uid);
}

@Service
public class SysuserServiceImpl implements ISysuserService {
    @Resource
    private ISysuseDao ud;

    //addUsers()执行多次添加在一个事务里
    @TransAnno   //表示该方法进行事务管理
    @Override
    public boolean addUsers() throws Exception {
        Sysuser u = new Sysuser("aaa","aaa");
        ud.addUser(u);
        Sysuser u1 = new Sysuser("bbd","bbb");
        ud.addUser(u1);
        return true;
    }

    @Override
    public boolean delUserByID(Integer uid) {
        ud.delUser(uid);
        return false;
    }
}
//dao
public interface ISysuseDao {
    public Boolean addUser(Sysuser u) throws Exception;
    void delUser(Integer uid);
}

@Repository
public class SysuserDaoImpl implements ISysuseDao {

    @Resource
    private CurrentConnection cc;

    @Override
    public Boolean addUser(Sysuser u) throws Exception{
            PreparedStatement pst = cc.getCurrentConnection().prepareStatement("insert into sysuser(uname,upwd) values(?,?)");
            pst.setString(1,u.getUname());
            pst.setString(2,u.getUpwd());
            return pst.execute();
    }

    @Override
    public void delUser(Integer uid) {
        try {
            Connection currentConnection = cc.getCurrentConnection();
            PreparedStatement pst = currentConnection.prepareStatement("delete from sysuser where uid=?");
            pst.setInt(1,uid);
            pst.execute();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
//tm
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TransAnno {
}

@Component
@Aspect
public class TmAspect {

    @Resource
    private MyTransactionManager tm;

    //不用另一种方法是因为这个具体
    //不可能每个方法都要事务管理
    @Pointcut("@annotation(com.javasm.tm.TransAnno)")  
    public void tmpc(){}

    @Pointcut("execution(* com.javasm.service.*.*(..))")
    public void all(){}

    @After("all()")
    public void closeConnection(){
        tm.close();
    }

    @Around("tmpc()")
    public Object execute(ProceedingJoinPoint jp){
        Object proceed = null;
        try {
            tm.openConnection();
            tm.beginTransaction();
            proceed = jp.proceed();
            tm.commit();
        } catch (Throwable throwable) {
            tm.rollback();
        }finally {
            tm.close();
        }
        return proceed;
    }


}
@Component
public class MyTransactionManager {

    @Resource
    private CurrentConnection cc;

    public void openConnection(){
        cc.openConneciton();
    }

    public void beginTransaction(){
        Connection currentConnection = cc.getCurrentConnection();
        try {
            currentConnection.setAutoCommit(false);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void commit(){
        Connection currentConnection = cc.getCurrentConnection();
        try {
            currentConnection.commit();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void rollback(){
        Connection currentConnection = cc.getCurrentConnection();
        try {
            currentConnection.rollback();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public void close(){
        Connection currentConnection = cc.getCurrentConnection();
        try {
            if(currentConnection!=null){
                currentConnection.close();
                currentConnection=null;
                cc.removeThreadLocal();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
@Component
public class CurrentConnection {
    //线程变量ThreadLocal,为每个独立的线程创建一个变量副本。set/get
    private ThreadLocal<Connection> conn=new ThreadLocal<>();

    @Resource
    private DataSource ds;

    public void openConneciton(){
        try {
            Connection connection = ds.getConnection();
            conn.set(connection);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public Connection getCurrentConnection(){
        Connection connection = conn.get();
        //如果没有事务帮我打开 那就自己打开
        if(connection==null){
            try {
                connection = ds.getConnection();
                //如果没有事务打开
                //这个在后面tm.close()事务中用于connection关闭
                conn.set(connection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return connection;
    }

    public void removeThreadLocal() {
        conn.remove();
    }
}
//spring.xml

<?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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:component-scan base-package="com.javasm"></context:component-scan>

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

    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

</beans>
//test
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestTransaction {

    @Resource
    private ISysuserService us;

    @Test
    public void addUsers() throws Exception {
        us.addUsers();
    }

    @Test
    public void delUser() throws Exception {
        us.delUserByID(30);
    }
}

其他

Dom4j解析xml:核心对象:SAXReader

jdk子代的Properties对象解析.properties:new Properties().load()

spring的xml配置文件的方式是在ssm整合框架中使用。

spring的类配置的方式是在springboot框架中使用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值