当前位置: 首页 > article >正文

JavaWeb合集08-项目开发实战

八、项目开发

1、项目搭建

在这里插入图片描述

1.1 配置配置文件

application.properties文件

spring.application.name=mybatis_test

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/homepage
spring.datasource.username=root
spring.datasource.password=123456

#打开Mybatis日志信息
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

#开启mybatis的驼峰命名自动映射开关
mybatis.configuration.map-underscore-to-camel-case=true

2、接口设计规范-Restful

Restful开发规范:REST (REpresentational State Transfer), 表述性状态转换,它是一种软件架构风格。

在这里插入图片描述

在这里插入图片描述

注意:

  1. REST是风格,是约定方式,约定不是规定,可以打破。
  2. 描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如: users、books等。

3、日志小技巧

使用日志框架(Slf4j) 来写日志,直接在类上加上注解:@Slf4j 即可使用自动定义的 log对象中的方法来记录日志。

4、分页查询

分页查询思路:①要返回分页查询的数据;②要返回总记录数据;所以要执行两条sql,这时可以使用一个实体类来封装这两个返回数据,再一起返回给前端。

SQL分页查询命令:select * from user limit 起始页页码 , 每页总数;

SQL总记录命令:select count(*) from user;

起始页页码=(要查询的某页-1)* 每页总数

//封装数据的实体类
@Data
@NoArgsConstructor  //无参构造器
@AllArgsConstructor  //全参构造器
public class PageBean {
    private List total; //总记录数
    private List rows;// 当前页数据列表 
}

查询思路:

在这里插入图片描述

4.1 不使用插件手动编写分页查询
  1. Controller层

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
      //分页查询
        //@RequestParam的属性defaultValue可以来设置参数的默认值。
     @GetMapping("/{startNum}/{totalPage}")
     public Result getAllUserPage(@PathVariable Integer startNum,@PathVariable Integer totalPage){
    
     return new Result(true,"分页查询",userService.getAllUserPage(startNum,totalPage));
            
        }
    }
    
  2. Service层

    @Slf4j
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
    
        @Override
        public PageBean getAllUserPage(Integer startNum, Integer totalPage) {
            //1、查询总记录数
            Long  count =userDao.count();
    
            //2、查询每页的数据:查询起始页页码=(要查询的页码-1)*每页总数
            List<User> list=userDao.getAllUserPage((startNum-1)*totalPage,totalPage);
    
            //3、封装查询出来的两个数据
            PageBean pageBean=new PageBean(count,list);
    
            //4、返回查询出来的数据
            return  pageBean;
        }
    
    }
    
    
  3. Mapping层

    @Component
    @Mapper
    public interface UserDao {
    
      //1、查询总记录数
        @Select("select count(*) from tb_user")
        Long count();
    
        //2、查询每页的数据
        @Select("select * from tb_user limit #{startNum},#{totalPage}")
        List<User> getAllUserPage(Integer startNum, Integer totalPage);
    }
    
  4. 返回结果

    {
        "flag": true,
        "msg": "分页查询",
        "data": {
            "total": 5,
            "rows": [
                {
                    "uid": 2,
                    "email": "333@qq.com",
                    "password": "55",
                    "nickName": "昵称",
              
                },
                {
                    "uid": 54,
                    "email": "1234@qq.com",
                    "password": "123456",
                    "nickName": "永恒之月",
                }
            ]
        }
    }
    
4.2 使用PageHelper插件编写分页查询
  1. 在pom.xml 中下载PageHelper依赖

     <dependency>
         <groupId>com.github.pagehelper</groupId>
           <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.4.6</version>
    </dependency>
    
  2. Controller层

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
      //分页查询
        //@RequestParam的属性defaultValue可以来设置参数的默认值。
     @GetMapping("/{startNum}/{totalPage}")
     public Result getAllUserPage(@PathVariable Integer startNum,@PathVariable Integer totalPage){
    
     return new Result(true,"分页查询",userService.getAllUserPage(startNum,totalPage));
            
        }
    }
    
  3. Service层

    @Slf4j
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
    
      @Override
        public PageBean getAllUserPage(Integer startNum, Integer totalPage) {
    
            log.info("开始页码:"+startNum+"  每页数量"+totalPage);
            //1、设置设置分页参数(将传递过来的分页参数作为参数)
            PageHelper.startPage(startNum,totalPage);
    
            //2、执行查询,将查询结果强制转换为Page类型
             List<User> userList= userDao.getAllUserPage();
             Page<User> p= (Page<User>) userList;
    
            //3、封装查询出来的两个数据(通过Page对象的两个静态方法来获取这两个结果)
            PageBean pageBean=new PageBean(p.getTotal(),p.getResult());
    
            //4、返回查询出来的数据
            return  pageBean;
        }
    
    }
    
    
  4. Mapping层

    @Component
    @Mapper
    public interface UserDao {
    
        //直接写上查询全部信息方法,其它交给PageHelper完成sql拼接
         @Select("select * from tb_user")
        List<User> getAllUserPage();
    }
    

5、条件分页查询

分页查询实现思路

