zoukankan      html  css  js  c++  java
  • -- 1 -- springboot

    框架或工具:Lombok
    项目地址:https://github.com/h837272998/next-springboot

    一、Maven依赖

    <?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>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.7.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.hjh.myspringboot</groupId>
        <artifactId>myspringboot</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>myspringboot</name>
        <description>自学习springboot</description>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    

    二、配置文件

    springboot 提供了两种配置文件格式。分别是properties和yml。选择一种使用,结果都一样。这里选择properties。(为了看懂其他项目两者最好都掌握,喜欢用哪个看个人习惯)

    server.port t端口
    server.servlet.context-path 上下文路径
    spring.datasource.* 数据库连接配置

    server.port=8080
    server.servlet.context-path=
    
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/spring?serverTimezone=GMT&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=123456
    

    三、RESTful API

    常见的API

    操作 url method
    增加 /user/add?name=xx POST
    删除 /user/delete?id=x GET
    修改 /user/update?id=x&name=xxx POST
    获取 /user/get?id=x GET
    查询 /user/list?name=xx GET

    采用RESTful API

    操作 url method
    增加 /user POST
    删除 /user/x DELETE
    修改 /user/x PUT
    获取 /user/x GET
    查询 /user?name=xx GET

    RESTful架构:
    遵循统一接口原则,统一接口包含了一组受限的预定义操作,不论什么资源,都是通过使用相同的接口进行资源访问
    RESTful 是一种风格,不是强制标准。

    四、编写RESTful和测试用例。

    实现对用户表的增删改查

    User.java

    @Data
    public class User {
        @JsonView(View.Summary.class)
        private long id;
    
        @JsonView(View.Summary.class)
        private String username;
    
        @JsonView(View.SummaryWithDetail.class)
        private String password;
    
        @JsonView(View.SummaryWithDetail.class)
        private Date createDate;
    }
    

    View.java

    public class View {
        public interface Summary{}
        public interface SummaryWithDetail extends Summary{}
    }
    

    UserController.java

    /**
     * @Description:
     * @Author: HJH
     * @Date: 2019-08-16 21:32
     */
    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        @PostMapping
        public User add(@RequestBody User user){
            log.info("增加用户;"+user.toString());
            user.setId(1);
            return user;
        }
    
    
        //正则匹配 id只能为数字
        @DeleteMapping("/{id:\d+}")
        public void delete(@PathVariable long id){
            log.info("删除用户id:"+id);
        }
    
        @PutMapping("/{id:\d+}")
        public User update(@RequestBody User user){
            log.info("修改用户:"+user);
            return user;
        }
    
        @GetMapping("/{id:\d+}")
        public User get(@PathVariable long id){
            User user = new User();
            user.setId(1);
            user.setUsername("hjh");
            user.setPassword("123");
            return user;
        }
    
        @GetMapping
        @JsonView(View.Summary.class)
        public List<User> list(User user){
            log.info("查询用户名:" + user);
            ArrayList<User> users = new ArrayList<>();
            users.add(new User());
            users.add(new User());
            users.add(new User());
            return users;
        }
    
    }
    

    测试用例
    UserControllerTest.java

    // 引入静态对象。简化
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    
    /**
     * @Description:
     * @Author: HJH
     * @Date: 2019-08-16 21:38
     */
    @Slf4j
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserControllerTest {
    
        @Autowired
        private WebApplicationContext webApplicationContext;
    
        private MockMvc mockMvc;
    
        @Before
        public void before(){
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        }
    
        @Test
        public void whenAddSuccess() throws Exception {
            String content = "{"username":"hjh","password":null,"createDate":"+new Date().getTime()+"}";
            String result = mockMvc.perform(post("/user")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(content))  //请求
                    .andExpect(status().isOk()) //断言响应结果
                    .andExpect(jsonPath("$.id").value(1))
                    .andReturn().getResponse().getContentAsString();
            log.info("增加结果;"+result);
        }
    
        @Test
        public void whenUpdateSuccess() throws Exception {
            String content = "{"username":"hjh","password":null,"createDate":"+new Date().getTime()+"}";
            String result = mockMvc.perform(put("/user/1")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content(content))  //请求
                    .andExpect(status().isOk()) //断言响应结果
                    .andReturn().getResponse().getContentAsString();
            log.info("修改结果;"+result);
        }
    
        @Test
        public void whenDeleteSuccess() throws Exception {
            String result = mockMvc.perform(delete("/user/1")
                    .contentType(MediaType.APPLICATION_JSON_UTF8))
                    .andExpect(status().isOk()) //断言响应结果
                    .andReturn().getResponse().getContentAsString();
        }
    
        @Test
        public void whenGetSuccess() throws Exception {
            String result = mockMvc.perform(get("/user/1")
                    .contentType(MediaType.APPLICATION_JSON_UTF8))
                    .andExpect(status().isOk()) //响应结果
                    .andReturn().getResponse().getContentAsString();
            log.info("获得结果;"+result);
        }
    
        @Test
        public void whenListSuccess() throws Exception {
            String result = mockMvc.perform(get("/user").param("username","hjh")
                    .contentType(MediaType.APPLICATION_JSON_UTF8))
                    .andExpect(status().isOk()) //断言响应结果
                    .andReturn().getResponse().getContentAsString();
            log.info("查询结果;"+result);
        }
    }
    

    Jackson @JsonVIew
    @JsonView可以过滤序列化对象的字段属性,使有选择的序列化对象
    可以将View类理解为一组标识,Summary只是其中一种标识,其中DetailSummary继承了Summary
    当使用@JsonView序列化User对象的时候,就只会序列化选择的属性,可以隐藏一些不想序列化的字段属性。
    简单的说就是可以控制控制器层某个方法输出对象的属性。例如在查看用户详细信息的时候可以看到用户的所有信息。当查询所有用户时就不显示密码。

    五、数据验证

    数据验证:对前台传输的数据进行验证。

    1. 在entity定义对应成员变量的验证
      @Past:过去的时间
      @NotBlank 不能为空

    1. 需要验证的方法中添加@Valid注解
      再通过BindingResult捕获错误

    1. 常见的验证

    2. 自定义消息

    通过重写message

    3. 自定义校检注解

    定义注解MyConstraint
    在注解里面的注解称为元注解,例如Target...
    Target 作用目标:作用在METHOD(方法)和FIELD(字段)
    Retention 保留位置:RetentionPolicy.RUNTIME : 注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解。

    MyConstraint.java

    @Target({ElementType.METHOD,ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = MyConstraintValidate.class)
    public @interface MyConstraint {
    
        String message() default "测试验证";
    
        Class<?>[] groups() default { };
    
        Class<? extends Payload>[] payload() default { };
    }
    

    注解实现类

    MyConstraintValidate.java

    @Slf4j
    public class MyConstraintValidate implements ConstraintValidator<MyConstraint, Object> {
        @Override
        public void initialize(MyConstraint constraintAnnotation) {
            //初始化
        }
    
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            log.info("MyConstraintValidate: ",value);
            return false;
        }
    }
    

    通过isValid 判断值是否通过验证

    六、异常处理

    1. springboot原生异常

    覆盖默认的处理方式

    1. 自定义一个bean,实现ErrorController接口,那么默认的错误处理机制将不再生效。
    2. 自定义一个bean,继承BasicErrorController类,使用一部分现成的功能,自己也可以添加新的public方法,使用@RequestMapping及其produces属性指定新的地址映射。
    3. 自定义一个ErrorAttribute类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。
    4. 继承AbstractErrorController

    BasicErrorController.class

    SpringBoot在页面 发生异常的时候会自动把请求转到/error,SpringBoot内置了一个BasicErrorController对异常进行统一的处理.

    浏览器404

    应用程序404(postMan)

    分析

    修改浏览器响应的404
    在templates添加error/404
    404.html

    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>404</title>
    </head>
    <body>
    
        <h1>访问的页面不存在</h1>
        <p th:text="'状态码:'+${status}"></p>
        <p th:text="'错误:'+${error}"></p>
        <p th:text="'错误信息:'+${message}"></p>
        <p th:text="'路径:'+${path}"></p>
        <p th:text="'时间戳:'+${timestamp}"></p>
    </body>
    </html>
    

    结果:

    error/.. 如果需要模板渲染需要放在渲染路径。像thymeleaf,如果放在 classpath:static/errror/. 就不会被渲染,从而无法获得status等数据。

    2. 自定义异常类和全局异常

    1. 自定义异常
      UserNotExistException.java
    public class UserNotExistException extends RuntimeException {
        private long id;
    
        public UserNotExistException(long id){
            super("the user is not exist...");
            this.id = id;
        }
        public long getId() {
            return id;
        }
        public void setId(long id) {
            this.id = id;
        }
    }
    

    进行测试

    @GetMapping("/exceptionTest")
        public void test(){
            throw new UserNotExistException(1);
        }
    

    结果测试:

    添加Controller全局异常捕获

    ControllrExceptionhandler.java

    /**
     * @Description:全局控制器异常处理器
     * @Author: HJH
     * @Date: 2019-08-17 15:43
     */
    @ControllerAdvice
    public class ControllerExceptionHandler {
    
    
        /**
         * @Description:处理UserNotExistException,返回的是json对象
         * @Author: HJH
         * @Date: 2019-08-17 16:46
         * @Param: [ex]
         * @Return: java.util.Map<java.lang.String,java.lang.Object>
         */
        @ExceptionHandler(UserNotExistException.class)
        @ResponseBody
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public Map<String,Object> handleUserNotExistException(UserNotExistException ex){
            Map<String,Object> result = new HashMap<>();
            result.put("id",ex.getId());
            result.put("message", ex.getMessage());
            return result;
        }
    
        /**
         * @Description:全局Exception处理。返回的是html页面
         * @Author: HJH
         * @Date: 2019-08-17 15:56
         * @Param: [req, e]
         * @Return: ModelAndView
         */
        @ExceptionHandler(value = Exception.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception ex) throws Exception {
            ModelAndView mav = new ModelAndView();
            mav.addObject("exception", ex);
            mav.addObject("url", req.getRequestURL());
            mav.setViewName("error/errorPage");
            return mav;
        }
    
    }
    

    测试控制器

    @GetMapping("/exceptionTest")
        public void test(@RequestParam String ex){
            if("runtime".equals(ex)){
                throw new RuntimeException("this is a RuntimeException");
            }
            throw new UserNotExistException(1);
        }
    

    测试结果

    1. /user/exceptionTest?ex=user
      使用浏览器和app(PostMan)

    2. /user/exceptionTest?ex=runtime
      使用浏览器和app(PostMan)

    通过上面就可以对控制器层的异常进行捕获
    弊端,只能返回页面或者json格式。在需要json格式异常时也只是返回页面

    改进

    @ExceptionHandler(value = Exception.class)
        @ResponseBody
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public Object defaultErrorHandler(HttpServletRequest req, Exception ex) throws Exception {
    
            if ("application/json".equals(req.getHeader("Content-Type"))){
                Map<String, Object> result = new HashMap<>();
                result.put("message", ex.getMessage());
                return result;
            }else {
                ModelAndView mav = new ModelAndView();
                mav.addObject("exception", ex);
                mav.addObject("url", req.getRequestURL());
                mav.setViewName("error/errorPage");
                return mav;
            }
        }
    

    测试结果:还是ok的。但没有真正的测试 ,当是ajax请求抛出错误是否能够判断。但思路应该是可行的
    虽然加了@ResponseBody但是返回是ModelAndView时还是可以解析成页面

    七、对API的拦截

    1. 过滤器(Filter)

    定义一个时间过滤器,实现对API访问进行计时

    @Slf4j
    @Component
    public class TimeFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            log.info("TimeFilter Init");
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            log.info("time filter start");
            long begin = System.currentTimeMillis();
            filterChain.doFilter(servletRequest,servletResponse);
            log.info("the request spent :"+(System.currentTimeMillis()-begin));
            log.info("time filter finish");
        }
    
        @Override
        public void destroy() {
            log.info("TimeFilter Destroy");
        }
    }
    

    使用@Component 就可以使过滤器生效

    调用一个API,结果

    自定义的拦截器可以利用注解实现注入使过滤器生效。但是其他第三方过滤器时该如何配置。
    添加配置文件。将timeFilter的@Component去掉使用下面也可以实现添加过滤器。

    WebConfig.java

    @Configuration
    public class WebConfig {
        @Bean
        public FilterRegistrationBean timeFilter(){
            FilterRegistrationBean registrationBean = new FilterRegistrationBean();
            TimeFilter timeFilter = new TimeFilter();
            registrationBean.setFilter(timeFilter);
    
            List<String> urls = new ArrayList<>();
            urls.add("/*");  //拦截url
            registrationBean.setUrlPatterns(urls);
            return registrationBean;
        }
    }
    

    2. 拦截器(Interceptor)

    在过滤器中。使用的Filter

    是在包 javax.servlet中定义的。并不知道spring的那一套

    使用拦截器实现运行时间。但可以获取执行函数
    TimeInterceptor.java

    /**
     * @Description:
     * @Author: HJH
     * @Date: 2019-08-17 19:31
     */
    @Slf4j
    @Component
    public class TimeInterceptor implements HandlerInterceptor {
    
        /**
         * @Description:控制器方法调用之前
         * @Author: HJH
         * @Date: 2019-08-17 19:35
         * @Param: [request, response, handler]
         * @Return: boolean
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
            log.info("Interceptor perHandle");
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            log.info("Interceptor Class Name: "+handlerMethod.getBean().getClass().getName());
            log.info("Interceptor Method Name: "+handlerMethod.getMethod().getName());
            request.setAttribute("startTime",System.currentTimeMillis());
            return true;
        }
        /**
         * @Description:控制器完成之后。但当控制器抛出错误时就不会进入该函数
         * @Author: HJH
         * @Date: 2019-08-17 19:36
         * @Param: [request, response, handler, modelAndView]
         * @Return: void
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                               @Nullable ModelAndView modelAndView) throws Exception {
            log.info("Interceptor postHandle");
            long start = (long) request.getAttribute("startTime");
            log.info("Time Interceptor Spent:"+(System.currentTimeMillis()-start));
        }
    
        /**
         * @Description:类似try-catch 的finally
         * @Author: HJH
         * @Date: 2019-08-17 19:37
         * @Param: [request, response, handler, ex]
         * @Return: void
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                    @Nullable Exception ex) throws Exception {
            log.info("Interceptor afterCompletion");
            long start = (long) request.getAttribute("startTime");
            log.info("Time Interceptor Spent:"+(System.currentTimeMillis()-start));
            log.info("Exception is "+ex);
        }
    }
    
    

    WebInterceptorConfig.java

    @Configuration
    public class WebInterceptorConfig implements WebMvcConfigurer {
    
        @Autowired
        private TimeInterceptor timeInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(timeInterceptor);
        }
    }
    

    运行结果

    可以看到 afterCompletion一定会执行的

    3. 切片(Aspect)

    spring aop

    切片实现
    要点:切入点 (通过注解)1. 在什么方法上起作用。2. 在什么时候起作用。
    要点:增强(通过方法):起作用时执行的业务逻辑

    TimeAspect.java

    @Slf4j
    @Aspect
    @Component
    public class TimeAspect {
    
        @Around("execution(* com.hjh.myspringboot.myspringboot.web.controller.UserController.*(..))")
        public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable {
    
            log.info("Time Aspect Start");
            //获取传输args
            Object[] args = pjp.getArgs();
            for (Object arg:args){
                log.info("arg is:" +arg);
            }
            long start = System.currentTimeMillis();
            Object o = pjp.proceed();
            log.info("Time Aspect spent:"+(new Date().getTime()-start));
            log.info("Time Aspect end");
            return o;
        }
    }
    

    结果

    总结

    拦截顺序

    七、文件上传下载

    比较简单的实现和单元测试
    增加单元测试

    @Test
        public void whenUploadSuccess() throws Exception {
            String result = mockMvc.perform(multipart("/file")
            .file(new MockMultipartFile("file","test.txt","multipart/form-data","hello".getBytes("utf-8"))))
                    .andExpect(status().isOk())
            .andReturn().getResponse().getContentAsString();
            log.info("上传结果"+result);
        }
    

    fileupload在springboot2.0x过时。

    FileController

    @Slf4j
    @RestController
    @RequestMapping("/file")
    public class FileController  {
    
        public static final String  FOLDER = "D:/U";
    
        @PostMapping
        public Map upload(MultipartFile file) throws IOException {
            log.info("上传文件名"+file.getName());
            log.info("上传文件原始名"+file.getOriginalFilename());
            log.info("上传文件大小:"+file.getSize());
    
    
            File file1 = new File(FOLDER,System.currentTimeMillis()+".txt");
            file.transferTo(file1);
            Map<String, String> map = new HashMap<>();
            map.put("path",file1.getAbsolutePath());
            return map;
        }
    
        @GetMapping("/{id}")
        public void download(@PathVariable String id, HttpServletResponse response, HttpServletRequest request) throws IOException {
    
            try(FileInputStream inputStream = new FileInputStream(new File(FOLDER, id + ".txt"));
                OutputStream outputStream = response.getOutputStream()) {
                    response.setContentType("application/x-download");
                    response.addHeader("Content-Disposition","attachment;filename=test.txt");
    
                IOUtils.copy(inputStream,outputStream);
                outputStream.flush();
            }
        }
    
    }
    

    八、异步处理

    当同时访问一个需要处理10s的方法时

    结果

    通过输出:
    第一个访问过来10s后第二个访问才被处理。也就是说在当前方法中。同一时间只能处理一个请求。只有当前请求处理完后才能处理下一个。

    1、使用Runable异步

    @GetMapping("async2")
        public Callable<String> async2(){
            log.info("主线程开始");
            Callable<String> result = new Callable<String>() {
                @Override
                public String call() throws Exception {
                    log.info("副线程开始");
                    for (int i=0;i<10;i++){
                        Thread.sleep(1000);
                    }
                    log.info("副线程返回");
                    return "success";
                }
            };
            log.info("主线程结束");
            return result;
        }
    

    在同一时间可以处理同一类请求,提高了服务器的吞吐量
    场景比较单一,添加一个副线程。

    2. 使用DeferredResult异步处理请求

    使用消息队列解决请求丢失等问题,可以用于订单处理等,重要的,高可用,高并发的请求。

    下面使用异步模拟处理订单

    使用RibbitMq作为消息队列

    添加maven依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    

    添加mq配置

    spring.application.name=spring-boot
    spring.rabbitmq.host=127.0.0.1
    spring.rabbitmq.port=5672
    spring.rabbitmq.username=admin
    spring.rabbitmq.password=123456
    

    添加springboot配置
    使用Exchange类型的Direct。添加两条队列。一个用于接收下单,一个接收订单处理结果。
    RabbitMQConfig.java

    Configuration
    public class RabbitMQConfig {
    
        @Bean
        public Queue queue1(){
            return new Queue("order");
        }
    
        @Bean
        public Queue queue2(){
            return new Queue("finish");
        }
    }
    

    DeferredResultHolder.java

    @Component
    public class DeferredResultHolder {
        private Map<String , DeferredResult<String>> map = new HashMap<>();
    
        public Map<String, DeferredResult<String>> getMap() {
            return map;
        }
    
        public void setMap(Map<String, DeferredResult<String>> map) {
            this.map = map;
        }
    }
    
    1. 线程1:
    @Autowired
        private AmqpTemplate rabbitTemplate;
    
        @GetMapping("async4")
        public DeferredResult<String> async4() throws InterruptedException {
            log.info("主线程开始");
            String orderId = RandomStringUtils.randomNumeric(8);
            log.info("2.发送下单请求:"+orderId);
            rabbitTemplate.convertAndSend("order",orderId);
            DeferredResult<String> result = new DeferredResult();
            deferredResultHolder.getMap().put(orderId,result);
            return result;
        }
    
    1. 应用2:监听请求并处理
    @Slf4j
    @Component
    @RabbitListener(queues = "order")
    public class OrderReceiver {
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @RabbitHandler
        public void process(String re) {
    
            new Thread(()->{
                log.info("3.监听到下单请求:"+re);
                //处理
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //处理完成
                log.info("4.订单处理完成:"+re);
                rabbitTemplate.convertAndSend("finish",re);
            }).start();
        }
    
    1. 线程2:接收完成订单,并响应请求。
    @Slf4j
    @Component
    @RabbitListener(queues = "finish")
    public class OrderFinishThread {
    
        @Autowired
        private DeferredResultHolder deferredResultHolder;
    
        @RabbitHandler
        public void process(String re) {
            new Thread(()->{
                log.info("5.监听到完成的订单2:"+re);
                deferredResultHolder.getMap().get(re).setResult("订单成功");
            }).start();
        }
    }
    
    

    使用jmeter对Callable和DeferredResult异步进行简单的测试
    并发100个请求
    Callable

    DeferredResult

  • 相关阅读:
    day08超市商品库存案例
    day07
    day06_03
    day06_02
    day06_01
    最简单的库存管理java案例
    Day05_homework
    方法的使用注意事项
    day05
    冒泡排序
  • 原文地址:https://www.cnblogs.com/hjh614/p/11374363.html
Copyright © 2011-2022 走看看