SpringMVC

SpringMVC

可以用于替代Servlet,也就是Servlet能做的SpringMVC都能做

参数注解(重要)

最新的spring控制层传参的时候如果参数是从url获取的,都要用参数注解注明参数名便于获取,区分各种类型的参数注解

@RequestBody

从json数据获取,也就是从请求体中获取的,参数前加@RequestBody注解

@RequestParam("参数名")

从url中获取(GET请求),需要指定参数名

@PathVariable("参数名")

从url中获取但是是REST风格的url

简介

概述

SpringMVC是一种基于Java实现MVC模型的轻量级Web框架,优点在于使用简单,开发便捷(相对于Servlet),且灵活性强

SpringMVC是一种表现层框架技术

SpringMVC用于进行表现层功能开发

入门案例

导入坐标

先导入servlet的坐标和springMVC的坐标

xml 复制代码
<dependencies>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>6.2.7</version>
    </dependency>
</dependencies>
创建SpringMVC控制器类(等同Servlet功能)

先用@Controller注解把类定义为一个控制类Bean

然后用@RequestMapping("/调用名称")来给控制类的方法设置调用名称,调用名称一般在类上也用@RequestMapping注解加上前缀名,tomcat运行的时候完整的调用名称是前缀名加方法调用名(比如/user/save),因为BookController和UserController可能都有save方法,需要用前缀名加以区别

然后用@ResponseBody注解(这个注解相当于告诉SpringMVC该方法的返回值无需找视图,只需要转换成数据直接返回给客户端)

UserControllerImpl

java 复制代码
@Controller
@RequestMapping("/user")
public class UserController {
    //设置该方法的访问路径
    @RequestMapping("/save")
//    设置当前操作的返回值类型(也就是把当前返回的东西作为响应数据传出去)
    @ResponseBody
    public String save(){
        System.out.println("user-save...");
        return "{'module':'springmvc'}";
    }
}
创建SpringMVC配置类

创建springmvc配置类,跟spring配置类一样用@ComponentScan扫描Bean,用@Configuration声明配置身份

SpringMVCConfig

java 复制代码
@Configuration
@ComponentScan("com.morgan.controller")
public class SpringMVCConfig {
}
创建servlet启动容器的配置类,在里面加载spring的配置

创建一个servlet容器启动的配置类,在里面加载spring的配置,继承AbstractDispatcherServletInitializer类,实现父类的三个方法(加载springmvc容器配置,设置归属springmvc处理的请求,加载spring配置)

加载springmvc容器配置的方法createServletApplicationContext的内容跟之前学spring-framework的时候加载spring差不多,但要注意之前的AnnotationConfigApplicationContext不适用web环境,需要换成AnnotationConfigWebApplicationContext,然后加载SpringMVC配置类

设置归属springmvc处理的请求把return的String[0]改成String[]{"/"}表示所有的请求都归springmvc处理

spring配置的加载暂时不需要动,跟原来一样return null就行

ServletContainerInitConfig

java 复制代码
//定义一个servlet容器启动的配置类,在里面加载spring的配置
public class ServletContainerInitConfig extends AbstractDispatcherServletInitializer {
    //加载springmvc容器配置
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        //原来那个Spring配置的ctx不适用于web环境,需要换成这个
        AnnotationConfigWebApplicationContext ctx=new AnnotationConfigWebApplicationContext();
        ctx.register(SpringMVCConfig.class);
        return ctx;
    }
    //设置归属springmvc处理的请求
    @Override
    protected String[] getServletMappings() {
        //所有请求都归springmvc处理
        return new String[]{"/"};
    }
    //加载Spring容器配置(现在暂时return null就行后面会再写Spring配置)
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }
}
静态页面的放行

因为后续会涉及到web的静态页面的放行问题,比如html应该由tomcat来管理但是由于在servlet启动容器配置类中把所有请求都归springmvc处理的,就会出问题,这时候就需要把这些静态资源放行

比如如果所有资源都是springmvc管理,那在controller类中定义方法跳转到jsp页面可以跳转,但在浏览器直接输jsp页面就进不去,因为这些本来应该由tomcat管理,但现在都交给springmvc管理了就导致直接在地址栏输入用不了

创建SpringMVCSurpport类,打上@Configuration注解,在里面配置不交由springmvc管理的模块

然后在SpringMVC配置类中@ComponentScan({"com.morgan.controller","com.morgan.config"}),加上config包来扫描到SpringMVCSupport类

SpringMVCConfig

java 复制代码
@Configuration
@ComponentScan({"com.morgan.controller","com.morgan.config"})
@EnableWebMvc
public class SpringMVCConfig implements WebMvcConfigurer {
}

SpringMVCSupport

java 复制代码
@Configuration
public class SpringMVCSupport extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        //表示检测到前面形式的url的时候访问后面这个包,下面还可以以相同类型继续添加包
        registry.addResourceHandler("/page/**").addResourceLocations("/page/");
    }
运行

配置tomcat,启动,转到/save

bean的加载和控制

SpringMVC要加载的Bean只有controller层的(因为是servlet功能负责的前后端对接),其他的Bean仍由Spring管理,那就需要避免Spring加载到SpringMVC已经加载的Bean

SpringMVC只需要正常加载com.morgan.controller包就行

而Spring要想避免加载到SpringMVC已经加载的包有两种方式,精确的加载到service包,dao包等或者加载到com.morgan但排除掉controller包

先创建Spring配置类

然后在servlet启动容器配置类中加载Spring配置类

