这篇3万字的Java后端面试总结,面试官看了瑟瑟发抖(三)

2022-05-05 16:58:16 浏览数 (1)

Linux

❝问:linux中怎么看日志,怎么看进程,怎么看磁盘大小,怎么看内存大小❞

代码语言:javascript复制
#看日志
tail -f xx.log
#看进程
jps   / ps -ef | grep xx     /netstat -tnlp | grep xx
#看磁盘大小
du   /  df
#看内存大小
free

更多Linu命令见文章:我在工作中用到的Linux命令

Spring相关

❝问:Spring事务 A,B 。A调B, A异常,B会回滚么❞

「事务传播类型:」

务传播行为类型

说明

PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY

使用当前的事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER

以非事务方式执行,如果当前存在事务,则抛出异常。

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

默认传播类型为:PROPAGATION_REQUIRED,上面问题,A,B会合并成一个事务,所以A异常,B会回滚,B异常,A会回滚。

❝问:Spring中 @Autowired与@Resource区别❞

  • 「@Autowired是Spring的注解,Autowired」默认先按 「byType」,如果发现找到多个 「bean」,则,又按照 「byName」方式比对,如果还有多个,则报出异常;
  • 「@Resource 是JDK1.6支持的注解」,默认按照名称( 「ByName」)进行装配, 如果没有指定 「name」属性,当注解写在字段上时,默认取字段名,按照名称查找,如果注解写在 「setter」方法上默认取属性名进行装配。当找不到与名称匹配的 「bean」时才按照类型进行装配。

推荐文章:https://mp.weixin.qq.com/s/IglQITCkmx7Lpz60QOW7HA

❝问:单体服务到微服务的演变史❞

演变:单体服务 -> SOA -> 微服务

「单体服务」

概念:所有功能全部打包在一起。应用大部分是一个war包或jar包。我参与网约车最开始架构是:一个乘客项目中有 用户、订单、消息、地图等功能。随着业务发展,功能增多,这个项目会越来越臃肿。

好处:容易开发、测试、部署,适合项目初期试错。

坏处:

随着项目越来越复杂,团队不断扩大。坏处就显现出来了。

  • 复杂性高:代码多,十万行,百万行级别。加一个小功能,会带来其他功能的隐患,因为它们在一起。
  • 技术债务:人员流动,不坏不修,因为不敢修。
  • 持续部署困难:由于是全量应用,改一个小功能,全部部署,会导致无关的功能暂停使用。编译部署上线耗时长,不敢随便部署,导致部署频率低,进而又导致两次部署之间 功能修改多,越不敢部署,恶性循环。
  • 可靠性差:某个小问题,比如小功能出现OOM,会导致整个应用崩溃。
  • 扩展受限:只能整体扩展,无法按照需要进行扩展, 不能根据计算密集型(派单系统)和IO密集型(文件服务) 进行合适的区分。
  • 阻碍创新:单体应用是以一种技术解决所有问题,不容易引入新技术。但在高速的互联网发展过程中,适应的潮流是:用合适的语言做合适的事情。比如在单体应用中,一个项目用spring MVC,想换成spring boot,切换成本很高,因为有可能10万,百万行代码都要改,而微服务可以轻松切换,因为每个服务,功能简单,代码少。

「SOA」

对单体应用的改进:引入SOA(Service-Oriented Architecture)面向服务架构,拆分系统,用服务的流程化来实现业务的灵活性。服务间需要某些方法进行连接,面向接口等,它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务 通常以独立的形式存在于操作系统进程中。各个服务之间 通过网络调用。但是还是需要用些方法来进行服务组合,有可能还是个单体应用。

所以要引入微服务,是SOA思想的一种具体实践。

微服务架构 = 80%的SOA服务架构思想 100%的组件化架构思想。

「微服务」

  • 无严格定义。
  • 微服务是一种架构风格,将单体应用划分为小型的服务单元。
  • 微服务架构是一种使用一系列粒度较小的服务来开发单个应用的方式;每个服务运行在自己的进程中;服务间采用轻量级的方式进行通信(通常是HTTP API);这些服务是基于业务逻辑和范围,通过自动化部署的机制来独立部署的,并且服务的集中管理应该是最低限度的,即每个服务可以采用不同的编程语言编写,使用不同的数据存储技术。

