• 面试阿里,美团,京东都会被问到的Spring ,从基础到源码帮你全搞定


    1 前言

    1. Spring是一个轻量级开源框架,它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架。
    2. Spring是众多优秀设计模式的组合(工厂、单例、代理、适配器、包装器、观察者、模板、策略)。
    3. Spring的用途不仅限于服务器端的开发。从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。
    4. Spring并未替代现有框架产品,而是将众多框架进行有机整合,简化企业级开发,俗称"胶水框架"。

    2 Spring架构组成

    Spring架构由诸多模块组成,可分类为

    • 核心技术:依赖注入,事件,资源,i18n,验证,数据绑定,类型转换,SpEL,AOP。
    • 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。
    • 数据访问:事务,DAO支持,JDBC,ORM,封送XML。
    • Spring MVC和 Spring WebFlux Web框架。
    • 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
    • 语言:Kotlin,Groovy,动态语言。
    • List item

    Spring架构组成如下图

    3 Spring环境搭建

    3.1 pom.xml中引入Spring常用依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation=
             "http://maven.apache.org/POM/4.0.0 
              http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.qf</groupId>
        <artifactId>hello-spring</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <!-- Spring常用依赖 -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.1.6.RELEASE</version>
            </dependency>
        </dependencies>
    </project>
    
    

    3.2 创建Spring配置文件

    命名无限制,约定俗成命名有:spring-context.xml、applicationContext.xml、beans.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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    </beans>
    
    

    4 Spring工厂编码(入门程序)

    4.1 定义目标Bean类型

    public class MyClass{
        public void show(){
            System.out.println("HelloWorld");
        }
    }
    
    

    4.2spring-context.xml中的< beans >内部配置bean

    在spring-context.xml中配置MyClass的bean后,当项目启动时,spring容器会自动创建MyClass实例,这个实例名字叫mc

    <!-- 配置实例(id:“唯一标识”  class="需要被创建的目标对象全限定名") -->
    <bean id="mc" class="com.qf.spring.part1.factory.MyClass" />
    
    

    测试代码

    public class TestFactory{
        /**
         * 程序中的对象都交由Spring的ApplicationContext工厂进行创建。
         */
        public static void main(String[] args){
            //1. 读取配置文件中所需创建的bean对象,并获得工厂对象
            ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-context.xml");
            //2. 通过id获取bean对象
    		MyClass mc = (MyClass) ctx.getBean("mc");
            //3. 使用对象
    		mc.show();
        }
    }
    
    

    5 IoC(Inversion of Control )控制反转

    控制反转是Spring框架的核心,所谓控制反转就是应用本身不负责依赖对象的创建及维护,依赖对象的创建及维护是由外部容器负责的, 这样控制权就由应用转移到了外部容器,控制权的转移就是所谓反转。这样就由之前的自己创建依赖对象,变为由spring容器创建。(变主动为被动,即反转)。控制反转解决了具有依赖关系的组件之间的强耦合,使得项目形态更加稳健。

    5.1 项目中强耦合问题

    public class UserDAOImpl implements UserDAO{....}
    
    
    public class UserServiceImpl implements UserService {
        // 通过传统的new方式强耦合了UserDAOImpl!!!,使得UserServiceImpl变得不稳健!!
        private UserDAO userDAO= new UserDAOImpl();
        @Override
        public User queryUser() {
            return userDAO.queryUser();
        }
        ....
    }
    
    

    5.2 解决方案

    // 不引用任何一个具体的组件(实现类),在需要其他组件的位置预留存取值入口(set/get)
    public class UserServiceImpl implements UserService {
        // !!!不再耦合任何DAO实现!!!,消除不稳健因素!!
        private UserDAO userDAO;
        // 为userDAO定义set/get,允许userDAO属性接收spring赋值
        //Getters And Setters
        @Override
        public User queryUser() {
            return userDAO.queryUser();
        }
        ....
    }
    
    

    在spring配置文件中配置UserDAO和UserService对应的bean

    <bean id="userDAO" class="com.qf.spring.part1.injection.UserDaoImpl"></bean>
    <!-- UserServiceImpl组件 -->
    <bean id="userService" class="com.qf.spring.part1.injection.UserServiceImpl">
        <!-- 由spring为userDAO属性赋值,值为id="userDAO"的bean -->
        <property name="userDAO" ref="userDAO"/>
    </bean>
    
    

    6 DI(Dependency Injection)依赖注入

    6.1 概念

    在Spring创建对象的同时,为其属性赋值,称之为依赖注入,注入方式主要有以下2种

    • 构造函数注入
    • Setter方法注入

    6.2 Setter方法注入

    创建对象时,Spring工厂会通过Setter方法为对象的属性赋值。

    6.2.1 定义目标Bean类型

    public class User {
        private Integer id;
        private String password;
        private String sex;
        private Integer age;
        private Date bornDate;
        private String[] hobbys;
        private Set<String> phones;
        private List<String> names;
        private Map<String,String> countries;
        private Properties files;
        //Getters And Setters
    }
    
    

    6.2.2 基本类型 + 字符串类型 + 日期类型

    <bean id="u1" class="com.qf.spring.part1.injection.User">
        <!--base field-->
        <property name="id" value="1001" />
        <property name="password" value="123456" />
        <property name="sex" value="male" />
        <property name="age" value="20" />
        <property name="bornDate" value="1990/1/1" /><!--注意格式"/"-->
    </bean>
    
    

    6.2.3 容器类型(list,set,map,Properties)

    <bean id="u1" class="com.qf.spring.part1.injection.User">	
    	<!--Array-->
        <property name="hobbys">
            <array>
                <value>Run</value>
                <value>Swim</value>
                <value>Climb</value>
            </array>
        </property>
    
        <!--Set-->
        <property name="phones">
            <set>
                <value>13777777777</value>
                <value>13888888888</value>
                <value>13999999999</value>
            </set>
        </property>
    
        <!--List-->
        <property name="names">
            <list>
                <value>tom</value>
                <value>jack</value>
                <value>marry</value>
            </list>
        </property>
    
        <!--Map-->
        <property name="countries">
            <map>
                <entry key="CN" value="China" />
                <entry key="US" value="America" />
                <entry key="KR" value="Korea" />
            </map>
        </property>
    
        <!--Properties-->
        <property name="files">
            <props>
                <prop key="first">One</prop>
                <prop key="second">Two</prop>
                <prop key="third">Three</prop>
            </props>
        </property>
    </bean>
    
    

    6.2.4 自定义类型

    <!--次要bean,被作为属性-->
    <bean id="addr" class="com.qf.spring.part1.injection.Address">
        <property name="position" value="北京市海淀区" />
        <property name="zipCode" value="100001" />
    </bean>
    
    <!--主要bean,操作的主体-->
    <bean id="u2" class="com.qf.spring.part1.injection.User">
        <property name="address" ref="addr" /><!--address属性引用addr对象-->
    </bean>
    
    

    6.3 构造注入

    创建对象时,Spring工厂会通过构造方法为对象的属性赋值。

    6.3.1 定义目标Bean类型

    public class Student {
        private Integer id;
        private String name;
        private String sex;
        private Integer age;
    
        //Constructors
      	public Student(Integer id , String name , String sex , Integer age){
          	this.id = id;
        	this.name = name;
      	    this.sex = sex;
    	    this.age = age;
        }
    }
    
    

    6.3.2 注入

     <!--构造注入-->
    <bean id="u3" class="com.qf.zcg.spring.day1.t2.ioc.Student">
        <constructor-arg name="id" value="1234" /> <!-- 除标签名称有变化,其他均和Set注入一致 -->
        <constructor-arg name="name" value="tom" />
        <constructor-arg name="age" value="20" />
        <constructor-arg name="sex" value="male" />
    </bean>
    
    

    7 Spring工厂特性

    7.1 饿汉式创建优势

    工厂创建之后,会将Spring配置文件中的所有对象都创建完成(饿汉式),提高程序运行效率,避免多次IO,减少对象创建时间。(概念接近连接池,一次性创建好,使用时直接获取)

    7.2 生命周期方法

    • 自定义初始化方法:添加“init-method”属性,Spring则会在创建对象之后,调用此方法。
    • 自定义销毁方法:添加“destroy-method”属性,Spring则会在销毁对象之前,调用此方法。
    • 销毁:工厂的close()方法被调用之后,Spring会毁掉所有已创建的单例对象。
    • 分类:Singleton对象由Spring容器销毁、Prototype对象由JVM销毁。

    7.3 生命周期注解

    import javax.annotation.PostConstruct;
    import javax.annotation.PreDestroy;
    
    @PostConstruct //初始化 
    public void init(){
        System.out.println("init method executed");
    }
    
    @PreDestroy //销毁
    public void destroy(){
        System.out.println("destroy method executed");
    }
    
    

    7.4 生命周期阶段

    单例bean:singleton

    随工厂启动创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》随工厂关闭销毁

    多例bean:prototype

    被使用时创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》JVM垃圾回收销毁

    8 代理设计模式

    8.1 概念

    将核心功能与辅助功能(事务、日志、性能监控代码)分离,达到核心业务功能更纯粹、辅助业务功能可复用。

    8.2 静态代理设计模式

    通过代理类的对象,为原始类的对象(目标类的对象)添加辅助功能,更容易更换代理实现类、利于维护。

    • 代理类 = 实现原始类相同接口 + 添加辅助功能 + 调用原始类的业务方法。
    • 静态代理的问题
      • 代理类数量过多,不利于项目的管理。
      • 多个代理类的辅助功能代码冗余,修改时,维护性差。

    8.3 动态代理设计模式

    8.3.1 JDK动态代理实现(基于接口)

    //目标
    final OrderService os = new OrderServiceImpl();
    //额外功能
    InvocationHandler handler = new InvocationHandler(){//1.设置回调函数(额外功能代码)
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            System.out.println("start...");
            method.invoke(os, args);
             System.out.println("end...");
            return null;
        }
    };
    //2.创建动态代理类
    Object proxyObj = Proxy.newProxyInstance(ClassLoader , Interfaces , InvocationHandler);
    
    

    8.3.2 CGlib动态代理实现(基于继承)

    final OrderService os = new OrderServiceImpl();
    Enhancer cnh = new Enhancer();//1.创建字节码曾强对象
    enh.setSuperclass(os.getClass());//2.设置父类(等价于实现原始类接口)
    enh.setCallback(new InvocationHandler(){//3.设置回调函数(额外功能代码)
        @Override
        public Object invoke(Object proxy , Method method, Object[] args) throws Throwable{
            System.out.println("start...");
            Object ret = method.invoke(os,args);
            System.out.println("end...");
            return ret;
        }
    });
    OrderService proxy = (OrderService)enh.create();//4.创建动态代理类
    proxy,createOrder();
    
    

    9 面向切面编程

    9.1 概念

    AOP(Aspect Oriented Programming),即面向切面编程,利用一种称为"横切"的技术,剖开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

    通俗的概念来讲,所谓的面向切面编程就是针对被代理对象的方法在某个特定的执行时机(方法调用之前、方法调用之后、方法抛出异常),做出一些额外的横向的处理。好处在于:1. 可以在不修改原有代码基础上横向扩展我们的内容;2. 将一些方法中的通用逻辑进行统一化的处理。

    OOP(面向对象编程)和AOP(面向切面编程)的区别:oop是对类纵向的扩展;aop是横向的扩展。

    9.2 AOP开发术语

    • 连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。
    • 切入点(Pointcut):被切入连接点。
    • 通知、增强(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知等。
    • 目标对象(Target):代理的目标对象
    • 织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。
    • 代理(Proxy):被AOP织入通知后,产生的结果类。
    • 切面(Aspect):由切点和通知组成,将横切逻辑织入切面所指定的连接点中。

    9.3 作用

    Spring的AOP编程即是通过动态代理类为原始类的方法添加辅助功能。

    9.4 环境搭建

    引入AOP相关依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.1.6.RELEASE</version>
    </dependency>
    
    

    spring-context.xml引入AOP命名空间

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd
           ">
    </beans>
    
    

    9.5 开发流程

    定义原始类

    package com.qf.aaron.aop.basic;
    
    public interface UserService {
        public void save();
    }
    
    
    package com.qf.aaron.aop.basic;
    
    public class UserServiceImpl implements UserService {
        public void save() {
            System.out.println("save method executed...");
        }
    }
    
    

    定义通知类(添加额外功能

    package com.qf.aaron.aop.basic;
    import org.springframework.aop.MethodBeforeAdvice;
    import java.lang.reflect.Method;
    
    public class MyAdvice implements MethodBeforeAdvice { //实现前置通知接口
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println("before advice executed...");
        }
    }
    
    

    定义bean标签

    <!--原始对象-->
    <bean id="us" class="com.qf.aaron.aop.basic.UserServiceImpl" />
    
    <!--辅助对象-->
    <bean id="myAdvice" class="com.qf.aaron.aop.basic.MyAdvice" />
    
    

    定义切入点(PointCut)

    形成切面(Aspect)

    <aop:config>
        <!--切点-->
        <aop:pointcut id="myPointCut" expression="execution(* save())" />
    </aop:config>
    
    ```java
    <aop:config>
        <!--组装切面 -->
        <aop:advisor advice-ref="myAdvice" pointcut-ref="myPointCut" />
    </aop:config>
    
    

    9.6 通知类

    可定义的通知类有6种,可以按需求选择通知类。

    前置通知:MethodBeforeAdvice
    
    后置通知:AfterAdvice
    
    后置通知:AfterReturningAdvice //有异常不执行,方法会因异常而结束,无返回值
    
    异常通知:ThrowsAdvice
    
    环绕通知:MethodInterceptor
    
    

    9.7 JDK动态代理和CGLIB动态代理的选择

    • spring底层,包含了jdk代理和cglib代理两种动态代理生成机制
    • 基本规则是:目标业务类如果有接口则用JDK代理,没有接口则用CGLib代理

    但是spring中默认开启JDK动态代理,当需要使用CGLIB动态代理时,需要在spring配置文件中配置。

    <!--  使用cglib的方式实现aop -->
        <aop:aspectj-autoproxy proxy-target-class="true"/>
    
    

    10 基于aspectJ的AOP实现

    编写aspectJ通知代码:

    @Aspect
    public class AspectJAdvisor {
    
        // 环绕通知
        @Around("execution(* org.example.service.impl.*.*(..))")
        public Object timer(ProceedingJoinPoint pjp) throws Throwable{
            System.out.println("前置通知");
            Object o = pjp.proceed();
            System.out.println("【后置通知】");
            return o;
        }
    
        // 后置通知
        @After("execution(* org.example.service.impl.*.*(..))")
        public void after(JoinPoint jp) throws Throwable{
            System.out.println("====After method invokded====");
        }
    
        // 前置通知
        @Before("execution(* org.example.service.impl.*.*(..))")
        public void before(JoinPoint jp){
            System.out.println("====before method invoked====");
        }
    	// 正常返回的通知
        @AfterReturning("execution(* org.example.service.impl.*.*(..))")
        public void afterReturning(JoinPoint jp){
            System.out.println("====after value return====");
        }
        // 抛出异常后的通知,方法的异常必须与代理类抛出的异常一致,throwing的值要与异常的形参名保持一致
        @AfterThrowing(value = "execution(* org.example.service.impl.*.*(..))", throwing="npe")
        public void afterThrowException(JoinPoint jp, NullPointerException npe){
            System.out.println("====after exception throwing====");
        }
    }
    
    

    配置

    <bean id="userService" class="org.example.service.impl.UserServiceImpl"></bean>
    
    <bean id="throwsAdvisor" class="org.example.advisor.AspectJAdvisor"></bean>
    
    <!--- aspectJ是使用cglib来实现动态代理的 -->
    <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
    
    

    11 基于注解开发

    11.1 声明bean

    用于替换自建类型组件的 <bean…>标签;可以更快速的声明bean

    • @Service 业务类专用
      @Repository dao实现类专用
      @Controller web层专用
    • @Component 通用
    • @Scope 用户控制bean的创建模式
    // @Service说明 此类是一个业务类,需要将此类纳入工厂  等价替换掉 <bean class="xxx.UserServiceImpl">
    // @Service默认beanId == 首字母小写的类名"userServiceImpl"
    // @Service("userService") 自定义beanId为"userService"
    @Service //声明bean,且id="userServiceImpl"
    @Scope("singleton") //声明创建模式,默认为单例模式 ;@Scope("prototype")即可设置为多例模式
    public class UserServiceImpl implements UserService {
     	...   
    }
    
    

    11.2 注入(DI)

    用于完成bean中属性值的注入

    • @Autowired 基于类型自动注入
    • @Resource 基于名称自动注入
    • @Qualifier(“userDAO”) 限定要自动注入的bean的id,一般和@Autowired联用
    • @Value 注入简单类型数据 (jdk8种+String)
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired //注入类型为UserDAO的bean
        @Qualifier("userDAO2") //如果有多个类型为UserDAO的bean,可以用此注解从中挑选一个
        private UserDAO userDAO;
    }
    
    
    @Service
    public class UserServiceImpl implements UserService {
    
    	@Resource("userDAO3") //注入id=“userDAO3”的bean
        private UserDAO userDAO;
        /*
        @Resource //注入id=“userDAO”的bean
        private UserDAO userDAO;
        */
    }
    
    
    public class XX{
        @Value("100") //注入数字
        private Integer id;
        @Value("shine") //注入String
    	private String name;
    }
    
    

    11.3 事务控制

    用于控制事务切入

    • @Transactional
    • 工厂配置中的 <tx:advice… 和 <aop:config… 可以省略 !!
    //类中的每个方法都切入事务(有自己的事务控制的方法除外)
    @Transactional(isolation=Isolation.READ_COMMITTED,propagation=Propagation.REQUIRED,readOnly=false,rollbackFor=Exception.class,timeout = -1)
    public class UserServiceImpl implements UserService {
    	...
        //该方法自己的事务控制,仅对此方法有效
    	@Transactional(propagation=Propagation.SUPPORTS)
    	public List<User> queryAll() {
    		return userDao.queryAll();
    	}
    	public void save(User user){
    		userDao.save(user);
    	}
    }
    
    

    11.4 注解所需配置

    <!-- 告知spring,哪些包中 有被注解的类、方法、属性 -->
    <!-- <context:component-scan base-package="com.qf.a,com.xx.b"></context:component-scan> -->
    <context:component-scan base-package="com.qf"></context:component-scan>
    
    <!-- 告知spring,@Transactional在定制事务时,基于txManager=DataSourceTransactionManager -->
    <tx:annotation-driven transaction-manager="txManager"/>
    
    

    11.5 AOP开发

    11.5.1 注解使用

    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.stereotype.Component;
    
    @Aspect // 声明此类是一个切面类:会包含切入点(pointcut)和通知(advice)
    @Component //声明组件,进入工厂
    public class MyAspect {
        // 定义切入点
        @Pointcut("execution(* com.qf.spring.service.UserServiceImpl.*(..))")
        public void pc(){}
    
        @Before("pc()") // 前置通知
        public void mybefore(JoinPoint a) {
            System.out.println("target:"+a.getTarget());
            System.out.println("args:"+a.getArgs());
            System.out.println("method's name:"+a.getSignature().getName());
            System.out.println("before~~~~");
        }
    
        @AfterReturning(value="pc()",returning="ret") // 后置通知
        public void myAfterReturning(JoinPoint a,Object ret){
            System.out.println("after~~~~:"+ret);
        }
    
        @Around("pc()") // 环绕通知
        public Object myInterceptor(ProceedingJoinPoint p) throws Throwable {
            System.out.println("interceptor1~~~~");
            Object ret = p.proceed();
            System.out.println("interceptor2~~~~");
            return ret;
        }
    
        @AfterThrowing(value="pc()",throwing="ex") // 异常通知
        public void myThrows(JoinPoint jp,Exception ex){
            System.out.println("throws");
            System.out.println("===="+ex.getMessage());
        }
    }
    
    

    11.5.2 配置

    <!-- 添加如下配置,启用aop注解 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
    

    12 Spring单元测试

    12.1 导入依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>4.3.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    
    

    12.2 测试编码

    可以免去工厂的创建过程;

    可以直接将要测试的组件注入到测试类。

    @RunWith(SpringJUnit4ClassRunner.class) //由SpringJUnit4ClassRunner启动测试
    @ContextConfiguration("classpath:applicationContext.xml") //spring的配置文件位置
    public class SpringTest{//当前测试类也会被纳入工厂中,所以其中属性可以注入
    
        @Autowired // 注入要测试的组件
        @Qualifier("userDAO")
        private UserDAO userDAO;
    
        @Test
        public void test(){
            // 测试使用userDAO
            userDAO.queryUser();
            ....
        }
    }
    

    最后

    感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

  • 相关阅读:
    linux(6)查看进程ps命令
    Python 基础03 序列
    Python 基础02 基本数据类型
    Python基础01 Hello World!
    Linux vi/vim
    Laravel 的HTTP控制器
    Laravel 下的伪造跨站请求保护 CSRF#
    Linux 磁盘管理
    Linux 用户he用户组管理
    Linxu 用户和用户组管理1
  • 原文地址:https://www.cnblogs.com/lwh1019/p/13425895.html
Copyright © 2020-2023  润新知