ServletContainerInitConfig

java 复制代码
//定义一个servlet容器启动的配置类,在里面加载spring的配置
public class ServletContainerInitConfig extends AbstractDispatcherServletInitializer {
    //加载springmvc容器配置
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        //原来那个Spring配置的ctx不适用于web环境,需要换成这个
        AnnotationConfigWebApplicationContext ctx=new AnnotationConfigWebApplicationContext();
        ctx.register(SpringMVCConfig.class);
        return ctx;
    }
    //设置归属springmvc处理的请求
    @Override
    protected String[] getServletMappings() {
        //所有请求都归springmvc处理
        return new String[]{"/"};
    }
    //加载Spring容器配置
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        //原来那个Spring配置的ctx不适用于web环境,需要换成这个
        AnnotationConfigWebApplicationContext ctx=new AnnotationConfigWebApplicationContext();
        ctx.register(SpringConfig.class);
        return ctx;
    }
}

然后在spring中加载包

精确加载(常用)

java 复制代码
@Configuration
@ComponentScan({"com.morgan.dao","com.morgan.service"})
public class SpringConfig {
}

排除加载

java 复制代码
@Configuration
@ComponentScan({"com.morgan.dao","com.morgan.service"})
/*@ComponentScan(value = "com.morgan",
        excludeFilters = @ComponentScan.Filter(
                //意思是按注解排除
                type = FilterType.ANNOTATION,
                //意思是排除注解为@Contoller的Bean
                classes = Controller.class
        )
)*/
public class SpringConfig {
}

请求与响应

请求映射路径

用@RequestMapper()注解来写请求映射路径

写在类上的是给本类所有方法设置的前缀名\

写在方法上的是检索名

java 复制代码
@Controller
@RequestMapping("/user")
public class UserControllerImpl implements UserController {
    //设置该方法的访问路径
    @RequestMapping("/save")
    //    设置当前操作的返回值类型(也就是把当前返回的东西作为响应数据传出去)
    @ResponseBody
    @Override
    public String save() {
        System.out.println("user-save...");
        return "{'module':'springmvc'}";
    }

    @RequestMapping("/delete")
    @ResponseBody
    @Override
    public String delete() {
        return "{'module':'springmvc-delete'}";
    }

}

用请求发送参数

普通参数
GET请求

在url后面加上?参数1名=参数1值&参数2名=参数2值

然后在需要接收参数的方法中以方法参数的形式接收参数,注意在每个参数前面要加上@RequestParam("参数名")注解来注明要接收的参数名

java 复制代码
@Controller
@RequestMapping("/user")
public class UserControllerImpl implements UserController {
    //设置该方法的访问路径
    @RequestMapping("/save")
    //    设置当前操作的返回值类型(也就是把当前返回的东西作为响应数据传出去)
    @ResponseBody
    @Override
    public String save() {
        System.out.println("user-save...");
        return "{'module':'springmvc'}";
    }

    @RequestMapping("/delete")
    @ResponseBody
    @Override
    public String delete(@RequestParam("name") String name,@RequestParam("age") int age) {
        System.out.println(name);
        System.out.println(age);
        return "{'module':'springmvc-delete'}";
    }

}

url为http://localhost/mvc_example1_war_exploded/user/delete?name=sad\&age=15

POST请求

POST请求需要在表单里提交,这里我们用Postman进行测试,提交数据,后端程序跟GET一样不用动

但POST需要处理中文乱码问题需要在servlet启动容器配置类中再实现一个方法getServletFilters(SpringMVC自己的方法不是自己写的),在方法里创建CharacterEncodingFilter对象filter作为过滤器,filter.setEncoding("UTF-8")来设定过滤器,最后返回new Filter[]{filter}数组来设定过滤器,如果有多个过滤器在大括号用逗号隔开往后加就行

ServletContainerInitConfig

java 复制代码
//定义一个servlet容器启动的配置类,在里面加载spring的配置
public class ServletContainerInitConfig extends AbstractDispatcherServletInitializer {
    //加载springmvc容器配置
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        //原来那个Spring配置的ctx不适用于web环境,需要换成这个
        AnnotationConfigWebApplicationContext ctx=new AnnotationConfigWebApplicationContext();
        ctx.register(SpringMVCConfig.class);
        return ctx;
    }
    //设置归属springmvc处理的请求
    @Override
    protected String[] getServletMappings() {
        //所有请求都归springmvc处理
        return new String[]{"/"};
    }
    //加载Spring容器配置
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        //原来那个Spring配置的ctx不适用于web环境,需要换成这个
        AnnotationConfigWebApplicationContext ctx=new AnnotationConfigWebApplicationContext();
        ctx.register(SpringConfig.class);
        return ctx;
    }

    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter filter=new CharacterEncodingFilter();
        filter.setEncoding("UTF-8");
        return new Filter[]{filter};
    }
}
实体类参数

假如要传的是一个实体类User

User

java 复制代码
public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

因为有getter和setter方法,要获取参数的controller方法就可以直接获取参数

UserControllerImpl

java 复制代码
@RequestMapping("/getEntity")
@ResponseBody
@Override
public String getEntity(User user) {
    System.out.println(user);
    return "{'module':'springmvc-getEn'}";
}

在url中还是正常的传name和age两个成员变量的值

如果成员变量有实体类变量,比如User的一个变量是实体类Address变量address,那传参的时候用address.province这种形式,多层实体类变量就一直点就行

数组参数