那么微服务有哪些「特性」呢:

代码语言:javascript复制
独立运行在自己进程中。

一系列独立服务共同构建起整个系统。

一个服务只关注自己的独立业务。

轻量的通信机制RESTful API。

使用不同语言开发。

全自动部署机制

「微服务优点」

  1. 独立部署。不依赖其他服务,耦合性低,不用管其他服务的部署对自己的影响。
  2. 易于开发和维护:关注特定业务,所以业务清晰,代码量少,模块变的易开发、易理解、易维护。
  3. 启动块:功能少,代码少,所以启动快,有需要停机维护的服务,不会长时间暂停服务。
  4. 局部修改容易:只需要部署 相应的服务即可,适合敏捷开发。
  5. 技术栈不受限:java,node.js等
  6. 按需伸缩:某个服务受限,可以按需增加内存,cpu等。
  7. 职责专一。专门团队负责专门业务,有利于团队分工。
  8. 代码复用。不需要重复写。底层实现通过接口方式提供。
  9. 便于团队协作:每个团队只需要提供API就行,定义好API后,可以并行开发。

「微服务缺点」

  1. 分布式固有的复杂性:容错(某个服务宕机),网络延时,调用关系、分布式事务等,都会带来复杂。
  2. 分布式事务的挑战:每个服务有自己的数据库,有点在于不同服务可以选择适合自身业务的数据库。订单用MySQL,评论用Mongodb等。目前最理想解决方案是:柔性事务的最终一致性。
  3. 接口调整成本高:改一个接口,调用方都要改。
  4. 测试难度提升:一个接口改变,所有调用方都得测。自动化测试就变的重要了。API文档的管理也尤为重要。推荐:yapi。
  5. 运维要求高:需要维护 几十 上百个服务。监控变的复杂。并且还要关注多个集群,不像原来单体,一个应用正常运行即可。
  6. 重复工作:比如java的工具类可以在共享common.jar中,但在多语言下行不通,C 无法直接用java的jar包。

「什么是刚性事务?」

代码语言:javascript复制
刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致状态。满足BASE理论的事务,我们称之为“柔性事务”。

关于如何设计划分服务,我觉得可以学习下DDD领域驱动设计,有很好的指导作用。

❝问:AOP怎么实现的Redis缓存注解❞

「1.定义注解」

代码语言:javascript复制
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomizeCache {

    String key();

    String value();

    long expireTimes() default 120L; //默认过期时间120s

    int semaphoreCount() default Integer.MAX_VALUE;  //默认限制线程并发数
}

「2.AOP切面编程」

代码语言:javascript复制
@Component
@Aspect
@Slf4j
public class CacheAspect {
    @Autowired
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.lvshen.demo.redis.cache.CustomizeCache)")
    public void cachePointcut() {
    }

    @Around("cachePointcut()")
    public Object doCache(ProceedingJoinPoint point) {
        Object value = null;
        Semaphore semaphore = null;
        MethodSignature signature = (MethodSignature) point.getSignature();

        try {
            //获取方法上注解的类容
            Method method = point.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
            CustomizeCache annotation = method.getAnnotation(CustomizeCache.class);
            String keyEl = annotation.key();
            String prefix = annotation.value();
            long expireTimes = annotation.expireTimes();
            int semaphoreCount = annotation.semaphoreCount();

            //解析SpringEL表达式
            SpelExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(keyEl);
            StandardEvaluationContext context = new StandardEvaluationContext();

            //添加参数
            Object[] args = point.getArgs();
            DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
            String[] parameterNames = discoverer.getParameterNames(method);
            for (int i = 0; i < parameterNames.length; i  ) {
                context.setVariable(parameterNames[i], args[i].toString());
            }

            //解析
            String key = prefix   "::"   expression.getValue(context).toString();

            //判断缓存中是否存在
            value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                log.info("从缓存中读取到值:{}", value);
                return value;
            }

            //自定义组件,如:限流,降级。。。
            //创建限流令牌
            semaphore = new Semaphore(semaphoreCount);
            boolean tryAcquire = semaphore.tryAcquire(3000L, TimeUnit.MILLISECONDS);
            if (!tryAcquire) {
                //log.info("当前线程【{}】获取令牌失败,等带其他线程释放令牌", Thread.currentThread().getName());
                throw new RuntimeException(String.format("当前线程【%s】获取令牌失败,等带其他线程释放令牌", Thread.currentThread().getName()));
            }

            //缓存不存在则执行方法
            value = point.proceed();

            //同步value到缓存
            redisTemplate.opsForValue().set(key, value, expireTimes, TimeUnit.SECONDS);


        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            if (semaphore == null) {
                return value;
            } else {
                semaphore.release();
            }
        }
        return value;
    }
}