在这里插入图片描述

  1. Controller层

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        //@RequestParam通过该注解来设置前端传递过来的表单参数,如果参数为空可以通过defaultValue 来设置默认值;required=false是当前端没有传值过来是默认为空
        @GetMapping
        public Result getUserByInfo(@RequestParam(defaultValue ="1") Integer page,@RequestParam(defaultValue = "10") Integer pageSize,
                                    @RequestParam(required=false) String email,@RequestParam(required=false) String nickName,@RequestParam(required=false) String sex,@RequestParam(required = false) Integer admin){
            return  new Result(true,"条件分页查询",userService.getUserByInfo(page,pageSize,email,nickName,sex,admin));
        }
    }
    
    
  2. Service层

    @Slf4j
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
        @Override
        public PageBean getUserByInfo(Integer page,Integer pageSize,String email,String nickName,String sex,Integer admin) {
            //1、设置设置分页参数(将传递过来的分页参数作为参数)
            PageHelper.startPage(page,pageSize);
            //2、执行查询,将查询结果强制转换为Page类型
           List<User> userList= userDao.getUserByInfo(email,nickName,sex,admin);
            Page<User> p= (Page<User>) userList;
            //3、封装查询出来的两个数据(通过Page对象的两个静态方法来获取这两个结果)
            PageBean pageBean=new PageBean(p.getTotal(),p.getResult());
            //4、返回查询出来的数据
            return  pageBean;
        }
    
    }
    
  3. Mapper层与xml映射

    @Component
    @Mapper
    public interface UserDao {
        List<User> getUserByInfo(String email,String nickName,String sex,Integer admin);
    }
    
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.yhzyai.dao.UserDao">
    
        <select id="getUserByInfo" resultType="com.yhzyai.pojo.User">
            select * from tb_user
            <where>
                <if test="email != null">
                    email like concat('%',#{email},'%')
                </if>
                <if test="nickName != null">
                    and nick_name like concat('%',#{nickName},'%')
                </if>
                <if test="sex != null">
                    and   sex=#{sex}
                </if>
                <if test="admin != null">
                    and admin=#{admin}
                </if>
            </where>
            order by email
    
        </select>
    
    </mapper>
    
    

6、批量删除数据

批量删除执行思路

在这里插入图片描述

  1. Controller层

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
    
        //批量删除用户
        @DeleteMapping("/{emails}")
        public Result delUsersById(@PathVariable List<String> emails){
            return new Result(userService.delUsersById(emails),"批量删除用户",null);
        }
    }
    
  2. Service层

    @Slf4j
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
    
        //批量删除用户
        @Override
        public boolean delUsersById(List<String> emails) {
            int listSize=emails.size();
            int delRows= userDao.delUsersById(emails);
            log.info("集合长度:"+listSize+" ,成功删除数量:"+delRows);
            return listSize==delRows;
        }
    
    }
    
  3. Mapper层和xml映射文件

    @Component
    @Mapper
    public interface UserDao {
        int delUsersById(List<String> emails);
    }
    
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.yhzyai.dao.UserDao">
    
    
    <!--  delete from tb_user where email in ("123@qq.com","234@qq.com");-->
        <delete id="delUsersById">
            delete from tb_user where email in <foreach collection="emails" item="email" separator="," open="(" close=")">
            #{email}
        </foreach>
        </delete>
    
    </mapper>
    
    
  4. 前端接口请求

    在这里插入图片描述

7、新增数据

新增数据实现思路

在这里插入图片描述

  1. Controller层

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
        @Autowired
        private UserService userService;
    
    
         //新增用户
        @PostMapping
        public Result addUser(@RequestBody User user){
            return new Result(userService.addUser(user),"新增用户");
        }
    }
    
  2. Service层

    @Slf4j
    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserDao userDao;
    
    
        //新增用户
          @Override
        public boolean addUser(User user) {
            return userDao.addUser(user)==1;
        }
    
    }
    
  3. Mapper层和xml映射文件

    @Component
    @Mapper
    public interface UserDao {
            int addUser(User user);
    }
    
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.yhzyai.dao.UserDao">
    
    
     <insert id="addUser">
            insert into tb_user
            <trim prefix="(" suffix=")" suffixOverrides=",">
                <if test="email != null ">
                    email,
                </if>
                <if test="password != null ">
                    password,
                </if>
                <if test="nickName != null ">
                    `nick_name`,
                </if>
                <if test="fase != null ">
                    fase,
                </if>
                <if test="admin != null ">
                    admin,
                </if>
                <if test="userStatus != null ">
                    `user_status`,
                </if>
                <if test="sex != null ">
                    sex,
                </if>
                <if test="birthday != null ">
                    birthday
                </if>
    
            </trim>
            <trim prefix="values (" suffix=")" suffixOverrides=",">
                <if test="email != null ">
                    #{email},
                </if>
                <if test="password != null ">
                    #{password},
                </if>
                <if test="nickName != null ">
                    #{nickName},
                </if>
                <if test="fase != null ">
                    #{fase},
                </if>
                <if test="admin != null ">
                    #{admin},
                </if>
                <if test="userStatus != null ">
                    #{userStatus},
                </if>
                <if test="sex != null ">
                    #{sex},
                </if>
                <if test="birthday != null ">
                    #{birthday}
                </if>
            </trim>
        </insert>
    
    </mapper>
    
    

8、文件上传

文件上传,是指将本地图片、视频、音频等文件,上传到服务器,供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件.上传功能。

前端页面三要素:①使用file类型的输入框;②必须使用POST上传方式;③enctype=multipart/form-data,表示表单的编码格式为二进制格式。

后端接收使用:MultipartFile类型,并且参数名要与前端的表单名保持一致。

在这里插入图片描述

8.1 文件存储-文件本地存储

文件存储分为:本地文件存储和云存储(阿里云OSS)

文件本地存储:就是将文件保存到服务器的本地磁盘中。

常用的MultipartFile对象方法