定义数组参数,比如String[] likes,然后在url用likes传多个值,比如?likes=ad&likes=ac这样,注意key必须都是likes

注意加@RequestParam注解

UserControllerImpl

java 复制代码
@RequestMapping("/getArray")
@ResponseBody
@Override
public String getArray(@RequestParam("likes") String[] likes) {
    System.out.println(Arrays.toString(likes));
    return "{'module':'springmvc-getArr'}";
}
集合参数

集合参数跟上面数组基本一模一样

java 复制代码
@RequestMapping("/getList")
@ResponseBody
@Override
public String getList(@RequestParam("likes") List<String> likes) {
    System.out.println(likes.toString());
    return "{'module':'springmvc-getList'}";
}
json数据参数

先导入坐标

xml 复制代码
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.19.0</version>
</dependency>

在SpringMVC配置类上加上@EnableWebMvc注解开启json数据转换

SpringMVC

java 复制代码
@Configuration
@ComponentScan("com.morgan.controller")
@EnableWebMvc
public class SpringMVCConfig {
}

把接收参数的地方参数前加上@RequestBody注解(相当于把@RequestParam改成这个,因为json数据是放在请求体里的而不是请求参数)

UserControllerImpl

java 复制代码
@RequestMapping("/getJson")
@ResponseBody
@Override
public String getJson(@RequestBody List<String> likes) {
    System.out.println(likes.toString());
    return "{'module':'springmvc-getJson'}";
}

Postman里发送json数据用body里的raw的json格式发送就行

json数组

都跟上面一样,Postman里要测试json数据用中括号字符串数组就行["a","b"]

java 复制代码
@RequestMapping("/getJson")
@ResponseBody
@Override
public String getJson(@RequestBody List<String> likes) {
    System.out.println(likes.toString());
    return "{'module':'springmvc-getJson'}";
}
json的实体类对象

也跟上面一样,Postman里要测试json数据用大括号键值对,如果有多层实体类就是多层大括号键值对

{"name":"张三",

"age":"15",

"address":{"province":"taiwan","city":"qinghai"}

}

java 复制代码
@RequestMapping("/getJsonPojo")
@ResponseBody
@Override
public String getJsonPojo(@RequestBody User user) {
    System.out.println(user);
    return "{'module':'springmvc-getJsonPojo'}";
}
json的实体类数组

List类型的参数

就中括号里套大括号就行

{"name":"张三", "age":"15", "address":{"province":"taiwan","city":"qinghai"} }, {"name":"李四", "age":"12", "address":{"province":"taiwan","city":"qinghai"} }

java 复制代码
@RequestMapping("/getJsonPojoList")
@ResponseBody
@Override
public String getJsonPojoList(@RequestBody List<User> list) {
    System.out.println(list);
    return "{'module':'springmvc-getJsonPojoList'}";
}
日期参数

传递日期型参数可以直接用Date参数,这样子传递的数据格式应当是yyyy/MM/dd默认格式,如果要其他格式用@DateTimeFormat(pattern="格式")注解,这样子改的格式既可以是年月日,还可以带上时间,当然@RequestParam注解也要加

java 复制代码
@RequestMapping("/getDateParam")
@ResponseBody
@Override
public String getDateParam(@RequestParam("date1") Date date1,@RequestParam("date2") @DateTimeFormat(pattern="yyyy-MM-dd") Date date2,@RequestParam("date3") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date date3) {
    System.out.println(date1);
    System.out.println(date2);
    System.out.println(date3);
    return "{'module':'springmvc-getDateParam'}";
}

响应

注意,如果没有@ResponseBody这个注解,SpringMVC管理controller方法默认都是返回一个字符串,然后把这个字符串解析为一个页面

所以无论是向返回字符串还是返回json数据都需要加@ResponseBody注解,只有要跳转的时候不加

响应页面

响应页面就只需要一个@RequestMapping("/响应名")注解就可以了,因为之前一直用的注解@ResponseBody的作用是把返回的内容作为文本数据返回而不解析

UserController

java 复制代码
@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/toJumpPage")
    public String toJumpPage(){
        System.out.println("跳转中");
        return "/page.jsp";
    }


}

页面会跳转到jsp文件page上

响应字符串

加上@ResponseBody注解就行了

UserController

java 复制代码
@RequestMapping("/toText")
@ResponseBody
public String toText(){
    System.out.println("返回数据");
    return "page.jsp";
}

最后返回字符串page.jsp而不是jsp页面

响应json数据
响应实体类对象

跟响应字符串差不多

User