「3.使用」

代码语言:javascript复制
@CustomizeCache(value = "member", key = "#name")
public List<Member> listByNameSelfCache(String name) {
    return memberMapper.listByName(name);
}

❝问:Spring Boot注解❞

「@EnableAutoConfiguration」:是自动配置的注解;

「@Configuration」:用于定义配置类;

「@ConditionalOnBean(A.class)」:仅仅在当前上下文中存在A对象时,才会实例化一个Bean;

关于Conditional开头的注解还有很多,有兴趣的可以去Spring官网:https://spring.io/projects/spring-boot

或者SpringBoot中文社区看看:https://springboot.io/

❝问:Bean的生命周期❞

简略的说,单列Bean生命随容器存在而存在。非单例的Bean不引用时就会被垃圾回收器回收。

❝问:AOP原理❞

AOP的思想是,不去动原来的代码,而是基于原来代码产生代理对象,通过代理的方法,去包装原来的方法,就完成了对以前方法的增强。AOP的底层原理就是动态代理的实现。

关于AOP的使用,比如我之前用AOP思想做的缓存注解等。

「切入点」:通过一个表达式告诉SpringAOP去哪个地方进行增强。也可以把这个表达式理解为一个查询条件,系统会根据这个查询条件查询到我们要进行增强的代码位置。

「连接点」:就是SpringAOP通过告诉它的切入点的位置找的的具体的要增强的代码的位置,这个代码位置就是连接点。

「切面」:切面由一组(增强处理和切入点)共同构成。

「目标对象」:目标对象就是被增强的目标类。我们也称之为委托类。

「AOP代理」:代理类就是AOP代理,里面包含了目标对象以及一些增强处理。系统会用AOP代理类代替委托类去执行功能。

「织入」:织入就是将我们的增强处理增强到指定位置的过程。

具体使用可以看看问题:「如何用AOP实现缓存注解」的代码。

❝问:Spring的动态代理❞

动态代理其实就是Java中的一个方法,这个方法可以实现:「动态创建一组指定的接口的实现对象(在运行时,创建实现了指定的一组接口的对象)」

分为JDK动态代理和Cglib动态代理

当目标对象实现了接口,默认使用JDK动态代理,也可以强制使用Cglib动态代理。

当目标对象没有实现接口,必须使用Cglib动态代理。

下面是代码:

代码语言:javascript复制
public interface UserService {
    void addUser(String name, String password);

    void delUser(String name);
}


public class UserServiceImpl implements UserService{
    @Override
    public void addUser(String name, String password) {
        System.out.println("调用addUser()...");
        System.out.println(String.format("参数为:name[%s],password[%s]",name,password));
    }

    @Override
    public void delUser(String name) {
        System.out.println("调用delUser()");
        System.out.println(String.format("参数为:name[%s]",name));
    }
}

「JdkProxy」

代码语言:javascript复制
public class JdkProxy  implements InvocationHandler {
    //需要代理的目标对象
    private Object target;
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK动态代理,监听开始...");
        Object invoke = method.invoke(target, args);
        System.out.println("JDK动态代理,监听结束...");
        return invoke;
    }

    public Object getJdkProxy(Object targetObject) {
        this.target = targetObject;
        //实例化
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);
    }
}