String getOriginalFilename(); //获取原始文件名

void transferTo(File dest); //将接收的文件转存到磁盘文件中心

long getSize(); //获取文件的大小,单位:字节

byte[] getBytes(); //获取文件内容的字节数组

InputStream getInputStream(); //获取接收到的文件内容的输入流

注意:在SpringBoot中,文件上传,默认单个文件允许最大大小为1M。如果需要上传大文件,可配置

#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB

#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
  1. Controller层

    @RestController
    @RequestMapping("/resources")
    public class ResourceController {
    
    @Autowired
        private ResourceService resourceService;
    
    @PostMapping
        public Result uploads(@RequestParam String tag, @RequestParam String type,@RequestParam  String email,@RequestParam  MultipartFile file) throws Exception {
    
        //将数据封装到对象中
        Resource resource=new Resource();
        resource.setTag(tag);
        resource.setEmail(email);
        resource.setType(type);
    
        return new Result(resourceService.upload(resource,file),"文件上传");
    }
    }
    
    
  2. Service层

    @Service
    public class ResourceServiceImpl implements ResourceService {
        @Autowired
        private ResourceDao resourceDao;
    
        @Override
        public boolean upload(Resource resource, MultipartFile file) throws Exception {
             //1、获取上传文件的名字(全名+后缀),获取后缀名
             String fileFullName=file.getOriginalFilename();
             int spotIndex=fileFullName.lastIndexOf('.'); //获取最有一个"."的下标位置
             String suffixName=fileFullName.substring(spotIndex);  //获取后缀名
    
            //2、生成UUID,作为文件名保存
            String uuid=UUID.randomUUID().toString();
            String fileName=uuid+suffixName;
    
            //3、获取到要上传到服务器的目录
            ApplicationHome applicationHome=new ApplicationHome(this.getClass()); //获取到项目本身的目录
            String pre=applicationHome.getDir().getParentFile().getParentFile()+
                    "\\src\\main\\resources\\static\\wallpaper\\";
    
            //4、将文件保存在服务器端目录下(要抛异常)
             file.transferTo(new File(pre+fileName));
    
             //5、生成文件的在线访问链接,添加到对象属性中
             resource.setRUrl("http://locahost:8080/resources/static/wallpaper"+fileName);
             resource.setRid(uuid);    //添加id
             resource.setUpDate(LocalDateTime.now()); //添加上传时间
    
            //6、将对象传给Mapping中的方法,插入到的数据库中
            return resourceDao.upload(resource)==1;
        }
    }
    
  3. Mapping层

    @Mapper
    @Component
    public interface ResourceDao {
    
        @Insert("insert into tb_resource(rid, tag, type, r_url, email, up_date) values (#{rid},#{tag},#{type},#{rUrl}," +
                "#{email},#{upDate})")
        public int  upload(Resource resource);
    }
    
8.2 文件存储-阿里云OSS

阿里云对象存储OSS ( Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

第三方服务-通用思路:①准备工作(账户注册等);②参照官方SDK参照官方软件开发工具包编写入门程序;③集成使用。

SDK: Software Development Kit的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包) 、 代码示例等,都可以叫做SDK。

Bucket:存储空间是用户用于存储对象(Object, 就是文件)的容器,所有的对象都必须隶属于某个存储空间。

准备工作:

①注册并实名认证阿里云账户

②小容量无需充值

③直接搜索OSS,找到对象存储OSS,并开通其服务。

④进入OSS中管理面板中,创建bucket,自需要选择地区,以及设置为公共读取设置。

⑤鼠标放到头像的地方,选择AccessKey管理,创建AccessKey,保存对应的AccessKey和AccessKeySecret

⑥参照阿里云提供的SDK文档,来编写入门程序。

⑦将OSS集成到项目中来使用,将OSS作为一个工具类来使用。

在这里插入图片描述

8.2.1 阿里云OSS-集成

集成OSS实现思路

在这里插入图片描述