java 复制代码
public class User {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

UserController

java 复制代码
@RequestMapping("/toPojo")
@ResponseBody
public User toPojo(){
    System.out.println("返回pojo");
    User user=new User();
    user.setName("zhang");
    user.setAge(15);
    return user;
}
响应实体类集合

跟实体类差不多

UserController

java 复制代码
@RequestMapping("/toPojoList")
@ResponseBody
public List<User> toPojoList(){
    System.out.println("返回pojo");
    //注意这里不要用一个User对象传两次,因为list.add方法往list中传user对象的时候传的是对象本事不是对象的副本
    User user1 =new User();
    user1.setName("zhang");
    user1.setAge(15);

    User user2=new User();
    user2.setName("li");
    user2.setAge(11);

    List<User> list=new ArrayList<>();
    list.add(user1);
    list.add(user2);
    return list;
}

REST

简介

格式

http://localhost/users 查询全部用户信息 GET(查询)

http://localhost/users/1 查询指定用户信息 GET(查询)

http://localhost/users 添加用户信息 POST(新增/保存)

http://localhost/users 修改用户信息 PUT(修改/更新)

http://localhost/users/1 删除用户信息 DELETE(删除)

注意描述模块的名称通常用复数表示此类资源而非单个资源,例如:users,books

根据REST风格对资源进行访问称为RESTful

优点

隐藏资源的访问行为,无法通过地址得知对资源是何种操作

书写简化

RESTful开发

普通开发

url传参是直接以/参数的形式

若不需要传参,按REST风格开发,@RequestMapping(value="/users",method=RequestMethod.GET)这种形式,同一个类所有的访问名都写成一样的(就像users这种风格),然后通过method来区分,这样只能限定到六个方法,基本满足简单开发,但实际开发更复杂,会扩展REST,这个后面再说

若要传参(查询指定用户信息或删除这种),在后端开发时在方法的参数前加上@PathVariable("参数名")表示这个参数是从url上获取的(这个用来替代@RequestParam注解),然后在访问名后面用/{参数名}的形式指定参数获取@RequestMapping(value="/users/{id}",method=RequestMethod.GET)

java 复制代码
@Controller
public class RestController {
//    添加用的发送方式是post
    @RequestMapping(value = "/users",method = RequestMethod.POST)
    @ResponseBody
    public String insert(){
        System.out.println("insert");
        return "insert";
    }

//    在参数前面加上@PathVariable注解表示参数是从url路径中获取的,删除的发送方式是delete,/user/{id}表示/user路径后面的值传给id参数
    @RequestMapping(value = "/users/{id}",method = RequestMethod.DELETE)
    @ResponseBody
    public String delete(@PathVariable("id") Integer id){
        System.out.println(id);
        return "delete";
    }

//    查询用的是GET
    @RequestMapping(value = "/users",method = RequestMethod.GET)
    @ResponseBody
    public String select(){
        System.out.println("select");
        return "select";
    }

    //    查询用的是GET,从路径上获取参数
    @RequestMapping(value = "/users/{id}",method = RequestMethod.GET)
    @ResponseBody
    public String selectById(@PathVariable("id") Integer id){
        System.out.println(id);
        return "select";
    }

//    修改用的是PUT
    @RequestMapping(value = "/users",method = RequestMethod.PUT)
    @ResponseBody
    public String update(){
        System.out.println("update");
        return "update";
    }
}
快速开发

由于访问名,@ResponseBody都是公用的,可以@RequestMapping("/访问名")和@ResponseBody直接放在类上表示整个类所有方法通用,然后@Controller注解和@ResponseBody注解可以合并成@RestController注解,方法注解@RequestMapping(method=RequestMethod.GET)可以直接简化为@GetMapping,如果是需要参数的就是@GetMapping("/{参数名}")

java 复制代码
@RestController
@RequestMapping("/books")
public class BookController {
    //    添加用的发送方式是post
    @PostMapping
    public String insert(){
        System.out.println("insert");
        return "insert";
    }

    //    在参数前面加上@PathVariable注解表示参数是从url路径中获取的,删除的发送方式是delete,/user/{id}表示/user路径后面的值传给id参数
    @DeleteMapping({"/{id}"})
    public String delete(@PathVariable("id") Integer id){
        System.out.println(id);
        return "delete";
    }

    //    查询用的是GET
    @GetMapping
    public String select(){
        System.out.println("select");
        return "select";
    }

    //    查询用的是GET,从路径上获取参数
    @GetMapping({"/{id}"})
    public String selectById(@PathVariable("id") Integer id){
        System.out.println(id);
        return "select";
    }

    //    修改用的是PUT
    @PutMapping
    public String update(){
        System.out.println("update");
        return "update";
    }
}

SSM整合

整合配置

坐标导入
xml 复制代码
    <dependencies>
<!--        springmvc-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>6.2.7</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.19.0</version>
        </dependency>
<!--        spring-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.2.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.2.7</version>
        </dependency>
<!--        mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.17</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.23</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
包框架

controller: 控制层,前后端交互

entity: 实体类,对应数据库的表

dao: 数据层,通过sql语句进行对数据库数据的操作

service: 功能层,通过对dao层的应用实现功能,需要子包impl(面向接口编程降低耦合度,dao层不需要是因为mybatis开发是自动代理直接用的接口不需要实现类,controller层不需要是因为需要应用mvc整合前后端,所有注解都基于实现类开发)

config: 配置,存放所有的配置类

配置
jdbc配置类(依赖于spring配置类)

通过外部properties文件导入数据库相关配置并存为一个DataSource类型的Bean方便后面mybatis配置使用

注意properties文件的导入不是直接导入到jdbc配置类中的而是导入到spring配置类中,然后jdbc配置类和mybatis配置类也导入spring配置类中,这三个都在一个位置就可以相互引用了,所以说jdbc配置类和mybatis配置类包括properties文件实际上都是spring配置类的一部分

JdbcConfig

java 复制代码
//数据库连接配置类
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;


    @Bean("dataSource")
    public DataSource dataSource(){
        DruidDataSource dataSource=new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}
Mybatis配置类(依赖于Spring配置类)

造一个存放数据库信息的工厂bean和获取代理开发用的mapper的bean,注意数据库信息存储的DataSource对象是通过spring按类型直接从Jdbc配置类中按类型获取的bean

MybatisConfig

java 复制代码
//mybatis配置类
public class MybatisConfig {