测试JdKProxy

代码语言:javascript复制
@org.junit.Test
public void testJdkProxy() {
    JdkProxy jdkProxy = new JdkProxy();
    UserService userService = (UserService) jdkProxy.getJdkProxy(new UserServiceImpl());

    userService.addUser("lvshen","123456");

}

「CglibProxy」

代码语言:javascript复制
public class CglibProxy implements MethodInterceptor {
    private Object target;
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLIB动态代理,监听开始...");
        Object invoke = method.invoke(target, objects);
        System.out.println("CGLIB动态代理,监听结束...");
        return invoke;
    }

    public Object getCglibProxy(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        //指定父类
        enhancer.setSuperclass(target.getClass());

        enhancer.setCallback(this);
        Object result = enhancer.create();
        return result;
    }
}

测试CglibProxy

代码语言:javascript复制
public class Test {
    @org.junit.Test
    public void testCglibProxy() {
        CglibProxy cglibProxy = new CglibProxy();
        UserService service = (UserService) cglibProxy.getCglibProxy(new UserServiceImpl());
        service.addUser("zhouzhou","654321");
    }
}

❝问:Spring与SpringBoot的区别❞

「SpringBoot特点」

  • 用来实现微服务;
  • 自动配置
  • 自定义配置
  • 模块化
  • 独立打包 直接运行
  • 内嵌服务器

❝问:springboot中bootstrap.properties与application.properties的区别❞

「区别:」

  • application.properties(application.yml)系统级别的一些参数配置,这些参数一般是不会变动的
  • bootstrap.properties(bootstrap.yml)定义应用级别的配置

「SpringBoot」 有两种上下文:

  • 「bootstrap」:应用程序的父上下文
  • 「application」:应用程序上下文

「bootstrap」 加载优先于 applicaton

「bootstrap」 里面的属性会优先加载,默认也不能被本地相同配置覆盖

「应用场景:」

  • 使用 Spring Cloud Config Server时,在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息。eg:指定spring.application.namespring.cloud.config.server.git.uri
  • 一些固定的不能被覆盖的属性
  • 一些加密/解密的场景

❝问:applicationContext与beanFactory的区别❞

两者都能获取bean.

beanFactory:懒加载,调用getBean是才实例化对象

applicationContext:预加载,启用applicationContext就实例化对象了

ApplicationContext 包含 BeanFactory 的所有特性,通常推荐使用前者。但是也有一些限制情形,比如移动应用内存消耗比较严苛,在那些情景中,使用更轻量级的 BeanFactory 是更合理的。然而,在大多数企业级的应用中,ApplicationContext 是你的首选。

代码语言:javascript复制

    public class HelloWorldApp {
        public static void main(String[] args) {
            XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
            HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");
            obj.getMessage();
        }
    }

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
    obj.getMessage();
}
   

MyBatis

❝问:Mybatis中 #{} 与 ${}的区别❞

使用#{parameterName}引用参数的时候,Mybatis会把这个参数认为是一个字符串,并自动加上'',例如传入参数是“Smith”,那么在下面SQL中:

代码语言:javascript复制
Select * from emp where name = #{employeeName}

使用的时候就会转换为:

代码语言:javascript复制
Select * from emp where name = 'Smith'; 

同时使用${parameterName}的时候在下面SQL中

代码语言:javascript复制
Select * from emp where name = ${employeeName}

就会直接转换为:

代码语言:javascript复制
Select * from emp where name = Smith

简单说#{}是经过预编译的,是安全的。而${}是未经过预编译的,「仅仅是取变量的值,是非安全的,存在SQL注入」

sql注入问题:

当使用#{}时

代码语言:javascript复制
DEBUG [http-nio-8080-exec-5] - ==>  Preparing: select * from user where account = ? and password = ?  DEBUG [http-nio-8080-exec-5] - ==> Parameters: 20200801(String), 111111 or account = 'admin' (String) DEBUG [http-nio-8080-exec-5] - <==      Total: 0 返回结果:null

当使用${}时