下面的代码是在插入数据的同时,上传文件(携带参数上传文件),但是这样有个弊端,就是如果参数比较多时比较繁琐,需要一个一个的编写对应的形参,并且在修改数据时,又要重新时间上传文件的接口,用户可能不修改头像,只修改基本信息,这时就会导致空指针异常,比较繁琐。所以推荐将上传文件单独设计成一个接口来使用。

  1. 创建阿里云OSS工具类

    package com.yhzyai.util;
    
    import com.aliyun.oss.*;
    import com.aliyun.oss.common.auth.CredentialsProviderFactory;
    import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
    import org.springframework.stereotype.Component;
    import org.springframework.web.multipart.MultipartFile;
    
    
    import java.io.InputStream;
    import java.util.UUID;
    
    //将类交给IOC容器管理(这样就用于去创建对象来调用其方法了)
    @Component
    //通过样例将OSS打造为一个工具类
    public class AliyunOSS {
        // Endpoint以华北2(北京)为例,其它Region请按实际情况填写。 概括->外网访问
        private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    
    
        // 填写Bucket名称,例如examplebucket。
       private String bucketName = "yhzy-resource";
    
    
        //文件上方法(返回上传后文件的访问路径)
        public  String upload(MultipartFile file) throws Exception {
            // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
           EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
    
            //1、获取上传文件的输入流
            InputStream inputStream=file.getInputStream();
    
            //2、避免覆盖,生成UUID作为文件名
            String fileFullName=file.getOriginalFilename();
            String fileName= UUID.randomUUID().toString()+fileFullName.substring(fileFullName.lastIndexOf("."));
    
            // 上传到OSS
            OSS ossClient=new OSSClientBuilder().build(endpoint,credentialsProvider);
             ossClient.putObject(bucketName,fileName,inputStream);
    
            //获取文件上传后的路径:https://bucket的名字.地区的路径/文件名字
            String url=endpoint.split("//")[0]+"//"+bucketName+"."+endpoint.split("//")[1]+"/"+fileName;
            //关闭 ossClient
            ossClient.shutdown();
    
            //返回文件访问的路径
            return url;
        }
    
    }
    
  2. Controller层

    @RestController
    @RequestMapping("/resources")
    public class ResourceController {
    
    @Autowired
        private ResourceService resourceService;
    
    @PostMapping
        public Result insert(@RequestParam String tag, @RequestParam String type,@RequestParam  String email,@RequestParam  MultipartFile file) throws Exception {
    
        //将数据封装到对象中
        Resource resource=new Resource();
        resource.setTag(tag);
        resource.setEmail(email);
        resource.setType(type);
    
        return new Result(resourceService.insert(resource,file),"文件上传");
    }
    
    }
    
  3. Service层

    @Service
    public class ResourceServiceImpl implements ResourceService {
        @Autowired
        private ResourceDao resourceDao;
    
        //注入阿里云OSS工具类
        @Autowired
        private AliyunOSS aliyunOSS;
    
    
        @Override
        public boolean insert(Resource resource, MultipartFile file) throws Exception {
    
            //1、通过ICO容器来调用工具类的方法,传入文件。
             String fileUrl= aliyunOSS.upload(file);
    
    
            //2、将信息添加的对象中
            resource.setRUrl(fileUrl);
            resource.setRid(fileUrl.substring(fileUrl.lastIndexOf("/")));    //添加id
            resource.setUpDate(LocalDateTime.now()); //添加上传时间
    
            //3、将对象传给Mapping中的方法,插入到的数据库中
            return resourceDao.insert(resource)==1;
        }
    }
    
    
  4. Mapping层

    @Mapper
    @Component
    public interface ResourceDao {
    
        @Insert("insert into tb_resource(rid, tag, type, r_url, email, up_date) values (#{rid},#{tag},#{type},#{rUrl}," +
                "#{email},#{upDate})")
        public int  insert(Resource resource);
    }
    

9、修改数据

实现思路:通过ID查询出对应数据显示出来,再对数据进行修改。

①前端通过点击编辑按钮,调用根据ID查询数据的接口,查询出数据,并展示。

②用户编辑基本信息,点击提交按钮,调用修改基本信息接口。

③用户点击上传文件按钮,选择文件后确定,自动调用文件上传接口,将文件上传到阿里云OSS中,并返回访问文件访问链接,就是原链接没有变化,就是将文件进行了替换。