    //造一个工厂Bean用来存放数据库信息
    //DataSource对象是spring按类型直接从jdbc配置类中获取的bean
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        //给实体类起别名,默认为类名首字母小写,如果不起就需要写完整包名很麻烦
        factoryBean.setTypeAliasesPackage("com.morgan.entity");
        return factoryBean;
    }

//    配置一个类来扫描数据层dao包获取mapper进行代理开发
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer mapperScannerConfigurer=new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.morgan.dao");
        return mapperScannerConfigurer;
    }
}
Spring配置类

SpringConfig

java 复制代码
//Spring配置类
@Configuration
@ComponentScan({"com.morgan.service"})
//引入文件资源的注解
@PropertySource("classpath:jdbc.properties")
//导入相关需要依赖的配置类的注解
@Import({JdbcConfig.class, MybatisConfig.class})
public class SpringConfig {
}
SpringMVC配置类

SpringMVCConfig

java 复制代码
//springmvc配置类
//配置类注解
@Configuration
//指定扫描的包的注解
@ComponentScan({"com.morgan.controller"})
//管理web相关操作的注解,比如json与字符串数据返回值转换等
@EnableWebMvc
public class SpringMVCConfig {
}
Web配置类

用来加载spring配置类和springmvc配置类并设定交给springmvc管理的请求

ServletConfig

java 复制代码
//web配置类
public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
//    配置Spring配置类(根配置)
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

//    配置springmvc配置类(web相关配置)
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{SpringMVCConfig.class};
    }

//    配置要交给springmvc配置的请求,在这里把所有的请求交给springmvc,若要交给tomcat再单独配置
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

功能实现

1.用注解开发dao层

BookDao(接口)

java 复制代码
public interface BookDao {
    //注意id设置的自增不需要写,前面的type是数据库字段,后面的type是参数book里的变量,表示把变量的值给到数据库2
    @Insert("insert into tbl_book (type,name,description) values (#{type},#{name},#{description})")
    void insert(Book book);
    @Update("update tbl_book set type=#{type},name=#{name},description=#{description} where id=#{id}")
    void update(Book book);
    @Delete("delete from tbl_book where id=#{id}")
    void delete(Integer id);
    @Select("select * from tbl_book")
    List<Book> selectAll();
    @Select("select * from tbl_book where id=#{id}")
    Book selectById(Integer id);
}
2.根据dao层方法实现service层功能
java 复制代码
public interface BookService {
    /**
     * 添加一组数据
     * @param book
     */
    boolean insert(Book book);

    /**
     * 按id修改某行数据
     * @param book
     */
    boolean update(Book book);

    /**
     * 按id删除某行数据
     * @param id
     */
    boolean delete(Integer id);

    /**
     * 查询所有的数据
     * @return
     */
    List<Book> selectAll();

    /**
     * 按id查询某行数据
     * @param id
     * @return
     */
    Book selectById(Integer id);
}

BookServiceImpl

java 复制代码
//注解定义为bean
@Service
public class BookServiceImpl implements BookService {
    //这里book可能会出现红标因为自动装配导致idea检查的时候找不到bean,但这个红标不影响程序
    //自动装配bean,这个Service类Bean依赖于BookDao类型的Bean要用这个注解进行依赖注入
    @Autowired
    private BookDao bookDao;

    @Override
    public boolean insert(Book book) {
        bookDao.insert(book);
        return true;
    }

    @Override
    public boolean update(Book book) {
        bookDao.update(book);
        return true;
    }

    @Override
    public boolean delete(Integer id) {
        bookDao.delete(id);
        return true;
    }

    @Override
    public List<Book> selectAll() {
        return bookDao.selectAll();
    }

    @Override
    public Book selectById(Integer id) {
        return bookDao.selectById(id);
    }
}
3.根据service层开发controller层

BookController

java 复制代码
//@Controller注解和@ResponseBody注解的合体
@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService;

//    这种参数是从json数据请求体中获取的不是url所以不用@RequestParam
    @PostMapping
    public boolean insert(@RequestBody Book book) {
        return bookService.insert(book);
    }

    @PutMapping
    public boolean update(@RequestBody Book book) {
        return bookService.update(book);
    }

    @DeleteMapping("/{id}")
    public boolean delete(@PathVariable("id") Integer id) {
        return bookService.delete(id);
    }

    @GetMapping
    public List<Book> selectAll() {
        return bookService.selectAll();
    }

    @GetMapping("/{id}")
    public Book selectById(@PathVariable("id") Integer id) {
        return bookService.selectById(id);
    }
}
4.开启spring事务

在spring配置类上加上@EnableTransactionManager注解

SpringConfig

java 复制代码
//Spring配置类
@Configuration
//开启spring事务
@EnableTransactionManagement
@ComponentScan({"com.morgan.service"})
//引入文件资源的注解
@PropertySource("classpath:jdbc.properties")
//导入相关需要依赖的配置类的注解
@Import({JdbcConfig.class, MybatisConfig.class})
public class SpringConfig {
}

由于事务是跟数据库相关的所以在Jdbc配置类中添加一个事务管理器为bean

JdbcConfig

java 复制代码
//平台事务管理器
//注意datasource用参数调用不用方法调用,要不然就不是spring管理了
@Bean
public PlatformTransactionManager platformTransactionManager(DataSource dataSource){
    DataSourceTransactionManager dataSourceTransactionManager=new DataSourceTransactionManager();
    dataSourceTransactionManager.setDataSource(dataSource);
    return dataSourceTransactionManager;
}

在Service层接口上添加@Transactional注解开启事务