代码语言:javascript复制
DEBUG [http-nio-8080-exec-5] - ==>  Preparing: select * from user where account = ? and password = ?  DEBUG [http-nio-8080-exec-5] - ==> Parameters: 201301001(String), 111111 or account = 'admin' (String) DEBUG [http-nio-8080-exec-5] - <==      Total: 0 转换为实际的SQL语句:select * from user where account = '20200801' and password = '111111 or account = 'admin'' 

设计模式

❝问:介绍下DDD领域驱动设计,是说的什么,里面分为哪些模块❞

这个说起来比较复杂。这种设计模式更加趋近于现实世界的状态,要求我们写代码时要区分业务代码与非业务代码。

推荐文章:https://www.cnblogs.com/cuiqq/p/10961337.html

❝问:设计模式分为哪种类❞

❝问:充血模型与贫血模型❞

一、贫血模型

所谓贫血模型,是指Model 中,仅包含状态(属性),不包含行为(方法),采用这种设计时,需要分离出DB层,专门用于数据库操作。

代码语言:javascript复制
@Data
public class Employee {
        public string Id ;
        public string Name ;
        public string Sex ;
        public DateTime? BirthDay;
      
        /// 直属上级的Id
        public string ParentId ;
 }
代码语言:javascript复制
//实现方法略    
public class EmpDAO {
        public static bool AddEmployee(Employee emp);
        public static bool UpdateEmployee(Employee emp);
        public static bool DeleteEmployee(Employee emp);
        public static Employee GetEmployeeById(string Id);
}

二、充血模型

Model 中既包括状态,又包括行为,是最符合面向对象的设计方式。

代码语言:javascript复制
@Data
public class Employee {
        public string Id ;
        public string Name ;
        public string Sex ;
        public DateTime;
  
        /// 直属上级的Id
        public string ParentIdl;
        private Employee _parent;

        public static Employee query(string id){
            Employee emp = new Employee();
            //实现略,仅需填充emp的熟悉即可
            return emp;
        }
    
        //保存对象,实现略
        public bool Save() {
            return true;
        }
        
        // 删除对象,实现略
        public bool Drop(){
            return true;
        }
      
    }

笔试题

❝问:手写单列模式❞

见文章:那些能让人秀出花的单列模式

代码语言:javascript复制
public class LazySimpleSingleton {
    private static volatile LazySimpleSingleton instance = null;

    private LazySimpleSingleton(){
        if (instance != null) {
            throw new RuntimeException("该构造方法禁止获取");
        }
    }

    public static LazySimpleSingleton getInstance() {
        if (instance == null) {
            synchronized (LazySimpleSingleton.class) {
                if (instance == null) {
                    instance = new LazySimpleSingleton();
                }
            }
        }
        return instance;
    }
}

❝问:手写冒泡排序❞

见文章:只知道冒泡排序?来看看这些排序算法

❝问:评测题目: 三个线程A、B、C,实现一个程序让线程A打印“A”,线程B打印“B”,线程C打印“C”, 三个线程输出ABCABCABC......ABC,循环10次“ABC”❞

见文章:阿里多线程面试题-按线程顺序输出

❝问:Description:给出有序数组(非递减)和闭区间, 找出数组中在区间之内的元素起始位置和结束位置

  • 输入:
    1. 有序数组[1,1,2,3,4,5,5]
    2. 闭区间[-3,3]
  • 输出:[0,3]
  • 解释:在数组中,前4个元素在区间之内,则起始位置为0,结束位置为3
  • 要求:最坏情况时间复杂度小于O(n)

见文章:腾讯云算法面试题-给定边界输出对应索引下标

❝问:写一个二分查找算法❞

代码语言:javascript复制
public static int getIndex(int[] arr, int key) {
        int mid = arr.length / 2;
        if (arr[mid] == key) {
            return mid;
        }
        int start = 0;
        int end = arr.length - 1;
        while (start <= end) {
            mid = (end - start) / 2   start;
            if (arr[mid] == key) {
                return mid;
            } else if (arr[mid] > key) {
                end = mid - 1;
            } else {
                start = start   1;
            }
        }
        //找不到,返回-1
        return -1;
    }

0 人点赞