在这里插入图片描述

  1. 阿里云OSS-工具类

    package com.yhzyai.util;
    
    import com.aliyun.oss.*;
    import com.aliyun.oss.common.auth.CredentialsProviderFactory;
    import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
    import org.springframework.stereotype.Component;
    import org.springframework.web.multipart.MultipartFile;
    
    
    import java.io.InputStream;
    import java.util.UUID;
    
    //将类交给IOC容器管理(这样就用于去创建对象来调用其方法了)
    @Component
    //通过样例将OSS打造为一个工具类
    public class AliyunOSS {
        // Endpoint以华北2(北京)为例,其它Region请按实际情况填写。 概括->外网访问
        private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    
        // 填写Bucket名称,例如examplebucket。
       private String bucketName = "yhzy-resource";
    
    
        //文件上方法(返回上传后文件的访问路径)
        public  String upload(MultipartFile file) throws Exception {
            // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
           EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
    
            //1、获取上传文件的输入流
            InputStream inputStream=file.getInputStream();
    
            //2、避免覆盖,生成UUID作为文件名
            String fileFullName=file.getOriginalFilename();
            String fileName= UUID.randomUUID().toString()+fileFullName.substring(fileFullName.lastIndexOf("."));
    
            // 上传到OSS
            OSS ossClient=new OSSClientBuilder().build(endpoint,credentialsProvider);
             ossClient.putObject(bucketName,fileName,inputStream);
    
            //获取文件上传后的路径:https://bucket的名字.地区的路径/文件名字
            String url=endpoint.split("//")[0]+"//"+bucketName+"."+endpoint.split("//")[1]+"/"+fileName;
            //关闭 ossClient
            ossClient.shutdown();
    
            //返回文件访问的路径
            return url;
        }
    
    
    
    
        //文件上方法(返回上传后文件的访问路径)
        public  String update(MultipartFile file,String filePath) throws Exception {
            // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
            EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
    
            //1、获取上传文件的输入流
            InputStream inputStream=file.getInputStream();
    
            //2、从文件路径中截取出文件名,使其覆盖远有我文件,达到修改的效果。
            String fileName =filePath.substring(filePath.lastIndexOf("/")+1);
    
            // 上传到OSS
            OSS ossClient=new OSSClientBuilder().build(endpoint,credentialsProvider);
            ossClient.putObject(bucketName,fileName,inputStream);
    
            //关闭 ossClient
            ossClient.shutdown();
    
            //返回文件访问的路径
            return fileName;
        }
    }
    
    
  2. Controller层

    @RestController
    @RequestMapping("/resources")
    public class ResourceController {
    
    @Autowired
        private ResourceService resourceService;
    
    @Autowired
    private AliyunOSS aliyunOSS;
    
    
        //根据ID查询
     @GetMapping("/{rid}")
    public Result  getResourceById(@PathVariable String rid){
      return new Result(true,"根据Id查询",resourceService.getResourceById(rid));
        }
    
    //文件上传,文件修改(只要Contrller层就行)
    @PostMapping("/uploadUpdate")
    public Result uploadUpdate(MultipartFile file,String filePath) throws Exception {
        //直接调用工具类
      String fileNewPath=aliyunOSS.update(file,filePath);
      //返回文件访问路径
      return new Result(true,"单文件上传",fileNewPath);
    }
    
      //只修改基本信息
       @PutMapping
        public Result update(@RequestBody Resource resource){
           System.out.println(resource.toString());
         return new Result(resourceService.update(resource),"修改基本信息");
     }
    }
    
    
  3. Service层

    @Service
    public class ResourceServiceImpl implements ResourceService {
        @Autowired
        private ResourceDao resourceDao;
    
        //注入阿里云OSS工具类
        @Autowired
        private AliyunOSS aliyunOSS;
        
       @Override
        public boolean upload(Resource resource, MultipartFile file) throws Exception {
    
            //1、通过ICO容器来调用工具类的方法,传入文件。
             String fileUrl= aliyunOSS.upload(file);
    
    
            //2、将信息添加的对象中
            resource.setRUrl(fileUrl);
            resource.setRid(fileUrl.substring(fileUrl.lastIndexOf("/")));    //添加id
            resource.setUpDate(LocalDateTime.now()); //添加上传时间
    
            //6、将对象传给Mapping中的方法,插入到的数据库中
            return resourceDao.upload(resource)==1;
        }
    
    
        @Override
        public Resource getResourceById(String rid) {
            return resourceDao.getResourceById(rid);
        }
    
        @Override
        public boolean update(Resource resource) {
            resource.setUpDate(LocalDateTime.now());
            return resourceDao.update(resource)==1;
        }
    }
    
  4. Mapping层和对应的映射文件

    @Mapper
    @Component
    public interface ResourceDao {
    
        @Insert("insert into tb_resource(rid, tag, type, r_url, email, up_date) values (#{rid},#{tag},#{type},#{rUrl},#{email},#{upDate})")
        int  upload(Resource resource);
    
        @Select("select * from tb_resource where rid=#{rid}")
        Resource getResourceById(String rid);
    
        int update(Resource resource);
    }
    
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.yhzyai.dao.ResourceDao">
    
    <update id="update">
        update tb_resource
        <set>
        <if test="tag != null and tag != ''">
            tag=#{tag},
        </if>
        <if test="type != null and type != ''">
            type=#{type},
        </if>
        <if test="rUrl != null and rUrl != ''">
            r_url=#{rUrl},
        </if>
        <if test="email != null and email != ''">
            email=#{email},
        </if>
        <if test="upDate != null">
            up_date=#{upDate}
        </if>
        </set>
        where rid=#{rid}
    
    </update>
    
    </mapper>
    
    

10、配置文件

10.1 参数配置化

就是将一些常用,切重复的配置项,定义到配置文件中,通过@value来进行注入调用。

由于之前定义的阿里云OSS工具类中有一些特殊的变量,如果多个工具类都要用到相同的配置,或是要改变时,需要找到对应的工具类来进行修改,太过繁琐,这时可以将对应的变量定义到配置文件中(application.properties),通过@Value来引用配置的变量。

@Value注解:通常用于外部配置的属性注入,具体用法为: @Value(“${配置文件中的key}”)

#application.properties
#名字可以自己定义(尽量定义的有意义)
#阿里云OSS
aliyun.oss.endpoint=https://oss-cn-beijing.aliyuncs.com
aliyun.oss.bucketName=yhzy-resource
//将类交给IOC容器管理(这样就用于去创建对象来调用其方法了)
@Component
//通过样例将OSS打造为一个工具类
public class AliyunOSS {
    // Endpoint以华北2(北京)为例,其它Region请按实际情况填写。 概括->外网访问
    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    // 填写Bucket名称,例如examplebucket。
   @Value("${aliyun.oss.bucketName}")
    private String bucketName;


    //文件上方法(返回上传后文件的访问路径)
    public  String upload(MultipartFile file) throws Exception {
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();

        //1、获取上传文件的输入流
        InputStream inputStream=file.getInputStream();

        //2、避免覆盖,生成UUID作为文件名
        String fileFullName=file.getOriginalFilename();
        String fileName= UUID.randomUUID().toString()+fileFullName.substring(fileFullName.lastIndexOf("."));

        // 上传到OSS
        OSS ossClient=new OSSClientBuilder().build(endpoint,credentialsProvider);
        ossClient.putObject(bucketName,fileName,inputStream);

        //获取文件上传后的路径:https://bucket的名字.地区的路径/文件名字
        String url=endpoint.split("//")[0]+"//"+bucketName+"."+endpoint.split("//")[1]+"/"+fileName;
        //关闭 ossClient
        ossClient.shutdown();

        //返回文件访问的路径
        return url;
    }




    //文件上方法(返回上传后文件的访问路径)
    public  String update(MultipartFile file,String filePath) throws Exception {
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();

        //1、获取上传文件的输入流
        InputStream inputStream=file.getInputStream();

        //2、从文件路径中截取出文件名,使其覆盖远有我文件,达到修改的效果。
        String fileName =filePath.substring(filePath.lastIndexOf("/")+1);

        // 上传到OSS
        OSS ossClient=new OSSClientBuilder().build(endpoint,credentialsProvider);
        ossClient.putObject(bucketName,fileName,inputStream);

        //关闭 ossClient
        ossClient.shutdown();

        //返回文件访问的路径
        return fileName;
    }
}