BookService

java 复制代码
//开启事务,里面每个方法都是一个事务
@Transactional
public interface BookService {
    /**
     * 添加一组数据
     * @param book
     */
    boolean insert(Book book);

    /**
     * 按id修改某行数据
     * @param book
     */
    boolean update(Book book);

    /**
     * 按id删除某行数据
     * @param id
     */
    boolean delete(Integer id);

    /**
     * 查询所有的数据
     * @return
     */
    List<Book> selectAll();

    /**
     * 按id查询某行数据
     * @param id
     * @return
     */
    Book selectById(Integer id);
}
5.额外:测试

在test.java包里创建测试类测试service层的功能

com.morgan.BookServiceTest

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class BookServiceTest {
    @Autowired
    private BookService bookService;

//    注意测试方法是不能有参数的,参数数据都是直接在方法里给的
    @Test
    public void selectByIdTest(){
        Book book=bookService.selectById(1);
        System.out.println(book);
    }

    @Test
    public void selectTest(){
        List<Book> list=bookService.selectAll();
        System.out.println(list);
    }
}

前后端数据传输协议

由于前后端通信时可能出现各种问题,比如前端无法确定获取到的数据是一个还是多个,无法确定获取到的数据的用途(增删改查?),无法确定数据传输是否成功,所以前后端约定一套协议(自己约定的并非规范)来限定传达这些信息,一般将controller层各个方法的结果,传输信息(传输类型,传输结果等,比如传输类型用2001表示增,1表示成功,添加成功的code就是20011),错误信息设置为一个结果实体类Result的三个属性,然后所有的方法传递的都是一个Result类型的对象

注意由于data可能是列表,数组等等,所以定义要用Object

Result

java 复制代码
public class Result {
//    传输信息
    private Integer code;
//    传输数据,由于可能是各种类型数据所以用Object
    private Object data;
//    错误信息
    private String msg;

//    定义三个构造函数任挑选
    public Result() {
    }

    public Result(Integer code, Object data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    public Result(Integer code, Object data) {
        this.code = code;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

Code

java 复制代码
public class Code {
    //末尾数字区分成功(1)和失败(0),倒数第二位区分传输数据用途
    public static final Integer INSERT_OK=200511;
    public static final Integer DELETE_OK=200521;
    public static final Integer UPDATE_OK=200531;
    public static final Integer SELECT_OK=200541;

    public static final Integer INSERT_ERR=200512;
    public static final Integer DELETE_ERR=200522;
    public static final Integer UPDATE_ERR=200532;
    public static final Integer SELECT_ERR=200542;

}

BookController

java 复制代码
//@Controller注解和@ResponseBody注解的合体
@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService;

//    这种参数是从json数据请求体中获取的不是url所以不用@RequestParam
    @PostMapping
    public Result insert(@RequestBody Book book) {
        boolean data = bookService.insert(book);
        return new Result(data?Code.INSERT_OK:Code.INSERT_ERR,data);
    }

    @PutMapping
    public Result update(@RequestBody Book book) {
        boolean data = bookService.update(book);
        return new Result(data?Code.UPDATE_OK:Code.UPDATE_ERR,data);
    }

    @DeleteMapping("/{id}")
    public Result delete(@PathVariable("id") Integer id) {
        boolean data = bookService.delete(id);
        return new Result(data?Code.DELETE_OK:Code.DELETE_ERR,data);
    }

    @GetMapping("/{id}")
    public Result selectById(@PathVariable("id") Integer id) {
        Book data = bookService.selectById(id);
        Integer code=data!=null?Code.SELECT_OK:Code.SELECT_ERR;
        String msg=data!=null?"":"数据查询失败,请重试";
        return new Result(code,data,msg);
    }

    @GetMapping
    public Result selectAll() {
        List<Book> data = bookService.selectAll();
//        注意这里必须用data!=null只有这样才能证明是查询失败,如果用isEmpty有可能表本来就是空的
        Integer code=data!=null?Code.SELECT_OK:Code.SELECT_ERR;
        String msg=data!=null?"":"数据查询失败,请重试";
        return new Result(code,data,msg);
    }
}

异常处理

异常处理器

用的AOP思想对控制器类进行功能增强

在表现层controller层处理异常,用单独的异常类来进行异常的处理

给异常类加上@RestControllerAdvice:为Rest风格开发的控制器类做增强(并且这个注解自带@ResponseBody注解和Component注解)(注意由于一般异常类都放在表现层controller包下,如果不是,那还要在spring配置类中扫描到对应包)

给处理某一类异常对应的方法 加上@ExceptionHandler(异常类.class)注解来设置指定异常的处理方案,功能等同于控制器方法,出现异常后终止原始控制器执行,并转入当前方法执行,并且返回的也要是一个Result对象,这样可以反馈异常信息,并避免程序因异常直接不正常中断

异常码也需要规定

ExceptionAdvice

java 复制代码
//对rest风格的控制器类做功能增强(其实这个就是一个AOP切面),这个注解包含自带@ResponseBody注解和Component注解相当于直接注册了
@RestControllerAdvice
public class ExceptionAdvice {
    //设置该方法要处理的异常类型
    @ExceptionHandler(Exception.class)
    public Result doException(Exception e){
        return new Result(Code.EXCEPTION,null,"发生Exception异常,请进行排查并重试");
    }
}
项目异常处理
项目异常分类
业务异常(BusinessException)

规范的用户行为产生的异常

不规范的用户行为操作产生的异常

解决方案:发送对应消息传递给用户,提醒规范操作

系统异常(SystemException)

项目运行中可预计且无法避免的异常

解决方案:发送固定信息给用户,安抚用户; 发送特定信息给运维人员,提醒维护; 记录日志

其他异常(Exception)

编程人员未预期到的异常

解决方案:发送固定信息给用户,安抚用户; 发送特定信息给编程人员,提醒维护(纳入预期范围); 记录日志

项目异常处理

定义定义业务异常和系统异常的通知类(其他异常不需要,直接在写他的切面就行)

BusinessException

java 复制代码
public class BusinessException extends RuntimeException{
    //    定义一个code用来传递异常类型的信息
    private Integer code;

    public Integer getCode() {
        return code;
    }

    //    定义自己需要的构造方法
    public void setCode(Integer code) {
        this.code = code;
    }

    public BusinessException(Integer code,String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public BusinessException(Integer code,Throwable cause) {
        super(cause);
        this.code = code;
    }
}

SystemException

java 复制代码
//定义一个系统性异常,继承RuntimeException这样可以自动把异常往上抛,不需要一直手动throw
public class SystemException extends RuntimeException{
//    定义一个code用来传递异常类型的信息
    private Integer code;

    public Integer getCode() {
        return code;
    }

//    定义自己需要的构造方法
    public void setCode(Integer code) {
        this.code = code;
    }

    public SystemException(Integer code,String message) {
        super(message);
        this.code = code;
    }

    public SystemException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public SystemException(Integer code,Throwable cause) {
        super(cause);
        this.code = code;
    }
}

在Code类中添加三种异常对应的异常码

Code

java 复制代码
//异常码,最后两位是对应异常编号
public static final Integer SYSTEM_EXCEPTION=50001;
public static final Integer EXCEPTION=50002;
public static final Integer BUSINESS_EXCEPTION=50003;

在异常处理器中添加业务类异常,系统性异常和其他异常的异常处理

ExceptionAdvice

java 复制代码
//对rest风格的控制器类做功能增强(其实这个就是一个AOP切面),这个注解包含自带@ResponseBody注解和Component注解相当于直接注册了
@RestControllerAdvice
public class ExceptionAdvice {
//    系统性异常
    @ExceptionHandler(SystemException.class)
    public Result doSystemException(SystemException se){
        //记录日志
        //发消息给运维
        //发送邮件给开发人员,异常对象发送给开发人员
        return new Result(se.getCode(),null,se.getMessage());
    }

//    业务类异常
    @ExceptionHandler(BusinessException.class)
    public Result doBusinessException(BusinessException be){
        return new Result(be.getCode(),null,be.getMessage());
    }


    //设置该方法要处理的异常类型
//    其他异常,其他异常是无法预测的那些异常,所以不需要写通知类
    @ExceptionHandler(Exception.class)
    public Result doException(Exception e){
        //记录日志
        //发消息给运维
        //发送邮件给开发人员,异常对象发送给开发人员
        return new Result(Code.EXCEPTION,null,"服务器繁忙,请稍后再试");
    }
}

最后在业务类中模拟一下异常

BookServiceImpl

java 复制代码
@Override
public Book selectById(Integer id) {
    //仅做测试异常功能是否可用作用,实际开发用try-catch包围可能出现异常的地方进行处理
    if(id==10086){
        throw new SystemException(Code.BUSINESS_EXCEPTION,"服务器超时,请重试");
    }else if(id==10087){
        throw new BusinessException(Code.BUSINESS_EXCEPTION,"连接超时,请重试");
    }
    return bookDao.selectById(id);
}

拦截器

概念

拦截器是一种动态拦截方法调用的机制,相当于在contoller方法前后上一道门,实行对应的方法,比如权限校验等

也是AOP

作用

在指定的方法调用前后执行预先设定好的代码

阻止原始方法的执行(比如禁止无权限用户的访问)

拦截器和过滤器的区别

归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术

拦截内容不同:Filter对所有的访问进行增强,Interceptor仅针对SpringMVC的访问进行增强(因为Filter是在tomcat时进行的,Interceptor实在springmvc进行的)

拦截器设置

先在Contoller层定义一个通知类ProjectInterceptor,实现接口HandlerInterceptor

在通知类中实现三个通知方法preHandle,postHandle,afterCompletion(接口方法)

preHandle是原始方法执行前执行的,返回值true/false决定原始方法是否执行,postHandle在原始方法执行之后,afterCompletion在postHandle执行之后

用@Component把他注册为Bean以便后面拦截器调用他

ProjectInterceptor

java 复制代码
//本质上也是一个通知类
@Component
public class ProjectInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        原始方法执行前方法,这里只是模拟一下所以直接打印,实际开发写自己的需求,比如权限的校验等
        System.out.println("preHandle...");
//        return true表示继续执行原始方法,如果return false表示拦截原始方法的执行
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        在原始方法执行之后执行
        System.out.println("postHandle...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        在postHandle方法执行之后执行
        System.out.println("afterCompletion...");
    }
}
比较麻烦的设置方式

在配置包下的web增强类(之前用这个类配置不给springmvc管理的目录)加入拦截器的定义,规定拦截对象

SpringMVCSupport

java 复制代码
//定义一个类来对springmvc进行功能增强
@Configuration
//继承webmvc增强类WebMvcConfigurationSupport
public class SpringMVCSupport extends WebMvcConfigurationSupport {
//    自动装配拦截器bean以便下面切面方法使用
    @Autowired
    private ProjectInterceptor projectInterceptor;

//    排除不想交给springmvc管理的包
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/js/**").addResourceLocations("/js/");
        registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
    }

//    切面方法,对/books下的方法设置拦截器,注意**表示/books下所有包和子包,*表示/books下的所有包但不包括子包
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books/**");
    }
}

由于这个SpringMVCSupport类是配置类,需要被SpringMVC扫描到,所以在SpringMVC配置类的扫描注解中加上config包,并实现WebMvcConfigurer接口用于对springmvc进行功能增强,比如注册拦截器、配置视图解析器、处理静态资源等

SpringMVCConfig

java 复制代码
//springmvc配置类
//配置类注解
@Configuration
//指定扫描的包的注解
@ComponentScan({"com.morgan.controller","com.morgan.config"})
//管理web相关操作的注解,比如json与字符串数据返回值转换等
@EnableWebMvc
//WebMVCConfigurer接口用于对springmvc进行功能增强,比如注册拦截器、配置视图解析器、处理静态资源
public class SpringMVCConfig implements WebMvcConfigurer {
}
简单的设置方式

直接在SpringMVC配置类实现接口WebMvcConfigurer,然后资源过滤和拦截器都可以直接定义在这里面

由于没有了增强类,也不需要再扫描config包

SpringMVCConfig

java 复制代码
//springmvc配置类
//配置类注解
@Configuration
//指定扫描的包的注解
@ComponentScan({"com.morgan.controller"})
//管理web相关操作的注解,比如json与字符串数据返回值转换等
@EnableWebMvc
//WebMVCConfigurer接口用于对springmvc进行功能增强,比如注册拦截器、配置视图解析器、处理静态资源
public class SpringMVCConfig implements WebMvcConfigurer {
    //    自动装配拦截器bean以便下面切面方法使用
    @Autowired
    private ProjectInterceptor projectInterceptor;

    //    排除不想交给springmvc管理的包
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
        registry.addResourceHandler("/css/**").addResourceLocations("/css/");
        registry.addResourceHandler("/js/**").addResourceLocations("/js/");
        registry.addResourceHandler("/plugins/**").addResourceLocations("/plugins/");
    }

    //    切面方法,对/books下的方法设置拦截器,注意**表示/books下所有包和子包,*表示/books下的所有包但不包括子包
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(projectInterceptor).addPathPatterns("/books/**");
    }
}
拦截器参数
前置处理
java 复制代码
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        原始方法执行前方法,这里只是模拟一下所以直接打印,实际开发写自己的需求,比如权限的校验等
        System.out.println("preHandle...");
//        return true表示继续执行原始方法,如果return false表示拦截原始方法的执行
        return true;
    }

request:请求对象

response:响应对象

handler:被调用的处理器对象,本质上是一个方法对象,对反射技术中的method对象进行了再包装

返回值为false,被拦截的处理器将不执行

完成后处理
java 复制代码
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        在原始方法执行之后执行
        System.out.println("postHandle...");
    }

ex:如果处理器执行过程中出现异常对象可针对异常情况单独处理,但这个通常被自定义异常类替代了,没什么用

拦截器链(了解)

和过滤器一样,拦截器也可以配多个,配的顺序就是执行顺序

SpringMVCConfig

java 复制代码
@Autowired
private ProjectInterceptor projectInterceptor1;
@Autowired
private ProjectInterceptor projectInterceptor2;
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(projectInterceptor1).addPathPatterns("/books/**");
    registry.addInterceptor(projectInterceptor2).addPathPatterns("/books/**");
}

拦截器前置处理是顺序,后置处理和后后置处理是倒序

比如依次三个拦截器interceptor1,interceptor2,interceptor3,执行的顺序是

pre1=>pre2=>pre3=>原始方法=>post3=>post2=>post1=>afterCompletion3=>afterCompletion2=>afterCompletion1

所以其实叫拦截器栈更合适,先进后出嘛

然后如果在某个pre处return false,那后面的pre方法,原始方法和所有的post方法都不会执行,afterCompletion会从return false的后一个继续执行到最后

比如pre3返回false,执行pre1=>pre2=>pre3=>afterCompoletion2=>afterCompletion1

pre2返回false,执行pre1=>pre2=>afterCompletion1

pre1返回false,执行pre1

相关推荐
Arva .10 小时前
Spring基于XML的自动装配
xml·java·spring
王小王-12310 小时前
基于Hadoop的全国农产品批发价格数据分析与可视化与价格预测研究
大数据·hive·hadoop·flume·hadoop农产品价格分析·农产品批发价格·农产品价格预测
fatfishccc12 小时前
循序渐进学 Spring (上):从 IoC/DI 核心原理到 XML 配置实战
xml·java·数据库·spring·intellij-idea·ioc·di
回家路上绕了弯15 小时前
Spring AOP 详解与实战:从入门到精通
java·spring
计算机毕业设计木哥15 小时前
基于大数据spark的医用消耗选品采集数据可视化分析系统【Hadoop、spark、python】
大数据·hadoop·python·信息可视化·spark·课程设计
Laplaces Demon18 小时前
Spring 源码学习(十)—— DispatcherServlet
java·后端·学习·spring
写bug写bug19 小时前
彻底搞懂Spring Boot的系统监控机制
java·后端·spring
Lx35220 小时前
MapReduce性能调优:从理论到实践的经验总结
大数据·hadoop·后端
小小深1 天前
Spring进阶(八股篇)
java·spring boot·spring