10.2 yml 配置文件

基本语法如下

  1. 大小写敏感。

  2. 数值前边必须有空格,作为分隔符。

  3. 使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)。

  4. 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可。

  5. #表示注释,从这个字符一直到行尾,都会被解析器忽略。

#yml配置文件两种常用格式如下:

#定义对象/Map集合
user :
	name: Tom
	age: 20
	address: beijing

#定义数组/List/Set集合
hobby:
	- java
	- C
	- game
	- sport

将以前的配置内容通过yml文件来代替:

server:
  port: 8080

spring:
  #数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://locahost:3306/homepage
    username: root
    password: 123456
    #文件上传大小配置
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 100MB


#Mybatis配置(日志和驼峰命令)
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true


#阿里云OSS配置
aliyun:
  oss:
    endpoint: https://oss-cn-beijing.aliyuncs.com
    bucketName: yhzy-resource


10.3 @ConfigurationProperties注解

将多个配置项注入对应的类中。

操作步骤:

①在yml配置文件中自定义配置项;

②将这些配置项属性封装为一个实体类,加上@Data注解,加上@Compnent注解使其添加到IOC容器中;

③在需要引用的地方加上@ConfigurationProperties(prefix=“引用配置项的固定路径”)注解;

④使用@Autowrite注解,引入实体类对象。

⑤通过实体类对象来获取对应的配置项。

@ConfigurationProperties与@Value的相同与不同点

  • 相同点:都是用来注入外部配置的属性的。
  • 不同点:@Value注解只能一个一个的进行外部属性的注入。@ConfigurationPropertiesi可以批量的将外部的属性配置注入到bean对象的属性中。

11、用户登录技术

11.1 会话技术

会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一-方断开连接, 会话结束。在一-次会话中可以包含多次请求和响应。

会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。

会话跟踪方案:①客户端会话跟踪技术: Cookie;②服务端会话跟踪技术: Session;③令牌技术

  1. Cookie

    通过在服务端设置Cookie,第一次通过HTTP请求在响应头的set-cookie里可找到服务端项浏览器端设置的cookie,往后每一次请求,浏览器端都会携带请求头里的Cookie值。(响应头里:set-cookie,获取cookie;请求头里:cookie,用来验证请求)

    @S1f4j
    @RestController
    public class SessionController {
    //设置Cookie
    @GetMapping ("/c1")
    public Result cookie1 (HttpServletResponse response) {
    response.addCookie (new Cookie( name: "login_ username",value: "11111")); // 设置Cookie/响应Cookie
    return Result. success();
    }
        
    //获取Cookie
    @GetMapping ("/c2")
    public Result cookie2 (HttpServletRequest request) {
    Cookie[] cookies = request .getCookies(); // 获取所有的Cookie
    for (Cookie cookie : cookies) {
    if (cookie.getName().equals ("login_ _username")){ 
        //输出name为login_ username 的cookie
    System. out.println("login_ _username: "+cookie.getValue());
    }
    return Result. success() ;
    
    }
    
    
    
    
    
    }
    

    优点: HTTP协议中支持的技术
    缺点:①移动端APP无法使用ookie;②不安全,用户可以自己禁用Cookie;③Cookie不能跨域

  2. Session

    也是通过响应头和请求头的set-cookie和cookie来传输session

    @S1f4j
    @RestController
    public class SessionController {
    @GetMapping ("/s1")
    public Result sessionl (HttpSession session) {
    log.info ("HttpSession-s1: {}", session.hashCode());
    session.setAttribute ( name: "loginUser", value: "tom"); // 往session中存储数据
    return Result. success();
    }  
        
    //从HttpSession中获取值
    @GetMapping ("/s2")
    public Result session2 (HttpServletRequest request) {
    HttpSession session = request.getSession();
    log.info("HttpSession-s2: {}", session.hashCode());
    Object loginUser = session.getAttribute ( name: "loginUser"); // 从session中获取数据
    log.info ("loginUser: {}", loginUser) ;
    return Result. success (loginUser) ;
    }
    }
    

    优点:存储在服务端,安全
    缺点:①服务器集群环境下无法直接使用Session;②Cookie的缺点

  3. 令牌技术

优点:支持PC端、移动端;②解决集群环境下的认证问题;③减轻服务器端存储压力

缺点:需要自己实现

11.2 JWT 令牌技术

全称:JSON Web Token (https:/ /jwt.io/)

定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

Base64:是一种基于64个可打印字符(A-Z a-z0-9 +. /)来表示二进制数据的编码方式。

三个组成部分:

第一部分: Header(头) ,记录令牌类型、签名算法等。例如: {“alg”:“HS256”,“type’”:“WT”}

第二部分: Payload(有效载荷),携带一些自定义信息、 默认信息等。例如: {“id”:“1”,“username”:“Tom”}

第三部分: Signature(签名),防止Token被篡改、确保安全性。将header. payload, 并加入指定秘钥,通过指定签名算法计算而来。

在这里插入图片描述

在这里插入图片描述

11.2.1 JWT -生成与校验
  1. 引入JWT依赖

          <!--JWT令牌-->
            <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-api</artifactId>
                <version>0.11.5</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-impl</artifactId>
                <version>0.11.5</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-jackson</artifactId>
                <version>0.11.5</version>
            </dependency>
    
  2. 生成和校验JWT

    @SpringBootTest
    class MybatisTestApplicationTests {
    
        @Test
        //创建JWT
        void getJwt() {
            Map<String, Object> claims = new HashMap<>();
            claims.put("name", "小明");
            claims.put("sex", "男");
    
            // 生成一个安全的密钥
            byte[] keyBytes = generateSecureKey();
    
            // 将字节数组转换为Base64编码的字符串以便打印和存储
            String encodedKey = Base64.getEncoder().encodeToString(keyBytes);
    
            // 创建JWT
            Key key = Keys.hmacShaKeyFor(keyBytes);  //创建一个密钥对象,参数为字节数组
            String jwt = Jwts.builder()
                    .setClaims(claims)   // 自定义内容(载荷)
                    .signWith(key,SignatureAlgorithm.HS256)   // 设置密钥 和 设置加密算法为HS256
                    .setExpiration(new Date(System.currentTimeMillis() + 2 * 3600 * 1000))  // 设置过期时间为2小时,时间过期会报错
                    .compact();
    
            System.out.println("字节数组的密钥:"+Arrays.toString(keyBytes));
            System.out.println("Base64的密钥: " + encodedKey);
            System.out.println("生成的JWT: " + jwt);
    
            //调用解密的方法(传入字节数组密钥和JWt)
           Claims claimsParse=ParseJwt(keyBytes,jwt);
            System.out.println("解密结果:"+claimsParse);
        }
    
    
        //生成32位的字节数组作为密钥
        private byte[] generateSecureKey() {
            // 生成一个安全的随机字节数组作为密钥
            SecureRandom secureRandom = new SecureRandom();
            byte[] keyBytes = new byte[32]; // 256 bits / 32 bytes
            secureRandom.nextBytes(keyBytes);
            return keyBytes;
        }
    
    
        //JWT解密(校验)
        private  Claims ParseJwt(byte[] keyBytes ,String jwt){
            // 创建JWT解析器
            Key key = Keys.hmacShaKeyFor(keyBytes);  //创建一个密钥对象,参数为字节数组
            Claims claims=Jwts.parserBuilder()
                    .setSigningKey(key)   //指定签名密钥(要与生成的密钥相同)
                    .build()
                    .parseClaimsJws(jwt)  //解析令牌
                    .getBody();
    
            return claims;
        }
    
    }
    
11.2.2 JWT -登录实现工具类

令牌生成:登录成功后,生成JWT令牌,并返回给前端。

令牌校验:在请求到达服务端后,对令牌进行统一拦截、校验。

package com.yhzyai.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import java.security.Key;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;
import java.util.Map;

public class JwtUtil {

    //密钥字节数组
   private static byte[] keyBytes={42, -81, 2, -42, -74, 119, -17, -74, 23, 88, 22, 94, 37, -52, 95, 87, 39, -8, 103, -68, 62, 79, -14, 24, -32, -13, -97, 106, 6, -43, 109, 32};
   //过期时间
   private static Long expire=12*3600*1000L;


   //生成JWT
   public static String generateJwt(Map<String,Object> claims){
      String encodedKey= Base64.getEncoder().encodeToString(keyBytes);
      //生成JWT
        Key key= Keys.hmacShaKeyFor(keyBytes); //创建一个密钥对象,参数为字节数组
        String jwt= Jwts.builder()
                .setClaims(claims)
                .signWith(key, SignatureAlgorithm.HS256)
                .setExpiration(new Date(System.currentTimeMillis()+expire))
                .compact();
         return jwt;
   }

   public static Claims parseJWT(String jwt){

       Key key=Keys.hmacShaKeyFor(keyBytes);  //创建一个密钥对象,参数为字节数组
       Claims claims=Jwts.parserBuilder()
               .setSigningKey(key)
               .build()
               .parseClaimsJws(jwt)
               .getBody();

       return claims;
   }


    //生成32位的字节数组作为密钥
    private byte[] generateSecureKey() {
        // 生成一个安全的随机字节数组作为密钥
        SecureRandom secureRandom = new SecureRandom();
        byte[] keyBytes = new byte[32]; // 256 bits / 32 bytes
        secureRandom.nextBytes(keyBytes);
        return keyBytes;
    }
}

11.3 过滤器 Filter

概念: Filter 过滤器,是JavaWeb三大组件(Servlet、Filter. Listener)之一 。

过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。

过滤器一般完成一 些通用的操作,比如:登录校验、统一编码处理、 敏感字符处理等。

11.3.1 快速入门
  1. 定义Filter: 定义- -个类,实现Filter接口,并重写其所有方法。
  2. 配置Filter: Filter类.上加@WebFilter注解,配置拦截资源的路径。引导类.上加@ServletComponentScan开启Servlet组件支持。

在这里插入图片描述

11.3.2 Filter执行流程

Filter执行流程:浏览器发起请求,Filter进行请求拦截,执行放行前的逻辑(jwt验证等);放行请求操作数据库;执行放行后的逻辑。

注意:操作完数据库后,会回到Filter,执行放行后的逻辑代码。

chain.doFilter (request, response); //放行代码

在这里插入图片描述

11.3.3 Filter 拦截路径

Filter可以根据需求,配置不同的拦截资源路径:

@WebFilter (urlPatterns ="/*"){    //*代表拦截全部路径
public class DemoFilter  implements Filter
}
拦截路径urlPatters值说明
拦截具体的路径/login只有访问/login路径时,才会被拦截
拦截目录/emps/*访问/emps下及其本身的所有资源,都会被拦截
拦截所有/*访问所有资源,都会被拦截
11.3.4 Filter 过滤器链

介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。

当第一个过滤器放行后,它会放行到下一个过滤器,直到最后一个过滤器放行,才放行到web资源中。

在这里插入图片描述

注意:过滤器的执行顺序与Filter的类名有关,按照类名的字母顺序进行执行过滤器。

11.3.5 Filter 登录校验

不是所有的请求都要进行拦截JWT校验,有一 个例外,就是登录请求。

拦截到请求后,有令牌, 且令牌校验通过(合法) ;否则都返回未登录错误结果

登录校验执行流程:

  1. 获取请求url。
  2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
  3. 获取请求头中的令牌( token)。
  4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)
  5. 解析token,如果解析失败。返回错误结果(未登录)。
  6. 放行。

在这里插入图片描述

Filter工具类:

@Slf4j
//拦截所有请求
@WebFilter(urlPatterns = "/*")
public class FilterUtil implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        //先将servletRequest 和 servletResponse对象强制转换为HttpServletRequest和HttpServletResponse对象(要获取链接和token)
        HttpServletRequest request= (HttpServletRequest) servletRequest;
        HttpServletResponse response= (HttpServletResponse) servletResponse;

//        1. 获取请求url。
        String url=request.getRequestURL().toString();
        log.info("请求的URL为:"+url);
//        2. 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
           if(url.contains("/login")){
            filterChain.doFilter(request,response);
            return;   //放行后就不执行Filter的后面代码了
           }

//        3. 获取请求头中的令牌( token)。
         String token=request.getHeader("token");

//        4. 判断令牌是否存在,如果不存在,返回错误结果(未登录),判断token是否有长度,有true。
           if(!StringUtils.hasLength(token)){
               //没有长度,响应前端错误信息
              Result result=new Result(false,"Not Login");
              //手动的将对象信息,转换为JSON字符串返回,使用阿里巴巴的fastJSON
               /* <dependency> <groupId>com.alibaba</groupId><artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency>*/
               String errorMsg=JSONObject.toJSONString(result);
               //通过响应体里的输出流来将信息响应给前端
               response.getWriter().write(errorMsg);
              return;
           }

//        5. 解析token,如果解析失败。返回错误结果(未登录)。(校验成功不报错,校验失败会报错)
         try {
             //调用JWT工具类进行jwt校验
             JwtUtil.parseJWT(token);
        } catch (Exception e) {
             //校验失败,返回错误信息
             Result result=new Result(false,"Not Login");
             //手动的将对象信息,转换为JSON字符串返回,使用阿里巴巴的fastJSON
             String errorMsg=JSONObject.toJSONString(result);
             //通过响应体里的输出流来将信息响应给前端
             response.getWriter().write(errorMsg);
             return;
        }

//        6. 放行(到这里说明没有报错)。
           filterChain.doFilter(request,response);

    }
}
11.4 拦截器 Interceptor

概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。

作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。

11.4.1 快速入门
  1. 定义拦截器,实现HandlerInterceptor接口, 并重写其所有方法(ctrl+O)。
  2. 注册拦截器

在这里插入图片描述

11.4.2 拦截器-拦截路径

拦截路径可以根据需求配置

在这里插入图片描述

拦截路径含义例子
/*一级路径能匹配/depts, /emps, /login, 不能匹配/depts/1
/**任意路径能匹配/depts, /depts/1, /depts/1/2
/depts/*/depts 下的一级路径能匹配/depts/1,不能匹配/depts/1/2, /depts
/depts/**/depts下的任意级路径能匹配/ depts, /depts/1, /depts/1/2, 不能匹配/emps/1

拦截器执行流程

在这里插入图片描述

Filter与Interceptor

接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。

拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只 会拦截Spring环境中的资源。

11.5 异常处理

程序开发过程中不可避免的会遇到异常现象,要如何规范的响应错误信息给前端。

当操作Mapping层发送错误时,会逐层向上抛异常,这时我们可以定义一个全局异常处理器。

在这里插入图片描述

@RestControllerAdvice = @ControllerAdvice + @ResponseBody,所有返回结果会自动转换为JSON格式。

//全局异常处理器(工具类)
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)  //捕获全部的异常
    public Result  ex(Exception exception){
        exception.printStackTrace();
        return new Result(false,"操作错误,请联系管理员");
    }
}

http://www.kler.cn/news/355364.html

相关文章:

  • 1022. 宠物小精灵之收服
  • mysql数据同步ES方案---Canal
  • Mybatis中的映射文件编写原则
  • ssh远程打开图形化程序
  • 【Linux】Anaconda下载安装配置Pytorch安装配置(保姆级)
  • ROS理论与实践学习笔记——6 ROS机器人导航(仿真)
  • 软件安全开发生命周期(Software Security Development Lifecycle, SSDLC)模型
  • 逍遥安卓模拟器命令行合集(memuc命令)
  • JsonElement 类
  • ES 全文检索完全匹配高亮查询
  • 云贝教育 |【技术文章】OpenTenBase_V2.6基于麒麟V10源码编译安装
  • leetcode动态规划(六)-不同路径(有障碍物)
  • Java学习Day25:基础篇15:反射
  • MuSig2(一种多签名方案,具有签名聚合的特性
  • Python近红外光谱数据分析技术
  • IPv6 DNS简介
  • 一种用于机械手自适应抓取控制的紧凑型指尖形视触觉传感器
  • 【银行科技岗】相关考试知识点总结及部分考题
  • 学习的文档10/14
  • 系统共享库(Shared Library)